#!/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