CascadingDev/assets/hooks/pre-commit

141 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Safety settings: exit on errors, treat unset variables as errors, and catch pipeline failures
set -euo pipefail
# Find and navigate to the git repo root (or current dir if not in a repo) so file paths work correctly regardless of where the commit command is run
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
cd "$ROOT"
resolve_template() {
local tmpl="$1" rel_path="$2"
local today dirpath basename name ext feature_id stage
today="$(date +%F)"
dirpath="$(dirname "$rel_path")"
basename="$(basename "$rel_path")"
name="${basename%.*}"
ext="${basename##*.}"
feature_id=""
stage=""
feature_id="$(echo "$rel_path" | sed -n 's|.*Docs/features/\(FR_[^/]*\).*|\1|p')"
stage="$(echo "$basename" | sed -n 's/^\([A-Za-z0-9_-]\+\)\.discussion\.md$/\1|p')"
echo "$tmpl" \
| sed -e "s_{date}_$today_g" \
-e "s_{rel}_$rel_path_g" \
-e "s_{dir}_$dirpath_g" \
-e "s_{basename}_$basename_g" \
-e "s_{name}_$name_g" \
-e "s_{ext}_$ext_g" \
-e "s_{feature_id}_$feature_id_g" \
-e "s_{stage}_$stage_g"
}
# Helper function to apply a patch with 3-way merge fallback
apply_patch_with_3way() {
local patch_file="$1"
local target_file="$2"
if [ ! -f "$patch_file" ]; then
echo >&2 "[pre-commit] Error: Patch file not found: $patch_file"
return 1
fi
# Attempt 3-way apply
if git apply --index --3way --recount --whitespace=nowarn "$patch_file"; then
echo >&2 "[pre-commit] Applied patch to $target_file with 3-way merge."
elif git apply --index "$patch_file"; then
echo >&2 "[pre-commit] Applied patch to $target_file with strict apply (3-way failed)."
else
echo >&2 "[pre-commit] Error: Failed to apply patch to $target_file."
echo >&2 " Manual intervention may be required."
return 1
fi
return 0
}
# Helper function to check if changes to a discussion file are append-only
check_append_only_discussion() {
local disc_file="$1"
local diff_output
# Get the cached diff for the discussion file
diff_output=$(git diff --cached "$disc_file")
# Check if there are any deletions or modifications to existing lines
# This is a simplified check; a more robust solution would parse hunks
if echo "$diff_output" | grep -E "^-[^-]" | grep -Ev "^--- a/" | grep -Ev "^\+\+\+ b/"; then
echo >&2 "[pre-commit] Error: Deletions or modifications detected in existing lines of $disc_file."
echo >&2 " Discussion files must be append-only, except for allowed header fields."
return 1
fi
# Check for modifications to header fields (status, timestamps, feature_id, stage_id)
# This is a basic check and might need refinement based on actual header structure
# For now, we'll allow changes to lines that look like header fields.
# A more robust solution would parse YAML front matter.
local header_modified=0
if echo "$diff_output" | grep -E "^[-+]" | grep -E "^(status|created|updated|feature_id|stage_id):" > /dev/null; then
header_modified=1
fi
# If there are additions, ensure they are at the end of the file, or are allowed header modifications
# This is a very basic check. A more advanced check would compare line numbers.
# For now, if there are additions and no deletions/modifications to body, we assume append-only.
if echo "$diff_output" | grep -E "^\+[^+]" | grep -Ev "^\+\+\+ b/" > /dev/null && [ "$header_modified" -eq 0 ]; then
: # Placeholder for more robust append-only check
fi
return 0
}
# -------- collect staged files ----------
# Get list of staged added/modified files into STAGED array, exit early if none found
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true)
[ "${#STAGED[@]}" -eq 0 ] && exit 0
# -------- ensure discussion summaries exist (companion files) ----------
# Create and auto-stage a summary template file for any discussion file that doesn't already have one
ensure_summary() {
local disc="$1"
local dir; dir="$(dirname "$disc")"
local sum="$dir/$(basename "$disc" .md).sum.md"
local template_path="assets/templates/feature.discussion.sum.md"
if [ ! -f "$sum" ]; then
# Copy the template content directly
cat "$template_path" > "$sum"
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"
if ! check_append_only_discussion "$f"; then
exit 1 # Exit with error if append-only check fails
fi
;;
esac
done
# -------- orchestration (non-blocking status) ----------
# -------- automation runner (AI outputs) ----------
if [ -f "automation/runner.py" ]; then
if ! python3 -m automation.runner; then
echo "[pre-commit] automation.runner failed" >&2
exit 1
fi
fi
# -------- orchestration (non-blocking status) ----------
# NOTE: automation/workflow.py provides non-blocking vote status reporting.
# It parses VOTE: lines from staged discussion files and prints a summary.
# Run workflow status check if available, but don't block commit if it fails.
if [ -x "automation/workflow.py" ]; then
python3 automation/workflow.py --status || true
fi
exit 0