5d2ad40336
Whole-tree radon blocking let 16 legacy C+ functions block every commit, including docs-only ones. Touched functions still block; legacy debt is reported as a warning.
108 lines
4.3 KiB
Bash
Executable File
108 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ============================================================
|
|
# pre-commit-guard.sh
|
|
# Claude Code PreToolUse hook — 拦截 git commit,执行全量检查
|
|
#
|
|
# 触发时机: Claude 尝试执行 git commit 之前
|
|
# 作用: 阻塞提交直到质量门通过(确定性质量守卫)
|
|
# 退出码: 0 = 放行, 2 = 阻塞(Claude 必须先修复问题)
|
|
#
|
|
# 注册: 见 .claude/settings.json → hooks.PreToolUse (matcher: Bash)
|
|
# ============================================================
|
|
set -euo pipefail
|
|
|
|
# PolyGateway conda 环境(存在则优先其工具链)
|
|
PROJ_ENV="$HOME/miniconda3/envs/PolyGateway/bin"
|
|
[[ -d "$PROJ_ENV" ]] && export PATH="$PROJ_ENV:$PATH"
|
|
|
|
INPUT=$(cat)
|
|
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
|
|
# 只拦截 git commit 命令
|
|
if ! echo "$COMMAND" | grep -qE '(^|\s|&&|;)\s*git\s+commit'; then
|
|
exit 0
|
|
fi
|
|
|
|
ERRORS=""
|
|
WARNINGS=""
|
|
|
|
CODE_DIRS=("src")
|
|
|
|
for CODE_DIR in "${CODE_DIRS[@]}"; do
|
|
[[ -d "$CODE_DIR" ]] || continue
|
|
|
|
# ── 1. 全量代码质量检查 ──
|
|
if command -v ruff &> /dev/null; then
|
|
# --quiet: 成功时零输出(避免把 "All checks passed!" 误判为问题),失败时只打印诊断行
|
|
RUFF_OUTPUT=$(ruff check --quiet --output-format concise "$CODE_DIR" 2>&1 || true)
|
|
if [[ -n "$RUFF_OUTPUT" ]]; then
|
|
ERROR_COUNT=$(echo "$RUFF_OUTPUT" | wc -l)
|
|
ERRORS+="[ruff] $CODE_DIR/ 中有 ${ERROR_COUNT} 个问题。运行 ruff check $CODE_DIR/ 查看详情。\n"
|
|
fi
|
|
fi
|
|
|
|
# 复杂度只对"本次改动触及的函数"硬拦: 全量拦截会让存量债务挡住一切提交
|
|
# (含纯文档提交),久之只剩绕过一条路;存量 ≥C 函数降为告警,随触及随还。
|
|
# 用 HEAD 对比工作区+暂存区: 钩子在 `git add && git commit` 同一命令里先于 add 运行
|
|
if command -v radon &> /dev/null; then
|
|
mapfile -t CHANGED_PY < <(git diff HEAD --name-only --diff-filter=AM -- "$CODE_DIR" | grep '\.py$' || true)
|
|
if [[ ${#CHANGED_PY[@]} -gt 0 ]]; then
|
|
TOUCHED=$(git diff HEAD -U0 -- "${CHANGED_PY[@]}" | python3 -c '
|
|
import json, re, subprocess, sys
|
|
hunks, path = {}, None
|
|
for line in sys.stdin:
|
|
if line.startswith("+++ "):
|
|
path = line[6:].strip() if line.startswith("+++ b/") else None
|
|
m = re.match(r"@@ -\S+ \+(\d+)(?:,(\d+))? @@", line)
|
|
if m and path:
|
|
start, count = int(m.group(1)), int(m.group(2) or 1)
|
|
hunks.setdefault(path, []).append((start, start + max(count, 1) - 1))
|
|
data = json.loads(subprocess.run(["radon", "cc", "-j", "-n", "C", *hunks], capture_output=True, text=True).stdout or "{}")
|
|
for path, blocks in data.items():
|
|
for b in blocks if isinstance(blocks, list) else []:
|
|
if any(s <= b["endline"] and e >= b["lineno"] for s, e in hunks.get(path, [])):
|
|
print("%s:%s %s - %s (%s)" % (path, b["lineno"], b["name"], b["rank"], b["complexity"]))
|
|
' 2>&1 || true)
|
|
if [[ -n "$TOUCHED" ]]; then
|
|
ERRORS+="[radon] 本次改动触及的函数圈复杂度 ≥ C:\n$TOUCHED\n"
|
|
fi
|
|
fi
|
|
LEGACY_COUNT=$(radon cc "$CODE_DIR" -n C 2>/dev/null | grep -cE '^\s+[FMC]\s' || true)
|
|
if [[ "$LEGACY_COUNT" -gt 0 ]]; then
|
|
WARNINGS+="[radon] $CODE_DIR/ 存量 ${LEGACY_COUNT} 个函数圈复杂度 ≥ C(未被本次改动触及,不阻塞)。\n"
|
|
fi
|
|
fi
|
|
|
|
# ── 2. 文件行数检查 ──
|
|
while IFS= read -r pyfile; do
|
|
lines=$(wc -l < "$pyfile")
|
|
if [[ "$lines" -gt 200 ]]; then
|
|
WARNINGS+="[行数] $pyfile 有 ${lines} 行,超过 200 行建议上限。\n"
|
|
fi
|
|
done < <(find "$CODE_DIR" -name "*.py" 2>/dev/null || true)
|
|
done
|
|
|
|
# ── 3. 测试检查 ──
|
|
if [[ -d tests ]] && command -v pytest &> /dev/null; then
|
|
if ! TEST_OUTPUT=$(pytest tests/ --tb=line -q 2>&1); then
|
|
FAILED=$(echo "$TEST_OUTPUT" | tail -3)
|
|
ERRORS+="[测试] 有测试未通过:\n$FAILED\n"
|
|
fi
|
|
fi
|
|
|
|
# ── 判定结果 ──
|
|
if [[ -n "$WARNINGS" ]]; then
|
|
echo -e "⚠️ Warnings(不阻塞):\n" >&2
|
|
echo -e "$WARNINGS" >&2
|
|
fi
|
|
|
|
if [[ -n "$ERRORS" ]]; then
|
|
echo -e "❌ 提交被阻塞 — 请先修复以下问题:\n" >&2
|
|
echo -e "$ERRORS" >&2
|
|
echo -e "修复完成后重新执行 git commit。" >&2
|
|
exit 2
|
|
fi
|
|
|
|
echo "✅ 所有检查通过,允许提交。"
|
|
exit 0
|