141 lines
5.1 KiB
Bash
Executable File
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
|