#!/usr/bin/env bash
# pre-push:
#   1) Repo-wide lint (tens of seconds). Full tests / difftest / benchmarks are
#      left to CI. engineering.md §2.3.
#   2) Self-wrapping post-push: after lint passes, this hook performs the inner
#      push itself, then blocks until CI finishes + collects PR comment
#      activity, reporting the result via stderr to the caller (script /
#      Claude Code background bash). git has no native post-push; this is the
#      emulation.
#
# Skip: use `git push --no-verify` in emergencies only.
set -uo pipefail

if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then
  exit 0
fi

# Inner (self-wrapped) push: lint already ran in the outer round; here the
# real push operation is being initiated inside git. The marker is an env
# variable scoped to this process subtree (no file, no persistence), so it
# cannot leak into later unrelated git pushes (no stale-flag risk).
if [ -n "${GIT_POSTPUSH:-}" ]; then
  exit 0
fi

cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)"

command -v go >/dev/null 2>&1 || exit 0

# -- lint (the original pre-push behavior) ------------------------------------
if command -v golangci-lint >/dev/null 2>&1; then
  echo "pre-push: golangci-lint run ./..."
  if ! golangci-lint run ./...; then
    echo "✗ lint 未通过;fix 后重新 push(跳过:git push --no-verify,仅紧急情况)" >&2
    exit 1
  fi
else
  echo "pre-push: golangci-lint 未安装,降级 go vet"
fi
if ! go vet ./...; then
  echo "✗ go vet 未通过" >&2
  exit 1
fi

echo "pre-push: all lint checks passed."

# -- post-push CI watch (self-wrapping) ----------------------------------------
# git has no native post-push hook. Emulation: when the OUTER push enters this
# hook GIT_POSTPUSH is unset, and we run an INNER push ourselves with the
# marker set; when the INNER push re-enters this hook the marker is set and it
# exits 0 right at the top. Control returns to the OUTER invocation, which
# continues into the CI watch below.
#
# Read the refspecs git passes on stdin (one per line:
#   <local ref> <local sha> <remote ref> <remote sha>)
# The INNER push mirrors these refspecs **verbatim** instead of assuming a
# current-branch push. Force pushes / tag pushes / deletion pushes
# (`git push origin :foo`) / multi-ref pushes (`git push --all`) are all
# handled correctly.
zero="0000000000000000000000000000000000000000"
refspecs=()
# Per-refspec `--force-with-lease=<ref>:<expect>` arguments, populated **only
# when PREPUSH_ALLOW_FORCE=1** (otherwise a non-fast-forward push is rejected
# by the remote as usual instead of being silently escalated to a force push
# by the hook; see the in-loop comment below).
force_lease_args=()
have_update=0          # at least one non-deletion ref => the CI watch is meaningful
head_branch_pushed=0   # whether the refspecs include the branch HEAD is on
head_remote_ref=""     # remote ref of the candidate -u target (`refs/heads/<branch>`)

# Which branch HEAD is on (empty when detached; -u is not considered then)
head_ref="$(git symbolic-ref HEAD 2>/dev/null || true)"

while read -r local_ref local_sha remote_ref remote_sha; do
  [ -z "${remote_ref:-}" ] && continue
  if [ "$local_sha" = "$zero" ]; then
    # Deletion: the refspec is ":<remote_ref>" (empty source)
    refspecs+=(":${remote_ref}")
  else
    refspecs+=("${local_ref}:${remote_ref}")
    have_update=1
    # Non-fast-forward (the remote has commits we do not) => a plain push
    # would be rejected. This hook used to unconditionally escalate the
    # INNER push to `--force-with-lease`, which meant **a plain push got
    # silently upgraded to a force push** (overwriting the remote even
    # though the user never passed --force). Tightened: the lease argument
    # is added only when the user explicitly sets PREPUSH_ALLOW_FORCE=1;
    # otherwise it is omitted and the INNER push is rejected by the remote
    # as usual (matching git's behavior without the hook).
    # The lease uses the precise `--force-with-lease=<ref>:<remote_sha>`
    # form: the bare form computes the lease from remote-tracking refs
    # (`refs/remotes/origin/*`) and mis-rejects when those are stale;
    # `remote_sha` is the actual remote sha git currently sees.
    if [ "$remote_sha" != "$zero" ] \
       && ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
      if [ "${PREPUSH_ALLOW_FORCE:-}" = "1" ]; then
        force_lease_args+=("--force-with-lease=${remote_ref}:${remote_sha}")
      else
        echo "pre-push: non-fast-forward push to ${remote_ref} — NOT escalating to force." >&2
        echo "pre-push:   if the rewrite is intentional, re-run with PREPUSH_ALLOW_FORCE=1 git push --force ..." >&2
      fi
    fi
    # The branch HEAD is on is being pushed => candidate -u target
    if [ -n "$head_ref" ] && [ "$local_ref" = "$head_ref" ]; then
      head_branch_pushed=1
      head_remote_ref="$remote_ref"
    fi
  fi
