testsAndMisc/linux_configuration/.githooks/pre-push
Krzysztof Rudnicki 2b3c1a69f7 fix(hooks): make git hooks and their toolchain mandatory and fail-closed
A completely absent `prettier` binary went unnoticed long enough for
unformatted markdown to reach main. Two independent causes, both fixed here.

No pre-push hook existed. The three hooks declaring `stages: [pre-push]` in
.pre-commit-config.yaml — prettier, ci-mirror, pytest-coverage — therefore
never ran locally at all. Adds one.

The pre-commit hook failed open. shfmt and shellcheck were wrapped in
`if command -v`, and the entire pre-commit framework was skipped with a
warning when its binary was missing. A silently skipped gate is
indistinguishable from a passing one, so a missing tool now aborts.

- lib/common.sh: require_tool tries `sudo -n pacman -S` and aborts with the
  exact command when a tool cannot be installed non-interactively;
  verify_toolchain applies it to all eight binaries the hooks invoke.
- The pre-push hook resolves git's stdin refs into --from-ref/--to-ref.
  Without a range, `pre-commit run --hook-stage pre-push` checks *staged*
  files, and nothing is staged at push time, so prettier would have received
  zero files and passed vacuously.
- ensure_hooks_path repairs core.hooksPath drift on every hook run.
- meta/scripts/install_hooks.sh covers the one case the hooks cannot
  self-heal — a fresh clone where the config was never set, so nothing runs.
  `--check` reports state and exits non-zero when incomplete.

Note `pre-commit install` must never be used here: it writes into
.git/hooks/, which git ignores entirely while core.hooksPath is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:09:11 +02:00

81 lines
2.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# Pre-push gate: runs the `stages: [pre-push]` hooks from
# .pre-commit-config.yaml (prettier, ci-mirror, pytest-coverage).
#
# Before this hook existed there was no pre-push gate at all, so those three
# never ran locally — which is how a completely absent `prettier` binary went
# unnoticed while unformatted markdown reached main.
#
# git passes the remote name/URL as $1/$2 and one line per ref on stdin:
# <local_ref> <local_sha> <remote_ref> <remote_sha>
# ============================================================================
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
# shellcheck source=linux_configuration/.githooks/lib/common.sh
source "${repo_root}/linux_configuration/.githooks/lib/common.sh"
readonly ZERO_SHA="0000000000000000000000000000000000000000"
# pre-commit arguments describing what to check; filled by resolve_push_range.
RANGE_ARGS=()
# Translate the refs git feeds us on stdin into a pre-commit file selector.
#
# This matters: `pre-commit run --hook-stage pre-push` with no range operates
# on *staged* files, and nothing is staged at push time — so file-scoped hooks
# like prettier would silently receive zero files and pass vacuously. Only the
# always_run hooks would really fire. Passing the range is what makes the gate
# real.
resolve_push_range() {
local local_ref local_sha remote_ref remote_sha
while read -r local_ref local_sha remote_ref remote_sha; do
# Branch deletion — nothing to check.
if [[ $local_sha == "$ZERO_SHA" ]]; then
continue
fi
# New branch on the remote: no common ancestor to diff against, so check
# every file rather than silently checking nothing.
if [[ $remote_sha == "$ZERO_SHA" ]]; then
printf 'Pushing a new branch (%s) — checking all files.\n' "$remote_ref"
RANGE_ARGS=(--all-files)
return 0
fi
printf 'Checking %s (%s..%s)\n' "$local_ref" "${remote_sha:0:8}" "${local_sha:0:8}"
RANGE_ARGS=(--from-ref "$remote_sha" --to-ref "$local_sha")
return 0
done
return 0
}
printf 'Verifying hook wiring and toolchain before pushing...\n'
ensure_hooks_path
verify_toolchain
printf ' ✓ toolchain complete\n'
resolve_push_range
if [[ ${#RANGE_ARGS[@]} -eq 0 ]]; then
printf 'Nothing to check (branch deletion or empty push).\n'
exit 0
fi
printf '\nRunning pre-push hooks (prettier, ci-mirror, pytest-coverage)...\n'
if ! pre-commit run --hook-stage pre-push "${RANGE_ARGS[@]}"; then
printf '\nPush aborted: pre-push hooks failed.\n' >&2
printf 'Fix the issues above and retry the push.\n' >&2
exit 1
fi
printf 'All pre-push checks passed. Proceeding with push.\n'