diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 00000000..6bbeed6f --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# JellyTau pre-commit hook — the fast half of CLAUDE.md's "Before Committing" +# list, enforced instead of remembered. +# +# TRACES: | DR-207 +# +# Install with: bun run hooks:install (sets core.hooksPath=scripts/hooks) +# Skip once with: git commit --no-verify +# +# What runs here is deliberately limited to gates that finish in seconds: +# +# bun run check svelte-check (types) +# bun run test vitest, single pass +# scripts/check-frontend-boundary.sh domain-taxonomy tripwire (DR-094) +# cargo fmt --all -- --check only when src-tauri/ is staged +# +# NOT here, on purpose: `cargo clippy` and `cargo test`. Both take minutes on a +# cold target dir, which turns every commit into a coffee break and trains +# people to reach for --no-verify. CI (.gitea/workflows/build-and-test.yml) is +# where those run; `bun run test:all` is the local equivalent. + +set -uo pipefail + +# Merge and rebase commits carry someone else's changes, and conflict resolution +# is exactly when a slow gate is least welcome. Let them through — CI still +# gates the merge result. +GIT_DIR_PATH="$(git rev-parse --git-dir 2>/dev/null)" || exit 0 +if [ -e "$GIT_DIR_PATH/MERGE_HEAD" ] || + [ -d "$GIT_DIR_PATH/rebase-merge" ] || + [ -d "$GIT_DIR_PATH/rebase-apply" ] || + [ -e "$GIT_DIR_PATH/CHERRY_PICK_HEAD" ]; then + echo "pre-commit: merge/rebase in progress — skipping checks (CI still gates the result)." + exit 0 +fi + +# Nothing staged (e.g. `git commit --amend` that only edits the message): nothing +# to check. +STAGED="$(git diff --cached --name-only --diff-filter=ACMR)" +if [ -z "$STAGED" ]; then + exit 0 +fi + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" || exit 1 + +FAILED=0 + +run_gate() { + label="$1" + shift + echo "" + echo "🔎 pre-commit: $label" + if ! "$@"; then + echo "❌ pre-commit: $label failed" + FAILED=1 + fi +} + +run_gate "svelte-check (bun run check)" bun run check +run_gate "frontend tests (bun run test)" bun run test +run_gate "frontend/backend boundary" bash scripts/check-frontend-boundary.sh + +# rustfmt only matters when Rust actually changed, and `cargo fmt --check` is +# cheap (no compilation) whenever it does. +if printf '%s\n' "$STAGED" | grep -q '^src-tauri/'; then + if command -v cargo >/dev/null 2>&1; then + echo "" + echo "🔎 pre-commit: rustfmt (src-tauri/ is staged)" + if ! (cd src-tauri && cargo fmt --all -- --check); then + echo "❌ pre-commit: cargo fmt --all -- --check failed" + echo " fix with: cd src-tauri && cargo fmt" + FAILED=1 + fi + else + echo "⚠️ pre-commit: src-tauri/ staged but cargo is not on PATH — skipping rustfmt." + fi +fi + +if [ "$FAILED" -ne 0 ]; then + echo "" + echo "🛑 pre-commit checks failed. Fix them, or bypass deliberately with:" + echo " git commit --no-verify" + exit 1 +fi + +echo "" +echo "✅ pre-commit checks passed." diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100755 index 00000000..717a98bd --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Point git at the repo's tracked hooks directory. +# +# TRACES: | DR-207 +# +# bun run hooks:install # or: ./scripts/install-hooks.sh +# +# `core.hooksPath` is used rather than copying files into .git/hooks so the +# hooks stay version-controlled: an update to scripts/hooks/pre-commit reaches +# everyone on their next pull instead of needing a re-install. +# +# The setting is local to this clone (git config, not committed). To undo: +# git config --unset core.hooksPath + +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +HOOKS_DIR="scripts/hooks" + +if [ ! -d "$HOOKS_DIR" ]; then + echo "❌ $HOOKS_DIR does not exist — are you in the JellyTau repo?" >&2 + exit 1 +fi + +# Git refuses to run a hook that is not executable, and the bit is easy to lose +# on a fresh checkout on some filesystems. +chmod +x "$HOOKS_DIR"/* 2>/dev/null || true + +git config core.hooksPath "$HOOKS_DIR" + +echo "✅ core.hooksPath = $(git config core.hooksPath)" +echo "" +echo "Installed hooks:" +for hook in "$HOOKS_DIR"/*; do + [ -f "$hook" ] || continue + echo " - $(basename "$hook")" +done +echo "" +echo "pre-commit runs: bun run check, bun run test, check-frontend-boundary.sh," +echo "and cargo fmt --check when src-tauri/ is staged." +echo "Bypass a single commit with: git commit --no-verify"