#!/usr/bin/env bash
# pre-push -- VERIFY THE EXACT OBJECT BEING PUBLISHED: every pushed tip is
# materialized as a detached checkout and checked there, so a red head never
# leaves the box.
#
# WHY. The object VERIFIED was not the object PUBLISHED, five times in seven
# days on the ERP instance (its coordinator's ask, 2026-10-08): the pre-commit
# judged the working tree while the hosted lint judged the commit; a merge
# commit was pushed alone before its regeneration commit (f9ba676, 2026-10-07);
# a gate was never re-run after its subject changed. The pre-commit WARNS,
# never blocks, while a conflicted merge is being concluded, and a RENDERED
# INSTANCE has no merge window to re-check what was pushed -- so the check
# belongs at the push, on the pushed commit, never on the working tree.
#
# WHAT IT DOES. For each ref update whose new sha is not the zero sha (a
# deletion pushes no tree and is passed), it materializes the pushed commit as
# a detached checkout in a temp dir and, there, reads TWO TABLES FROM THE
# PUSHED TREE (never the working tree's copy):
#
#   .githooks/pre-push-checks.tsv   name, arming path, command. The
#       regeneration checks (in check mode) plus one row named `lint`, the
#       tree's own lint recipe. A row runs only when the file it names exists
#       in that tree, so one table serves argon and every rendered instance.
#   .githooks/push-policy.tsv       branch glob (matched against the full
#       ref, `refs/heads/main`), the regeneration rows to run (`all`, `none`,
#       or names joined by commas), and whether the lint leg runs (`yes`,
#       `no`, or `merge` = only when the pushed head has two or more parents).
#       The FIRST matching row wins. A tree WITHOUT the table, or a ref no row
#       matches, runs every regeneration row AND the lint leg: fail closed
#       toward checking.
#
# Each leg prints one line with its elapsed seconds, so a push's cost is
# measured, not assumed:
#   pre-push: leg <name> green|RED <n>s on <sha12> (<ref>)
#   pre-push: leg lint not run on <sha12> (<ref>) -- policy row <glob>: lint=<v>
# Any red leg REFUSES the push, by leg, sha and the leg's last lines, with the
# recipe:
#
#   THE RECIPE:  run the regenerators, commit, push again (a red `lint` leg:
#                fix what its lines name, commit, push again).
#
# THE LANDING MARKER. scripts/land.sh pushes main only after its gate round is
# green on the exact tip it pushes, and it says so to this hook by setting
#   ARGON_LAND_GATED_TIP=<the sha it gated>
# on that one push command. A ref update whose new sha EQUALS that value passes
# without re-running anything (the round already ran it); any other sha -- a
# stale export in some shell included -- is checked as usual.
#
# THE OVERRIDE. `git push --no-verify` skips this hook entirely; it is the
# operator's override, and the only one. It leaves a trace this hook cannot
# write, so it is READ instead: `bash .githooks/pre-push --audit <ref>` checks
# <ref>'s tip the same way (the regeneration rows its branch's policy row
# names; never the lint leg, which is the push's cost, not the audit's), and
# the instance refresh driver prints its verdict on the GITHOOKS-OK line
# (`tip-audit=RED <sha> (pushed past pre-push: --no-verify or before the hook)`).
#
# USAGE (git calls it as `pre-push <remote> <url>` with ref lines on stdin):
#   bash .githooks/pre-push --help          this header
#   bash .githooks/pre-push --audit <ref>   check <ref>'s tip (green | RED <sha>)
# EXIT: 0 pass, 1 refused (or a check that could not run: fails closed).
# ---- end of header
set -uo pipefail

HDR_END='^# ---- end of header'
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
  sed -n "2,/$HDR_END/p" "$0" | sed '$d' | sed 's/^# \{0,1\}//'
  exit 0
fi

ZERO_RE='^0+$'

# The repo binding git exports to hooks must not reach commands aimed at the
# temp checkout (the pre-commit's "A HOOK IS A GIT SUBPROCESS" rule): the list
# is DERIVED from git, never copied, and an empty list fails closed.
UNSET=()
while IFS= read -r v; do [ -n "$v" ] && UNSET+=(-u "$v"); done <<<"$(git rev-parse --local-env-vars 2>/dev/null)"

is_merge() {  # is_merge <sha> -> 0 when it has two or more parents
  local n
  n="$(git rev-list --parents -n1 "$1" 2>/dev/null | wc -w)"
  [ "${n:-0}" -ge 3 ]
}

# policy <tree> <ref> -> prints `<glob>\t<checks>\t<lint>` for the first
# matching row of the tree's push-policy.tsv; the fail-closed default when the
# table is absent or no row matches.
policy() {
  local table="$1/.githooks/push-policy.tsv" ref="$2" glob checks lint
  if [ -f "$table" ]; then
    while IFS=$'\t' read -r glob checks lint; do
      case "$glob" in ''|'#'*) continue ;; esac
      # shellcheck disable=SC2053  # the glob column IS a pattern
      if [[ "$ref" == $glob ]]; then
        printf '%s\t%s\t%s\n' "$glob" "${checks:-all}" "${lint:-yes}"; return 0
      fi
    done < "$table"
    printf '%s\t%s\t%s\n' "<no row matched>" all yes; return 0
  fi
  printf '%s\t%s\t%s\n' "<no push-policy.tsv>" all yes
}

