#!/bin/sh
# Installed by `vibey-gh install` — do not edit; edit the template in vibey-gh.
#
# THE GATE. A repository that has adopted the vibey GitHub automation must keep it
# installed and working. If the tooling is missing, or the fingerprints it enforces are
# not intact, the push is refused — to any branch, local or remote.
#
# The point is not ceremony. Provenance that can be skipped whenever it is inconvenient
# is provenance nobody can rely on, so the check has to sit on the last action before
# code leaves the machine.
#
# Escape hatch: `git push --no-verify` still works, because a hook that cannot be
# bypassed in an emergency gets uninstalled instead. CI applies the same rule server-side,
# so skipping here only defers the failure rather than avoiding it.
#
# A push that carries NO code has nothing here to judge, and the gate decides that for
# itself rather than asking a caller to skip it: see "The gate's own scope" below.

set -e

red() { printf '\033[0;31m%s\033[0m\n' "$1"; }

# Git hands this hook the refs being pushed on stdin, one line each. Read once, here,
# before anything can consume it: it is the only place the list is complete, and both the
# scope check and the chained hook below need every line of it.
refs=$(cat)
feed_refs() { if [ -n "$refs" ]; then printf '%s\n' "$refs"; fi; }


# Resolve the CLI. `vibey-gh` on PATH is the normal case, but a project virtualenv that
# has not been activated is just as normal — and a gate that reports "not installed"
# when the tool IS installed is a gate people learn to bypass. Look where it actually
# lives before giving up. Hooks run from the top of the working tree.
# Where this repository's own copy of vibey-gh lives, printed as a directory — or
# nothing, which is the ordinary case for a repository that merely adopted it.
#
# What is looked for is the pyproject that DECLARES the distribution, not a fixed path.
# It sits at the top of the tree in the standalone repository and under `src/` in the
# monorepo that absorbed it, and hard-coding either one breaks the other. `git ls-files`
# keeps the search bounded to tracked files, so a vendored copy under `.venv/` or
# `site/` can never match, and it is available here by definition: hooks run inside a
# git repository, from the top of the working tree.
vibey_gh_self() {
  # Read from `[install] self_source` in .vibey-gh.toml at runtime rather than rendered
  # in: `install.installed()` compares these hooks byte-for-byte against the template, so
  # a substituted hook would be permanently "out of date" and refuse every push.
  #
  # A DECLARED path, never a search. Searching the tree for a pyproject that declares
  # `name = "vibey-gh"` reads whatever is in the working tree, which on a contributor's
  # branch is whatever they put there -- and this hook executes what it finds.
  dir=$(sed -n 's/^[[:space:]]*self_source[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' \
    .vibey-gh.toml 2>/dev/null | head -n 1)
  [ -n "$dir" ] || dir=.
  case "$dir" in /*|*..*) return 0 ;; esac
  grep -qE '^name = "vibey-gh"' "$dir/pyproject.toml" 2>/dev/null || return 0
  [ -d "$dir/vibey_gh" ] || return 0
  printf '%s' "$dir"
}


vibey_gh() {
  # A repository that CARRIES vibey-gh must run that source, never an installed copy.
  # Its `develop` is ahead of the last release nearly always, so a globally installed
  # vibey-gh compares the repository's managed assets against the older ones it bundles,
  # reports them out of date, and refuses every push — a contributor who installed the
  # tool the obvious way cannot commit to it. The package is dependency-free stdlib, so
  # `python3 -m` against the checkout needs no install and no virtualenv at all.
  if [ -z "${_VIBEY_GH_SELF+x}" ]; then
    _VIBEY_GH_SELF=$(vibey_gh_self)
  fi
  # Every `python3` below runs with PYTHONSAFEPATH=1. Without it, `-c` and `-m` put the
  # current directory -- the top of the working tree -- first on sys.path, so a checked-out
  # branch carrying a `vibey_gh/` package of its own would be imported and executed by
  # this hook in place of the real one. The self-hosted branch still runs its source: it
  # names that source on PYTHONPATH, which PYTHONSAFEPATH leaves alone. Python 3.12+
  # honours the variable and older interpreters ignore it; vibey-gh requires 3.12.
  if [ -n "$_VIBEY_GH_SELF" ] && PYTHONSAFEPATH=1 \
      PYTHONPATH="$_VIBEY_GH_SELF${PYTHONPATH:+:$PYTHONPATH}" \
      python3 -c "import vibey_gh.cli" >/dev/null 2>&1; then
    PYTHONSAFEPATH=1 PYTHONPATH="$_VIBEY_GH_SELF${PYTHONPATH:+:$PYTHONPATH}" \
      python3 -m vibey_gh.cli "$@"
  elif command -v vibey-gh >/dev/null 2>&1; then
    vibey-gh "$@"
  elif [ -n "${VIRTUAL_ENV:-}" ] && [ -x "${VIRTUAL_ENV}/bin/vibey-gh" ]; then
    "${VIRTUAL_ENV}/bin/vibey-gh" "$@"
  elif [ -x ".venv/bin/vibey-gh" ]; then
    ./.venv/bin/vibey-gh "$@"
  elif [ -x "venv/bin/vibey-gh" ]; then
    ./venv/bin/vibey-gh "$@"
  elif PYTHONSAFEPATH=1 python3 -c "import vibey_gh.cli" >/dev/null 2>&1; then
    PYTHONSAFEPATH=1 python3 -m vibey_gh.cli "$@"
  elif PYTHONSAFEPATH=1 python3 -c "import vibey_bootstrap.gh.cli" >/dev/null 2>&1; then
    PYTHONSAFEPATH=1 python3 -m vibey_bootstrap.gh.cli "$@"   # pre-split layout
  else
    return 127
  fi
}

if ! vibey_gh trailer-key >/dev/null 2>&1; then
  red "✖ push refused: the vibey GitHub automation is not installed."
  echo
  echo "  This repository requires it for provenance and release automation."
  echo
  echo "      pip install __VIBEY_GH_FALLBACK_PACKAGE__"
  echo "      vibey-gh install"
  echo
  echo "  Then push again. To bypass once: git push --no-verify"
  exit 1
fi

# The gate's own scope. This stage judges code on its way off the machine, and some pushes
# carry none: the sovereign heartbeat is an empty tree with no parents on a ref outside
# refs/heads/. `vibey-gh push-scope` applies one rule to every line git wrote above: every
# ref outside refs/heads/ and refs/tags/, every commit an empty tree with no parents. Only
# then does it print the token, and only the exact token ends the hook here. A branch, a
# tag, a tree with anything in it, a commit with a parent, a deletion, an unreadable object,
# an older vibey-gh that lacks the command, a crash -- each prints no token, and the whole
# gate below runs. This is the gate applying its own rule, not a caller skipping it: there
# is no flag or variable that asks for it, only what the objects being pushed are.
scope=$(feed_refs | vibey_gh push-scope) || scope=
if [ "$scope" = "carries-no-code" ]; then
  exit 0
fi

if ! vibey_gh check --quiet; then
  red "✖ push refused: the provenance fingerprints are not intact."
  echo
  echo "      vibey-gh check          # see what is missing"
  echo "      vibey-gh check --apply  # add the missing file headers"
  echo
  echo "  Commit trailers are added automatically by the commit-msg hook."
  echo "  To bypass once: git push --no-verify"
  exit 1
fi

# Chain to a pre-existing project hook, so adopting this does not discard local checks.
# Its refusal is this hook's refusal. `set -e` and being the last line already carry the
# status out; `|| exit $?` says so explicitly, so neither can be lost to a later edit. The
# refs this hook read are handed on unchanged: the pre-commit framework reads them from
# stdin, and with none it decides there is nothing to check and runs no gate at all.
if [ -x "$(dirname "$0")/pre-push.local" ]; then
  feed_refs | "$(dirname "$0")/pre-push.local" "$@" || exit $?
fi
