#!/usr/bin/env bash # # CascadingDev Pre-commit Hook # ============================= # This hook provides safety checks during git commits. # # What it does: # 1. Scans for potential secrets in staged changes # 2. Creates companion summary files (.sum.md) for discussion files # # Environment Variables: # CDEV_SKIP_HOOK=1 Skip all checks (hook exits immediately) # CDEV_SKIP_SUMMARIES=1 Skip summary file generation # # Safety: Exits on errors to prevent broken commits set -euo pipefail if [[ -n "${CDEV_SKIP_HOOK:-}" ]]; then exit 0 fi # Navigate to git repository root so all file paths work correctly ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")" cd "$ROOT" # ============================================================================ # CRITICAL: Acquire Hook Execution Lock # ============================================================================ # Prevents concurrent hook executions from corrupting Git repository. # Race condition scenario: # - Process A runs `git add file1.md`, computes blob SHA, starts writing to .git/objects/ # - Process B runs `git add file2.md` concurrently # - Blob object creation fails, leaving orphaned SHA in index # - Result: "error: invalid object 100644 for ''" # # Solution: Use flock to ensure only one hook instance runs at a time. # The lock is automatically released when this script exits. # ============================================================================ LOCK_FILE="${ROOT}/.git/hooks/pre-commit.lock" exec 9>"$LOCK_FILE" if ! flock -n 9; then echo >&2 "[pre-commit] Another pre-commit hook is running. Waiting for lock..." flock 9 # Block until lock is available echo >&2 "[pre-commit] Lock acquired, continuing..." fi # Cleanup: Remove lock file on exit trap 'rm -f "$LOCK_FILE"' EXIT # -------- collect staged files ---------- mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true) [ "${#STAGED[@]}" -eq 0 ] && exit 0 # -------- tiny secret scan (fast, regex only) ---------- # Abort commit if staged changes contain potential secrets matching common patterns DIFF="$(git diff --cached)" if echo "$DIFF" | grep -Eqi '(api[_-]?key|secret|access[_-]?token|private[_-]?key)[:=]\s*[A-Za-z0-9_\-]{12,}'; then echo >&2 "[pre-commit] Possible secret detected in staged changes." echo >&2 " If false positive, commit with --no-verify and add an allowlist later." exit 11 fi # -------- ensure discussion summaries exist (companion files) ---------- if [[ -z "${CDEV_SKIP_SUMMARIES:-}" ]]; then ensure_summary() { local disc="$1" local dir; dir="$(dirname "$disc")" local sum="$dir/$(basename "$disc" .md).sum.md" if [ ! -f "$sum" ]; then cat > "$sum" <<'EOF' # Summary — ## Decisions (ADR-style) - (none yet) ## Open Questions - (none yet) ## Awaiting Replies - (none yet) ## Action Items - (none yet) ## Votes (latest per participant) READY: 0 • CHANGES: 0 • REJECT: 0 - (no votes yet) ## Timeline (most recent first) - : ## Links - Related PRs: – - Commits: – - Design/Plan: ../design/design.md EOF git add "$sum" fi } # Process each staged discussion file and ensure it has a summary for f in "${STAGED[@]}"; do case "$f" in Docs/features/*/discussions/*.discussion.md) ensure_summary "$f";; esac done fi exit 0