125 lines
3.8 KiB
Bash
Executable File
125 lines
3.8 KiB
Bash
Executable File
#!/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 <SHA> for '<file>'"
|
||
#
|
||
# 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 — <Stage Title>
|
||
|
||
<!-- SUMMARY:DECISIONS START -->
|
||
## Decisions (ADR-style)
|
||
- (none yet)
|
||
<!-- SUMMARY:DECISIONS END -->
|
||
|
||
<!-- SUMMARY:OPEN_QUESTIONS START -->
|
||
## Open Questions
|
||
- (none yet)
|
||
<!-- SUMMARY:OPEN_QUESTIONS END -->
|
||
|
||
<!-- SUMMARY:AWAITING START -->
|
||
## Awaiting Replies
|
||
- (none yet)
|
||
<!-- SUMMARY:AWAITING END -->
|
||
|
||
<!-- SUMMARY:ACTION_ITEMS START -->
|
||
## Action Items
|
||
- (none yet)
|
||
<!-- SUMMARY:ACTION_ITEMS END -->
|
||
|
||
<!-- SUMMARY:VOTES START -->
|
||
## Votes (latest per participant)
|
||
READY: 0 • CHANGES: 0 • REJECT: 0
|
||
- (no votes yet)
|
||
<!-- SUMMARY:VOTES END -->
|
||
|
||
<!-- SUMMARY:TIMELINE START -->
|
||
## Timeline (most recent first)
|
||
- <YYYY-MM-DD HH:MM> <name>: <one-liner>
|
||
<!-- SUMMARY:TIMELINE END -->
|
||
|
||
<!-- SUMMARY:LINKS START -->
|
||
## Links
|
||
- Related PRs: –
|
||
- Commits: –
|
||
- Design/Plan: ../design/design.md
|
||
<!-- SUMMARY:LINKS END -->
|
||
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
|