mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
The auto-commit bash and Python twins strip a leading/trailing quote from the configured `message:` value with an end-of-string-anchored quote strip. When the YAML value has trailing whitespace after the closing quote (`message: "Done" `), the close-quote strip is anchored to end-of-string, so it never matches the quote (spaces follow it). The commit message then keeps a dangling quote and trailing spaces (`Done" `). The PowerShell twin already .Trim()s before stripping, so it produced the clean `Done`. This left the three script variants out of parity. Trim the value before stripping quotes in the bash and Python twins so all three agree. Verified at the exact-code level: the old bash sed pipeline yields `spec done" ` and the new one `spec done`; the Python _strip_quotes matches. Add a parity regression test with trailing whitespace after the closing quote (runs under CI where Git bash is resolvable).
146 lines
5.2 KiB
Bash
Executable File
146 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Git extension: auto-commit.sh
|
|
# Automatically commit changes after a Spec Kit command completes.
|
|
# Checks per-command config keys in git-config.yml before committing.
|
|
#
|
|
# Usage: auto-commit.sh <event_name>
|
|
# e.g.: auto-commit.sh after_specify
|
|
|
|
set -e
|
|
|
|
EVENT_NAME="${1:-}"
|
|
if [ -z "$EVENT_NAME" ]; then
|
|
echo "Usage: $0 <event_name>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
_find_project_root() {
|
|
local dir="$1"
|
|
while [ "$dir" != "/" ]; do
|
|
if [ -d "$dir/.specify" ] || [ -d "$dir/.git" ]; then
|
|
echo "$dir"
|
|
return 0
|
|
fi
|
|
dir="$(dirname "$dir")"
|
|
done
|
|
return 1
|
|
}
|
|
|
|
REPO_ROOT=$(_find_project_root "$SCRIPT_DIR") || REPO_ROOT="$(pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
# Check if git is available
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
echo "[specify] Warning: Git not found; skipped auto-commit" >&2
|
|
exit 0
|
|
fi
|
|
|
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
echo "[specify] Warning: Not a Git repository; skipped auto-commit" >&2
|
|
exit 0
|
|
fi
|
|
|
|
# Read per-command config from git-config.yml
|
|
_config_file="$REPO_ROOT/.specify/extensions/git/git-config.yml"
|
|
_enabled=false
|
|
_commit_msg=""
|
|
|
|
if [ -f "$_config_file" ]; then
|
|
# Parse the auto_commit section for this event.
|
|
# Look for auto_commit.<event_name>.enabled and .message
|
|
# Also check auto_commit.default as fallback.
|
|
_in_auto_commit=false
|
|
_in_event=false
|
|
_default_enabled=false
|
|
|
|
while IFS= read -r _line; do
|
|
# Detect auto_commit: section
|
|
if echo "$_line" | grep -q '^auto_commit:'; then
|
|
_in_auto_commit=true
|
|
_in_event=false
|
|
continue
|
|
fi
|
|
|
|
# Exit auto_commit section on next top-level key
|
|
if $_in_auto_commit && echo "$_line" | grep -Eq '^[a-z]'; then
|
|
break
|
|
fi
|
|
|
|
if $_in_auto_commit; then
|
|
# Check default key
|
|
if echo "$_line" | grep -Eq "^[[:space:]]+default:[[:space:]]"; then
|
|
_val=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
[ "$_val" = "true" ] && _default_enabled=true
|
|
fi
|
|
|
|
# Detect our event subsection
|
|
if echo "$_line" | grep -Eq "^[[:space:]]+${EVENT_NAME}:"; then
|
|
_in_event=true
|
|
continue
|
|
fi
|
|
|
|
# Inside our event subsection
|
|
if $_in_event; then
|
|
# Exit on next sibling key (same indent level as event name)
|
|
if echo "$_line" | grep -Eq '^[[:space:]]{2}[a-z]' && ! echo "$_line" | grep -Eq '^[[:space:]]{4}'; then
|
|
_in_event=false
|
|
continue
|
|
fi
|
|
if echo "$_line" | grep -Eq '[[:space:]]+enabled:'; then
|
|
_val=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
[ "$_val" = "true" ] && _enabled=true
|
|
[ "$_val" = "false" ] && _enabled=false
|
|
fi
|
|
if echo "$_line" | grep -Eq '[[:space:]]+message:'; then
|
|
# Trim trailing whitespace before stripping the closing quote:
|
|
# a value like `message: "Done" ` (trailing spaces after the
|
|
# quote) would otherwise leave the quote dangling (`Done" `),
|
|
# since the closing-quote strip is anchored to end-of-string.
|
|
# The PowerShell twin .Trim()s first; match it for parity.
|
|
_commit_msg=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | sed 's/[[:space:]]*$//' | sed 's/^["'\'']//' | sed 's/["'\'']*$//')
|
|
fi
|
|
fi
|
|
fi
|
|
done < "$_config_file"
|
|
|
|
# If event-specific key not found, use default
|
|
if [ "$_enabled" = "false" ] && [ "$_default_enabled" = "true" ]; then
|
|
# Only use default if the event wasn't explicitly set to false
|
|
# Check if event section existed at all
|
|
if ! grep -q "^[[:space:]]*${EVENT_NAME}:" "$_config_file" 2>/dev/null; then
|
|
_enabled=true
|
|
fi
|
|
fi
|
|
else
|
|
# No config file — auto-commit disabled by default
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$_enabled" != "true" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Check if there are changes to commit
|
|
if git diff --quiet HEAD 2>/dev/null && git diff --cached --quiet 2>/dev/null && [ -z "$(git ls-files --others --exclude-standard 2>/dev/null)" ]; then
|
|
echo "[specify] No changes to commit after $EVENT_NAME" >&2
|
|
exit 0
|
|
fi
|
|
|
|
# Derive a human-readable command name from the event
|
|
# e.g., after_specify -> specify, before_plan -> plan
|
|
_command_name=$(echo "$EVENT_NAME" | sed 's/^after_//' | sed 's/^before_//')
|
|
_phase=$(echo "$EVENT_NAME" | grep -q '^before_' && echo 'before' || echo 'after')
|
|
|
|
# Use custom message if configured, otherwise default
|
|
if [ -z "$_commit_msg" ]; then
|
|
_commit_msg="[Spec Kit] Auto-commit ${_phase} ${_command_name}"
|
|
fi
|
|
|
|
# Stage and commit
|
|
_git_out=$(git add . 2>&1) || { echo "[specify] Error: git add failed: $_git_out" >&2; exit 1; }
|
|
_git_out=$(git commit -q -m "$_commit_msg" 2>&1) || { echo "[specify] Error: git commit failed: $_git_out" >&2; exit 1; }
|
|
|
|
echo "[OK] Changes committed ${_phase} ${_command_name}" >&2
|