Files
gh-fcakyon-claude-codex-set…/hooks/scripts/git_commit_confirm.py
2025-11-29 18:26:35 +08:00

163 lines
4.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""PreToolUse hook: show confirmation modal before creating git commit."""
import json
import re
import subprocess
import sys
def parse_git_commit_message(command: str) -> dict[str, str]:
"""Parse git commit command to extract commit message.
Args:
command (str): The git commit command string
Returns:
(dict): Dictionary with message and is_amend keys
"""
params = {"message": "", "is_amend": False}
# Check for --amend flag
params["is_amend"] = "--amend" in command
# Try to extract heredoc format: git commit -m "$(cat <<'EOF' ... EOF)"
heredoc_match = re.search(r"<<'EOF'\s*\n(.*?)\nEOF", command, re.DOTALL)
if heredoc_match:
params["message"] = heredoc_match.group(1).strip()
return params
# Try to extract simple -m "message" format
simple_matches = re.findall(r'(?:-m|--message)\s+["\']([^"\']+)["\']', command)
if simple_matches:
# Join multiple -m flags with double newlines
params["message"] = "\n\n".join(simple_matches)
return params
return params
def get_staged_files() -> tuple[list[str], str]:
"""Get list of staged files and diff stats.
Returns:
(tuple): (list of file paths, diff stats string)
"""
try:
# Get list of staged files
files_result = subprocess.run(
["git", "diff", "--cached", "--name-only"],
capture_output=True,
text=True,
timeout=5,
)
# Get diff stats
stats_result = subprocess.run(
["git", "diff", "--cached", "--stat"],
capture_output=True,
text=True,
timeout=5,
)
files = []
if files_result.returncode == 0:
files = [f for f in files_result.stdout.strip().split("\n") if f]
stats = ""
if stats_result.returncode == 0:
# Get last line which contains the summary
stats_lines = stats_result.stdout.strip().split("\n")
if stats_lines:
stats = stats_lines[-1]
return files, stats
except (subprocess.TimeoutExpired, FileNotFoundError):
return [], ""
def format_confirmation_message(message: str, is_amend: bool, files: list[str], stats: str) -> str:
"""Format commit parameters into readable confirmation message.
Args:
message (str): Commit message
is_amend (bool): Whether this is an amend commit
files (list): List of staged file paths
stats (str): Diff statistics string
Returns:
(str): Formatted confirmation message
"""
lines = []
# Header
if is_amend:
lines.append("💾 Amend Previous Commit?")
else:
lines.append("💾 Create Commit?")
lines.append("")
# Commit message
if message:
lines.append("Message:")
lines.append(message)
lines.append("")
# Files
if files:
lines.append(f"Files to be committed ({len(files)}):")
# Show first 15 files, truncate if more
display_files = files[:15]
for f in display_files:
lines.append(f"- {f}")
if len(files) > 15:
lines.append(f"... and {len(files) - 15} more files")
lines.append("")
# Stats
if stats:
lines.append("Stats:")
lines.append(stats)
# Warning if no files staged
if not files:
lines.append("⚠️ No files staged for commit")
return "\n".join(lines)
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
command = tool_input.get("command", "")
# Only handle git commit commands
if tool_name != "Bash" or not command.strip().startswith("git commit"):
sys.exit(0)
# Parse commit message
params = parse_git_commit_message(command)
# Get staged files and stats
files, stats = get_staged_files()
# Format confirmation message
message = format_confirmation_message(params["message"], params["is_amend"], files, stats)
# Return JSON with ask decision
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": message,
}
}
print(json.dumps(output))
sys.exit(0)