diff --git a/.githooks/pre-push b/.githooks/pre-push index 701021b..8aae5d5 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,37 +1,57 @@ #!/usr/bin/env bash -# pre-push hook: runs quality checks before every `git push`. +# pre-push hook: quality gate for the *pushed* commits — a poor man's CI. # Install with: make setup (sets core.hooksPath = .githooks) +# +# Why a throwaway worktree: a gate must judge what you publish, not the state of +# your desk. Reading the live working tree lets uncommitted WIP either break the +# gate or sneak past it. So we check out the exact commit being pushed into a +# clean, isolated worktree and run the checks there. set -euo pipefail REPO_ROOT=$(git rev-parse --show-toplevel) # shellcheck source=../tools.versions source "${REPO_ROOT}/tools.versions" -echo "pre-push: running checks..." +run_checks() { # cwd is a clean checkout of the pushed commit + echo " → gofmt" + local unformatted + unformatted=$(gofmt -l $(git ls-files '*.go')) + if [ -n "$unformatted" ]; then + echo " FAIL: the following files are not gofmt-formatted:" + echo "$unformatted" | sed 's/^/ /' + echo " Fix with: make lint-fix" + return 1 + fi -# ── 1. gofmt ────────────────────────────────────────────────────────────────── -echo " → gofmt" -UNFORMATTED=$(gofmt -l $(git ls-files '*.go')) -if [ -n "$UNFORMATTED" ]; then - echo " FAIL: the following files are not gofmt-formatted:" - echo "$UNFORMATTED" | sed 's/^/ /' - echo " Fix with: make lint-fix" - exit 1 -fi + echo " → go vet" + go vet ./... -# ── 2. go vet ───────────────────────────────────────────────────────────────── -echo " → go vet" -go vet ./... + echo " → golangci-lint" + go run github.com/golangci/golangci-lint/cmd/golangci-lint@"${GOLANGCI_LINT_VERSION}" run ./... -# ── 3. golangci-lint ────────────────────────────────────────────────────────── -echo " → golangci-lint" -go run github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} run ./... + echo " → gosec" + go run github.com/securego/gosec/v2/cmd/gosec@"${GOSEC_VERSION}" -quiet ./... -# ── 4. gosec (security scan) ────────────────────────────────────────────────── -echo " → gosec" -go run github.com/securego/gosec/v2/cmd/gosec@${GOSEC_VERSION} -quiet ./... + # govulncheck is intentionally omitted (network + slow). + # Run it manually with: make security +} -# govulncheck is intentionally omitted (network + slow). -# Run it manually with: make security +# pre-push feeds one " " line +# per pushed ref on stdin. +while read -r _local_ref local_sha _remote_ref _remote_sha; do + # Branch deletion (all-zero sha): nothing to check. + [[ "$local_sha" =~ ^0+$ ]] && continue + + echo "pre-push: checking ${local_sha:0:12} in a clean worktree..." + tmp=$(mktemp -d) + git worktree add --detach --quiet "$tmp" "$local_sha" + if ( cd "$tmp" && run_checks ); then + git worktree remove --force "$tmp" + else + git worktree remove --force "$tmp" + echo "pre-push: checks FAILED for ${local_sha:0:12}" + exit 1 + fi +done echo "pre-push: all checks passed."