#!/usr/bin/env bash # pre-commit hook: forbid committing compiled Go binaries at the repo root. # Install: `make install-hooks` (idempotent — sets git core.hooksPath hooks/). # Bypass: `git commit --no-verify`. set -euo pipefail # Only check newly-added files at exactly the repo root (no subdirs, no modifications). # `2>/dev/null || true` defends against rare git-diff failures (corrupted index, etc.) # propagating through `set -e` and killing the hook before any catches run. # Parens group OR tightly so awk errors aren't accidentally swallowed. STAGED_ROOT_FILES=$( (git diff --cached --name-only --diff-filter=A 2>/dev/null || true) | awk -F/ 'NF==1') [ -z "$STAGED_ROOT_FILES" ] && exit 0 # Without `go` on PATH we cannot reliably detect build artifacts — warn + exit safe. command -v go >/dev/null 2>&1 || { echo "WARN: go not on PATH; skipping root-binary check." >&2; exit 0; } # Use `while IFS= read -r` instead of `for f in $(...)` to be safe against # whitespace in filenames. while IFS= read -r f; do [ -d "$f" ] && continue [ -e "$f" ] || continue # Catch 1: a root binary whose name matches a Go cmd// package. if [ -d "cmd/$f" ] && ls "cmd/$f"/*.go >/dev/null 2>&1; then echo "REJECT: '$f' looks like the build output of cmd/$f/." >&2 echo " 'go build ./cmd/$f/' would produce this file at the repo root." >&2 echo " Fix: add /$f to .gitignore, rebuild from cmd/$f/." >&2 exit 1 fi # Catch 2: executable regular file at root. Broadens coverage to # arbitrary-ELF-at-root with unrelated names (Catch 1 only matches cmd//). if [ -x "$f" ] && [ -f "$f" ]; then echo "REJECT: '$f' is an executable file at the repo root." >&2 echo " Executables at the repo root are treated as build artifacts." >&2 echo " Fix: add /$f to .gitignore, or build into cmd/$f/ or internal/." >&2 echo " Opt out per-file: git update-index --chmod=-x $f" >&2 exit 1 fi # Catch 3: the staged root file is itself a buildable Go target. if go build -o /dev/null "./$f" 2>/dev/null; then echo "REJECT: '$f' is a buildable Go target at the root." >&2 echo " 'go build ./$f' succeeded, so this is treated as a build artifact." >&2 echo " Fix: add /$f to .gitignore, or move source under cmd/$f/ or internal/." >&2 exit 1 fi done <<< "$STAGED_ROOT_FILES" exit 0