From 5dfb03eaddcb48f6b78e65b92a69d437347139e6 Mon Sep 17 00:00:00 2001 From: Logikoma Date: Wed, 5 Aug 2026 18:33:54 -0400 Subject: [PATCH] Add local pre-push validation gates Co-authored-by: Andrew Kesterson --- .githooks/pre-push | 94 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 20 ++++++++++ 2 files changed, 114 insertions(+) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..9db155c --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# +# Run the repository's cheap gates before a push reaches the forge. CI remains +# the hard gate; this hook makes the common failure modes cheap to find. +# +# Install once per clone: +# +# git config core.hooksPath .githooks +# +# The mutation harness is deliberately opt-in because it is slow: +# +# AKERR_HOOK_MUTATION=1 git push +# +# Git's normal escape hatch remains available: +# +# git push --no-verify +# +# Builds go under .git/akerr-prepush so the hook does not disturb build/. +# Override that location with AKERR_HOOK_BUILD_DIR when needed. + +set -u + +ZERO_SHA=0000000000000000000000000000000000000000 + +root=$(git rev-parse --show-toplevel) || exit 1 +cd "$root" || exit 1 + +# A delete-only push, or an invocation with no refs on stdin, has no new code +# to validate. +has_updates=0 +while read -r _local_ref local_sha _remote_ref _remote_sha; do + if [ "$local_sha" != "$ZERO_SHA" ]; then + has_updates=1 + fi +done +if [ "$has_updates" -eq 0 ]; then + exit 0 +fi + +builddir="${AKERR_HOOK_BUILD_DIR:-$(git rev-parse --git-dir)/akerr-prepush}" +mkdir -p "$builddir" || exit 1 +logfile="$builddir/last.log" + +# Keep normal output short, but preserve the complete failing command output. +run() { + if ! "$@" > "$logfile" 2>&1; then + echo >&2 + echo "pre-push: FAILED: $*" >&2 + echo "---------------------------------------------------------------" >&2 + cat "$logfile" >&2 + echo "---------------------------------------------------------------" >&2 + echo "pre-push: push aborted. Use 'git push --no-verify' to override." >&2 + exit 1 + fi +} + +if [ -f scripts/cppcheck.sh ] && command -v cppcheck > /dev/null 2>&1; then + echo "pre-push: cppcheck" + run bash scripts/cppcheck.sh +elif [ ! -f scripts/cppcheck.sh ]; then + echo "pre-push: scripts/cppcheck.sh not present, skipping cppcheck" +else + echo "pre-push: cppcheck not installed, skipping cppcheck" +fi + +echo "pre-push: default build + ctest" +run cmake -S . -B "$builddir/default" +run cmake --build "$builddir/default" +run ctest --test-dir "$builddir/default" --output-on-failure + +echo "pre-push: AKERR_USE_STDLIB=OFF build + ctest" +run cmake -S . -B "$builddir/stdlib-off" \ + -DAKERR_USE_STDLIB=OFF -DAKERR_THREADS=none +run cmake --build "$builddir/stdlib-off" +run ctest --test-dir "$builddir/stdlib-off" --output-on-failure + +if [ "${AKERR_HOOK_MUTATION:-0}" = "1" ]; then + if command -v python3 > /dev/null 2>&1; then + echo "pre-push: mutation testing (this takes a while)" + if ! python3 scripts/mutation_test.py \ + --target src/error.c \ + --threshold "${AKERR_MUTATION_THRESHOLD:-65}"; then + echo >&2 + echo "pre-push: mutation score below threshold. Push aborted." >&2 + echo "pre-push: use 'git push --no-verify' to override." >&2 + exit 1 + fi + else + echo "pre-push: python3 not installed, skipping mutation testing" + fi +fi + +echo "pre-push: OK" +exit 0 diff --git a/README.md b/README.md index 60d8748..a38cbb7 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,26 @@ cmake --build build cmake --install build ``` +## Local pre-push checks + +Enable the repository's local pre-push checks once in each clone: + +```bash +git config core.hooksPath .githooks +``` + +The hook runs cppcheck when `scripts/cppcheck.sh` and cppcheck are available, +then the default build and tests, followed by the `AKERR_USE_STDLIB=OFF` +build and tests. Builds are kept under `.git/akerr-prepush` and do not disturb +the normal `build/` directory. Mutation testing is opt-in because it is slow: + +```bash +AKERR_HOOK_MUTATION=1 git push +``` + +CI remains the hard gate. For an intentional local escape, use Git's standard +`git push --no-verify` option. + The library depends on `stdlib` and on POSIX threads. Both are optional at the cost of some functionality — see [docs/building.md](docs/building.md) for `-DAKERR_USE_STDLIB=OFF` and