# check_tip <sha> <ref> <with-lint:1|0> -> 0 green, 1 red (or could not run);
# prints its own lines.
check_tip() {
  local sha="$1" ref="$2" with_lint="$3" short="${1:0:12}" tmp wt table red="" ran=0
  local name path cmd log rc pglob pchecks plint want_lint t0 dt kind kind_up
  if [ "${#UNSET[@]}" -eq 0 ]; then
    echo "pre-push: REFUSED $short -- \`git rev-parse --local-env-vars\` returned nothing, so the checks cannot run safely against a temp checkout." >&2
    return 1
  fi
  tmp="$(mktemp -d "${TMPDIR:-/tmp}/argon-pre-push.XXXXXX")" || return 1
  wt="$tmp/tree"
  if ! git worktree add --detach --quiet "$wt" "$sha" >"$tmp/wt.log" 2>&1; then
    echo "pre-push: REFUSED $short -- could not materialize the pushed commit to check it: $(tail -1 "$tmp/wt.log")" >&2
    rm -rf "$tmp"; return 1
  fi
  IFS=$'\t' read -r pglob pchecks plint <<<"$(policy "$wt" "$ref")"
  want_lint=0
  case "$plint" in
    yes) want_lint=1 ;;
    merge) is_merge "$sha" && want_lint=1 ;;
  esac
  [ "$with_lint" = 1 ] || want_lint=0
  kind="one-parent head"; kind_up="ONE-PARENT COMMIT"
  is_merge "$sha" && { kind="merge head"; kind_up="MERGE COMMIT"; }
  table="$wt/.githooks/pre-push-checks.tsv"
  if [ ! -f "$table" ]; then
    echo "pre-push: SKIP $short ($ref) -- $kind, but its tree carries no .githooks/pre-push-checks.tsv" >&2
  else
    while IFS=$'\t' read -r name path cmd; do
      case "$name" in ''|'#'*) continue ;; esac
      [ -f "$wt/$path" ] || continue
      if [ "$name" = lint ]; then
        if [ "$want_lint" -ne 1 ]; then
          if [ "$with_lint" = 1 ]; then
            echo "pre-push: leg lint not run on $short ($ref) -- policy row $pglob: lint=$plint ($kind)" >&2
          fi
          continue
        fi
      else
        case ",$pchecks," in
          ,all,) ;;
          *",$name,"*) ;;
          *) continue ;;
        esac
      fi
      ran=$((ran + 1))
      log="$tmp/$name.log"
      rc=0; t0=$SECONDS
      (cd "$wt" && env "${UNSET[@]}" bash -c "$cmd" </dev/null >"$log" 2>&1) || rc=$?
      dt=$((SECONDS - t0))
      if [ "$rc" -ne 0 ]; then
        red="$red $name"
        echo "pre-push: leg $name RED ${dt}s on $short ($ref)" >&2
        echo "pre-push: check $name RED (rc=$rc) on $short -- $cmd" >&2
        sed "s#$wt/##g" "$log" | tail -8 | sed 's/^/    /' >&2
      else
        echo "pre-push: leg $name green ${dt}s on $short ($ref)" >&2
      fi
    done < "$table"
  fi
  git worktree remove --force "$wt" >/dev/null 2>&1 || true
  rm -rf "$tmp"
  git worktree prune >/dev/null 2>&1 || true
  if [ -n "$red" ]; then
    cat >&2 <<MSG
pre-push: REFUSED -- $short is a $kind_up whose tree still differs from its
  generators:$red
  The pre-commit only WARNED while the merge was in progress; nothing after
  this push re-checks it in a rendered instance.

    the recipe:   run the regenerators, commit, push again.
    the override: git push --no-verify  (the refresh's GITHOOKS-OK line reports it)
MSG
    return 1
  fi
  echo "pre-push: PASS $short ($ref) -- $kind, $ran leg(s) green (policy row $pglob: checks=$pchecks lint=$plint)" >&2
  return 0
}

if [ "${1:-}" = "--audit" ]; then
  ref="${2:?usage: pre-push --audit <ref>}"
  sha="$(git rev-parse -q --verify "$ref^{commit}" 2>/dev/null)" || { echo "pre-push-audit: UNKNOWN $ref -- no such commit"; exit 2; }
  # the policy row is the branch's: origin/main and main both read refs/heads/main
  full="$(git rev-parse --symbolic-full-name "$ref" 2>/dev/null)"
  case "$full" in
    refs/remotes/*/*) full="refs/heads/${full#refs/remotes/*/}" ;;
    refs/*) ;;
    *) full="refs/heads/$ref" ;;
  esac
  if check_tip "$sha" "$full" 0 2>/dev/null; then echo "pre-push-audit: green ${sha:0:12}"; exit 0; fi
  echo "pre-push-audit: RED ${sha:0:12}"; exit 1
fi

refused=0
while read -r lref lsha rref rsha; do
  [ -n "${lsha:-}" ] || continue
  [[ "$lsha" =~ $ZERO_RE ]] && continue          # a deletion pushes no tree
  if [ -n "${ARGON_LAND_GATED_TIP:-}" ] && [ "$ARGON_LAND_GATED_TIP" = "$lsha" ]; then
    echo "pre-push: PASS ${lsha:0:12} -> $rref -- a landing push (scripts/land.sh gated this tip: ARGON_LAND_GATED_TIP)" >&2
    continue
  fi
  # a tag or other non-commit object: check the commit it names, if any
  csha="$(git rev-parse -q --verify "$lsha^{commit}" 2>/dev/null)" || continue
  check_tip "$csha" "$rref" 1 </dev/null || refused=1
done
exit "$refused"
