|
1 | 1 | #!/usr/bin/env bash |
2 | | -# Git pre-commit hook: runs cargo fmt --check and cargo clippy on staged Rust files. |
| 2 | +# .githooks/pre-commit |
3 | 3 | # |
4 | | -# Security & Quality Controls: |
5 | | -# - Strict mode: set -euo pipefail |
6 | | -# - Runs fmt check and clippy (-D warnings) against staged files |
7 | | -# - Bypassing is permitted via standard `git commit --no-verify` (Git native flag) or SKIP_PRE_COMMIT=1 |
| 4 | +# Runs `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` |
| 5 | +# against the workspace whenever Rust source files are staged. |
| 6 | +# |
| 7 | +# Install once with: |
| 8 | +# ./scripts/install_git_hooks.sh |
| 9 | +# |
| 10 | +# Bypass options (in order of preference): |
| 11 | +# git commit --no-verify # standard Git flag, no logging |
| 12 | +# SKIP_PRE_COMMIT=1 git commit # env-variable bypass, warning is logged |
| 13 | +# |
| 14 | +# The env-variable bypass exists for automated tooling (e.g. release scripts) |
| 15 | +# that need to commit without running the full check suite. It intentionally |
| 16 | +# prints a warning to stderr so the bypass is visible in CI logs. |
| 17 | +# |
| 18 | +# Clippy config drift: the hook honours clippy.toml / .clippy.toml when |
| 19 | +# present, and the workspace-level [lints.clippy] table in Cargo.toml. |
| 20 | +# Any lint configuration change is therefore automatically picked up on |
| 21 | +# the next commit — no hook update required. |
8 | 22 |
|
9 | 23 | set -euo pipefail |
10 | 24 |
|
11 | | -echo "==> Running pre-commit hooks..." |
12 | | - |
13 | | -# Check if hook bypass environment variable is explicitly set |
14 | | -if [ "${SKIP_PRE_COMMIT:-0}" = "1" ] || [ "${NO_VERIFY:-0}" = "1" ]; then |
15 | | - echo "[pre-commit] WARNING: Pre-commit checks bypassed via environment variable." |
| 25 | +# ── Bypass: SKIP_PRE_COMMIT env-var ───────────────────────────────────────── |
| 26 | +if [ "${SKIP_PRE_COMMIT:-0}" = "1" ]; then |
| 27 | + echo "[pre-commit] WARNING: checks bypassed via SKIP_PRE_COMMIT=1." >&2 |
16 | 28 | exit 0 |
17 | 29 | fi |
18 | 30 |
|
19 | | -# Retrieve staged .rs files (Added, Copied, Modified, Renamed) |
20 | | -STAGED_RS_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.rs$' || true) |
| 31 | +# ── Detect staged Rust files ───────────────────────────────────────────────── |
| 32 | +# diff-filter ACMR: Added, Copied, Modified, Renamed — excludes Deleted (D) |
| 33 | +# so we never try to check files that no longer exist on disk. |
| 34 | +STAGED_RS=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.rs$' || true) |
21 | 35 |
|
22 | | -if [ -z "$STAGED_RS_FILES" ]; then |
23 | | - echo "[pre-commit] No staged Rust files detected. Skipping fmt and clippy checks." |
| 36 | +if [ -z "$STAGED_RS" ]; then |
| 37 | + # No Rust files staged — nothing to check. |
24 | 38 | exit 0 |
25 | 39 | fi |
26 | 40 |
|
27 | | -echo "[pre-commit] Staged Rust files detected:" |
28 | | -echo "$STAGED_RS_FILES" | sed 's/^/ - /' |
| 41 | +echo "[pre-commit] Staged Rust files:" |
| 42 | +echo "$STAGED_RS" | sed 's/^/ /' |
29 | 43 |
|
30 | | -# Read staged files into array safely |
31 | | -mapfile -t FILES_ARRAY <<< "$STAGED_RS_FILES" |
| 44 | +# Resolve the repo root so cargo commands work regardless of where the user |
| 45 | +# ran `git commit` from (e.g. a subdirectory). |
| 46 | +REPO_ROOT=$(git rev-parse --show-toplevel) |
32 | 47 |
|
33 | | -# 1. Run cargo fmt --check against staged files |
34 | | -echo "[pre-commit] Running cargo fmt check..." |
35 | | -if ! cargo fmt --check -- "${FILES_ARRAY[@]}"; then |
| 48 | +# ── 1. cargo fmt --check ───────────────────────────────────────────────────── |
| 49 | +# Passes the list of staged files so only those files are checked. This is |
| 50 | +# faster than a full workspace fmt and avoids failing on pre-existing |
| 51 | +# formatting issues in files the commit does not touch. |
| 52 | +echo "[pre-commit] cargo fmt --check ..." |
| 53 | + |
| 54 | +# Build the file list as an array to handle paths with spaces safely. |
| 55 | +mapfile -t STAGED_ARRAY <<< "$STAGED_RS" |
| 56 | +# Prefix each path with the repo root so paths are absolute. |
| 57 | +ABS_FILES=() |
| 58 | +for f in "${STAGED_ARRAY[@]}"; do |
| 59 | + ABS_FILES+=("$REPO_ROOT/$f") |
| 60 | +done |
| 61 | + |
| 62 | +if ! cargo fmt --manifest-path "$REPO_ROOT/Cargo.toml" --check -- "${ABS_FILES[@]}" 2>&1; then |
36 | 63 | echo "" |
37 | | - echo "❌ Error: Code formatting check failed on staged files." |
38 | | - echo "💡 Fix by running: cargo fmt" |
| 64 | + echo "[pre-commit] ❌ Formatting check failed." >&2 |
| 65 | + echo "[pre-commit] Run \`cargo fmt\` to fix, then re-stage and commit." >&2 |
| 66 | + echo "[pre-commit] To bypass (not recommended): git commit --no-verify" >&2 |
39 | 67 | exit 1 |
40 | 68 | fi |
| 69 | +echo "[pre-commit] ✅ fmt OK" |
| 70 | + |
| 71 | +# ── 2. cargo clippy --all-targets ──────────────────────────────────────────── |
| 72 | +# We run clippy across all targets (lib, tests, benches, examples) rather than |
| 73 | +# only the staged files because: |
| 74 | +# a) Clippy cannot accept individual file paths — it works at crate granularity. |
| 75 | +# b) A change in one file can introduce a lint in a test that imports it. |
| 76 | +# |
| 77 | +# -D warnings turns all warnings into errors, matching the CI gate. |
| 78 | +# The workspace [lints.clippy] table and any clippy.toml are honoured automatically. |
| 79 | +echo "[pre-commit] cargo clippy --all-targets -- -D warnings ..." |
41 | 80 |
|
42 | | -# 2. Run cargo clippy against staged targets / workspace |
43 | | -echo "[pre-commit] Running cargo clippy..." |
44 | | -if ! cargo clippy --all-targets -- -D warnings; then |
| 81 | +if ! cargo clippy \ |
| 82 | + --manifest-path "$REPO_ROOT/Cargo.toml" \ |
| 83 | + --all-targets \ |
| 84 | + -- -D warnings 2>&1; then |
45 | 85 | echo "" |
46 | | - echo "❌ Error: Cargo clippy found compiler warnings or errors." |
47 | | - echo "💡 Fix the clippy issues before committing." |
| 86 | + echo "[pre-commit] ❌ Clippy check failed." >&2 |
| 87 | + echo "[pre-commit] Fix the warnings above, re-stage, and commit." >&2 |
| 88 | + echo "[pre-commit] To bypass (not recommended): git commit --no-verify" >&2 |
48 | 89 | exit 1 |
49 | 90 | fi |
| 91 | +echo "[pre-commit] ✅ clippy OK" |
50 | 92 |
|
51 | | -echo "✅ Pre-commit checks passed successfully!" |
| 93 | +echo "[pre-commit] All checks passed." |
52 | 94 | exit 0 |
0 commit comments