#!/bin/sh
# govrail: pre-push hook installed by `gov init --hooks`; `gov uninstall` removes it.
# Runs the smallest sufficient gate set for what is being pushed (rule 1):
# the push range from stdin selects gates by their `paths`; CI owns the
# full matrix. Bypass for one push: `git push --no-verify`.
#
# The hook unsets GIT_DIR & friends before running gov: they leak the
# hook's repository into every subprocess, and gov's own tooling (and
# self-test's scratch repositories) must resolve repositories by cwd,
# not by inherited environment (#20).

# Resolve gov robustly (D29): explicit override, then PATH, then module.
# $GOV_BIN is an argv STRING and is word-split by design — it may carry
# interpreter arguments ("python -m gov"); a path containing spaces is
# not a supported form.
gov_cmd() {
    if [ -n "$GOV_BIN" ]; then
        exec $GOV_BIN "$@"
    fi
    if command -v gov >/dev/null 2>&1; then
        GOV_BIN="gov"          # propagate the resolution: gate commands
        export GOV_BIN         # that name `gov` inherit it (#250)
        exec gov "$@"
    fi
    GOV_BIN="python3 -m gov"
    export GOV_BIN
    exec python3 -m gov "$@"
}

unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_QUARANTINE_PATH \
      GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES

# The all-zero object id depends on the repository's hash algorithm:
# 40 hex for sha1, 64 for sha256. A hardcoded 40-zero constant made
# every push from a sha256 clone read as "new branch", diff against a
# bad revision, and block the push — the exact opposite of the promise
# that a fresh install never goes red on its first run.
case "$(git rev-parse --show-object-format 2>/dev/null | tr -d '[:space:]')" in
    sha256) zero=0000000000000000000000000000000000000000000000000000000000000000 ;;
    *)      zero=0000000000000000000000000000000000000000 ;;
esac

base=""
bases=0
full=0
while read -r local_ref local_sha remote_ref remote_sha; do
    # A branch deletion pushes no content — nothing to gate. git reports
    # the deletion as local_ref="(delete)" with the ZERO OID in
    # local_sha, so the all-zero constant matches local_SHA, never
    # local_ref (a ref name is never zero — the old comparison here
    # compared against $local_ref and no deletion was ever skipped;
    # round-9 template review, verified live against a real push).
    [ "$local_sha" = "$zero" ] && continue
    if [ "$remote_sha" = "$zero" ]; then
        full=1   # new remote branch: everything is the change
    elif [ "$remote_sha" != "$base" ]; then
        base="$remote_sha"
        bases=$((bases + 1))
    fi
done

if [ "$full" = 0 ] && [ "$bases" = 0 ]; then
    # Every ref in this push is a deletion (or nothing reached stdin):
    # no commit is being updated, so no gate has anything to look at.
    # Falling through to the full matrix here would run the entire DAG
    # on a `git push origin :branch` — the very pushes that gate the
    # least content.
    exit 0
fi
if [ "$full" = 1 ] || [ "$bases" -gt 1 ]; then
    # Several push ranges — or a brand-new branch among them — cannot be
    # summarized by ONE base ref: scoping to the first range would let a
    # second branch's diff skip its gates, turning "smallest sufficient
    # set" into an insufficient set. Full matrix: always sufficient.
    gov_cmd run
else
    gov_cmd run --base "$base"
fi
