#!/bin/sh
# CLEO_MANAGED_HOOK v1
# T1588 — project-agnostic T-ID enforcement for every commit being pushed.
#
# git invokes this with `<remote> <url>` as args and feeds ref updates on
# stdin: `<local-ref> <local-sha> <remote-ref> <remote-sha>` (one per line).
#
# For each new commit being pushed (commits in `local-sha` that are NOT
# already in any remote ref), require a T-ID in the subject. Same allow-
# list as commit-msg: merge / revert / fixup / squash / amend.
#
# Override: `git push --no-verify` (standard git override).
#
# T1595:reconcile-extension-point
# Pre-push reconcile gate hooks here (see T1595 worker).
# Reserved range below — DO NOT remove these markers; T1595 extends here.
# T1595:reconcile-extension-point-end
#
# POSIX `/bin/sh` only. No node/pnpm dependency.
set -e

# Capture once so task-ID validation and project checks receive identical refs.
REF_INPUT=$(mktemp "${TMPDIR:-/tmp}/cleo-pre-push.XXXXXX") || {
  printf '%s\n' 'cleo pre-push: cannot capture refs; infrastructure failure, allowing push' >&2
  exit 0
}
trap 'rm -f "$REF_INPUT"' EXIT HUP INT TERM
if ! cat > "$REF_INPUT"; then
  printf '%s\n' 'cleo pre-push: cannot read refs; infrastructure failure, allowing push' >&2
  exit 0
fi

ZERO_SHA="0000000000000000000000000000000000000000"
EMPTY_TREE_SHA=""
FAIL=0
FAIL_LOG=""

check_subject() {
  subject="$1"
  sha="$2"
  case "$subject" in
    'Merge '*) return 0 ;;
    'Revert '*) return 0 ;;
    'fixup! '*) return 0 ;;
    'squash! '*) return 0 ;;
    'amend! '*) return 0 ;;
    # T13577: the empty commit `cleo init` makes so HEAD resolves. Exact
    # subject only, never a prefix, and only for a parentless (root) commit
    # (T13582), so it opens no bypass for later commits.
    'initial: cleo init')
      if [ "$(git rev-list --parents -n 1 "$sha" 2>/dev/null | wc -w)" -eq 1 ]; then
        return 0
      fi
      ;;
  esac
  if printf '%s' "$subject" | grep -Eq 'T[0-9]+'; then
    return 0
  fi
  return 1
}

# Read each ref-update line from stdin.
while read -r local_ref local_sha remote_ref remote_sha; do
  # Branch deletion (push :branch) — nothing to validate.
  if [ "$local_sha" = "$ZERO_SHA" ]; then
    continue
  fi

  # Scan range: commits reachable from local_sha that are NOT reachable
  # from any remote ref. This is rebase-safe — after `git rebase origin/main`
  # the new base commits (which include release-ship / chore(changelog) /
  # ci: nudge commits that legitimately lack T-IDs) are already on
  # `origin/main` and therefore excluded from the scan. The naive
  # `$remote_sha..$local_sha` form would include those rebased-on commits
  # because the OLD remote_sha is no longer an ancestor of the new
  # local_sha after a rebase. Using `--not --remotes` for BOTH new and
  # existing branches gives consistent, project-agnostic behaviour.
  #
  # T10488 (Saga T9862 Wave 5) — narrow scan to truly-local commits.
  range="$local_sha --not --remotes"

  # Subject extraction: %s = subject. NUL-delimit for safety.
  # shellcheck disable=SC2086
  commits=$(git rev-list $range 2>/dev/null || true)
  for sha in $commits; do
    subject=$(git log -1 --pretty=%s "$sha" 2>/dev/null || true)
    if [ -z "$subject" ]; then
      continue
    fi
    if ! check_subject "$subject" "$sha"; then
      FAIL=1
      FAIL_LOG="${FAIL_LOG}  ${sha}  ${subject}
"
    fi
  done
done < "$REF_INPUT"

if [ "$FAIL" -ne 0 ]; then
  cat >&2 <<EOF
cleo pre-push hook: refusing push — commits are missing task IDs.

${FAIL_LOG}
Every commit MUST reference at least one CLEO task (e.g. \`T1588\`).
Fix the subjects (\`git rebase -i\` + reword), or override with:

  git push --no-verify

EOF
  exit 1
fi

# Existing projects remain unchanged: the shared runner skips disabled hooks.
# T13774: an ungated push names its reason on one line, and says when this
# checkout has project hooks activated, so a stale cleo on PATH is never silent.
ungated() {
  active=""
  for dir in "$(git rev-parse --git-path cleo-project-hooks 2>/dev/null)" \
    "$(git rev-parse --git-common-dir 2>/dev/null)/cleo-project-hooks"; do
    if grep -q '"enabled":true' "$dir/activation.json" 2>/dev/null; then
      active=" Project hooks are ACTIVATED in this checkout but did not run."
    fi
  done
  printf 'cleo pre-push: WARNING: push NOT gated by project hooks: %s.%s Run cleo doctor hooks.\n' "$1" "$active" >&2
}
if command -v cleo >/dev/null 2>&1; then
  where=$(command -v cleo)
  version=$(cleo --version 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -n 1)
  capability=$(cleo hook run --probe -- execution < /dev/null 2>/dev/null) || capability=""
  case "$capability" in
    CLEO_PROJECT_HOOK_EXEC) ;;
    CLEO_PROJECT_HOOK_INERT)
      ungated "cleo ${version:-?} at $where runs no project hooks (hooks-v1 is not released in that build)"
      exit 0
      ;;
    CLEO_PROJECT_HOOK_V1)
      # A build before T13774: stable releases before 2026.10.17 carry the runner but keep it inert.
      if printf '%s\n' "$version" | awk -F. '!/-canary\./ && NF >= 3 { exit !($1 * 100000 + $2 * 1000 + $3 < 202610017) } { exit 1 }'; then
        ungated "cleo $version at $where predates hooks-v1 and runs no project hooks"
        exit 0
      fi
      ;;
    *)
      ungated "cleo ${version:-?} at $where has no project hook runner"
      exit 0
      ;;
  esac
  status=0
  cleo hook run --source git --event pre-push -- "$@" < "$REF_INPUT" || status=$?
  case "$status" in
    0) ;;
    1) exit 1 ;; # An explicit project verdict/checker policy blocks.
    *) ungated "the project hook runner failed (exit $status)" ;;
  esac
else
  ungated "cleo is not on PATH"
fi
exit 0