done

# Empty stdin => let the OUTER push run itself; nothing to wrap.
if [ "${#refspecs[@]}" -eq 0 ]; then
  exit 0
fi

# Deletions only => no CI to watch. Let the OUTER push perform the deletions
# and return.
if [ "$have_update" -eq 0 ]; then
  exit 0
fi

remote_name="${1:-origin}"

# Automatic `-u` relay (absorbing ctex-kit #888): git does not forward the
# outer command-line arguments to the hook, so the inner push cannot know the
# user originally passed `-u`. Detection: if the branch HEAD is on is being
# pushed and it **currently has no upstream**, add `--set-upstream` to the
# inner push automatically. Branches that already have an upstream are left
# alone, detached HEAD is left alone, tag-only / other-branch pushes are left
# alone -- no side effects.
#
# This replaces the old "always pass -u when pushing a non-master branch"
# workflow (see memory feedback_self_wrapper_upstream_bug) -- legacy baggage
# from the hook not relaying -u.
set_upstream_args=()
if [ "$head_branch_pushed" -eq 1 ] \
   && ! git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
  set_upstream_args=(--set-upstream)
  echo "post-push: auto --set-upstream ($(git rev-parse --abbrev-ref HEAD) → ${remote_name}/${head_remote_ref#refs/heads/})" >&2
fi

# bash 3.2 (stock macOS) aborts with "unbound variable" under set -u when
# expanding an **empty array** as `"${arr[@]}"`. The `${arr[@]+"${arr[@]}"}`
# form expands only when the array is set; empty arrays are safe and
# side-effect free. bash >= 4.4 behaves identically.
echo "post-push: performing real push to '${remote_name}' (${refspecs[*]})..." >&2
GIT_POSTPUSH=1 git push \
  ${set_upstream_args[@]+"${set_upstream_args[@]}"} \
  ${force_lease_args[@]+"${force_lease_args[@]}"} \
  "${remote_name}" "${refspecs[@]}"
push_rc=$?
if [ "$push_rc" -ne 0 ]; then
  echo "post-push: inner push failed (rc=${push_rc}) — not watching CI." >&2
  exit "$push_rc"
fi

# The INNER push has already updated the remote. The OUTER push that invoked
# this hook is now guaranteed to fail -- either git's atomic ref protection
# rejects it ("remote rejected / cannot lock ref") or the connection is cut
# during the long CI watch (SIGPIPE / "connection closed"). **Both are
# expected and harmless**: the push already succeeded via the INNER one. The
# OUTER push's exit code does **not** reflect CI status -- the real verdict
# is the check/cross report below.
echo "post-push: ✔ push succeeded (remote updated by inner push)." >&2
echo "post-push:   any outer 'remote rejected' or 'connection closed' line below is expected — ignore it." >&2
echo "post-push:   the real verdict is the ✓/✗ report once CI finishes." >&2

# The INNER push updated the remote. Block on CI, then report.
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
if [ -x "${repo_root}/scripts/check-pr-ci.sh" ]; then
  "${repo_root}/scripts/check-pr-ci.sh"
  ci_rc=$?
  # rc 2  = no PR / gh missing        => non-fatal, let it through
  # rc 1  = CI failed                  => report failure
  # rc 75 = CI passed but there is new review activity / an unresolved
  #         thread => also report failure. Returning 0 would make the OUTER
  #         push exit 0, and tools that only look at the exit status (agent
  #         loop / CI driver) would silently miss the "address review
  #         feedback" stderr instructions.
  if [ "$ci_rc" -eq 1 ] || [ "$ci_rc" -eq 75 ]; then
    exit 1
  fi
  # rc 0: check-pr-ci.sh already printed the "nothing more to do" terminal
  # line, or the Claude-facing instruction block. Nothing to add here.
fi

# Reaching here means the real push already succeeded (done by INNER).
# Return 0 so the OUTER push becomes a no-op ("Everything up-to-date") -- it
# will **not** push again.
exit 0
