diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..d159335 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# +# pre-push: run the cheap local gates before a commit reaches the forge. +# +# Install once per clone: +# +# git config core.hooksPath .githooks +# +# The AKGL build and mutation harness are opt-in because they are expensive: +# +# AKBASIC_HOOK_AKGL=1 git push +# AKBASIC_HOOK_MUTATION=1 git push +# +# Bypass everything with Git's escape hatch: git push --no-verify + +set -u + +ZERO_SHA=0000000000000000000000000000000000000000 + +root=$(git rev-parse --show-toplevel) || exit 1 +cd "$root" || exit 1 + +# A push that only deletes refs, or an empty push input, has no new commit to +# test. Git calls hooks for those pushes too. +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 + +if [ ! -f deps/libakerror/CMakeLists.txt ]; then + echo "pre-push: deps/libakerror is empty. Run:" >&2 + echo " git submodule update --init --recursive" >&2 + exit 1 +fi + +builddir="${AKBASIC_HOOK_BUILD_DIR:-$(git rev-parse --git-dir)/akbasic-prepush}" +mkdir -p "$builddir" || exit 1 +logfile="$builddir/last.log" + +# Keep output quiet on success, but retain the complete failing command output +# so a failed push is actionable without rerunning the gate by hand. +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 [ -x scripts/cppcheck.sh ]; then + if command -v cppcheck > /dev/null 2>&1; then + echo "pre-push: cppcheck" + run scripts/cppcheck.sh + else + echo "pre-push: cppcheck not installed, skipping the optional check" >&2 + fi +else + echo "pre-push: scripts/cppcheck.sh not present, skipping the optional check" >&2 +fi + +echo "pre-push: default build + ctest" +run cmake -S . -B "$builddir/default" +run cmake --build "$builddir/default" --parallel 2 +run ctest --test-dir "$builddir/default" --output-on-failure + +echo "pre-push: sanitizer build + ctest" +run cmake -S . -B "$builddir/sanitize" -DAKBASIC_SANITIZE=ON +run cmake --build "$builddir/sanitize" --parallel 2 +run ctest --test-dir "$builddir/sanitize" --output-on-failure + +if [ "${AKBASIC_HOOK_AKGL:-0}" = "1" ]; then + echo "pre-push: AKGL build + ctest" + run cmake -S . -B "$builddir/akgl" -DAKBASIC_WITH_AKGL=ON + run cmake --build "$builddir/akgl" --parallel 2 + run env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + SDL_RENDER_DRIVER=software ctest --test-dir "$builddir/akgl" --output-on-failure +fi + +if [ "${AKBASIC_HOOK_MUTATION:-0}" = "1" ]; then + threshold="${AKBASIC_MUTATION_THRESHOLD:-65}" + mutants="${AKBASIC_HOOK_MUTANTS:-260}" + echo "pre-push: mutation testing, threshold ${threshold}%" + run python3 scripts/mutation_test.py \ + --target src/symtab.c \ + --max-mutants "$mutants" \ + --threshold "$threshold" +fi + +echo "pre-push: OK" +exit 0 diff --git a/README.md b/README.md index 47103c1..d49e4b1 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,18 @@ cmake --build build --parallel ctest --test-dir build --output-on-failure ``` +To run the same fast build, sanitizer and test gates before every push, install the advisory +pre-push hook once in this clone: + +```sh norun +git config core.hooksPath .githooks +``` + +The hook keeps its build trees under `.git/akbasic-prepush`, skips the optional cppcheck gate +when its script or tool is unavailable, and leaves the AKGL build and mutation harness behind +`AKBASIC_HOOK_AKGL=1` and `AKBASIC_HOOK_MUTATION=1`. `git push --no-verify` remains the escape +hatch. `core.hooksPath` is local clone configuration, so CI remains the hard gate. + ```sh norun ./build/basic # the REPL ./build/basic tests/language/functions.bas # run a program