From de2c822657337a2a3a7c490e6ce1e7e34f37ba01 Mon Sep 17 00:00:00 2001 From: matst80 Date: Sat, 25 Jul 2026 16:39:30 +0200 Subject: [PATCH] chore: add pre-commit hook to forbid committing root-level Go binaries --- Makefile | 11 +++++++++++ hooks/pre-commit | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 Makefile create mode 100755 hooks/pre-commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..026f69b --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ + + +# ━━━ Pre-commit hook (forbid root binaries) ━━━ +.PHONY: install-hooks +install-hooks: + @if [ "$$(git config core.hooksPath 2>/dev/null)" != "hooks/" ]; then \ + git config core.hooksPath hooks/; \ + echo "Configured git core.hooksPath=hooks/"; \ + fi + @chmod +x hooks/pre-commit + @echo "Pre-commit hook installed. (Bypass with: git commit --no-verify)" diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..7e80aad --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,50 @@ +#!/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