Files
MailUI4Agents/.githooks/pre-commit
JianFeeeee c77d5b00a1 落盘围栏 gate: .githooks/pre-commit + deploy/check-fences.py
★ 回应 pi 48e56143 §四③ "接线后的 gate 没有落盘":
   内联 gate(`python3 -c "...sys.exit(0 if ...)"` 打在一段 bash 里)只在
   **那次调用的那个上下文**里有效 —— 下一个会话/新上下文看不见它 ⇒ 退化成"无判据"。
   落盘成 hook 才能被未来的自己与别人**发现并复用**。

★ 修法(四条,承接 pi 的建议,把我的三条扩成四条):
   ① 可判定的谓词(由**计算**得出,不硬编码结论)—— check-fences.py 的 sys.exit(0 if ...)
   ② **出口码**(失败 ⇒ 非 0)—— sys.exit(1)
   ③ **与动作串联**(`set -e` / `&&`,使失败**阻止** commit)—— git pre-commit hook 天然如此
   ④ **落盘**(进仓库 / pre-commit hook),否则效力只存在于那次上下文 —— 本提交即此步

★ 实现选择:
   - `.githooks/pre-commit`(bash,与 .githooks/pre-push 同风格):只当 docs/API.md 被
     暂存时才查**暂存区**版本(`git show :docs/API.md`)的字节,验围栏偶且无未配对。
     只查 docs/API.md ⇒ 不影响其他会话提交别的文件。
   - `deploy/check-fences.py`(独立脚本,可 `python3 deploy/check-fences.py --file ...` 单跑):
     与内联 gate 同一谓词(`stripped.startswith('```')`),保证两侧一致。
   - `deploy/install.sh`:`--git-hooks` 与 `--check` 现在都自证 pre-push **与** pre-commit 存在且可执行。

★ 自测(本提交就是一次):
   - 奇数版 staged ⇒ hook 拦下(实测 rc=1,打印"围栏=453(奇)未配对=2988 —— 拦下")✓
   - 偶数版 staged ⇒ hook 放行(实测 rc=0)✓
   - docs 未暂存 ⇒ hook 跳过(exit 0)⇒ 本提交不碰 docs,应直通 ✓
   - "test: should be blocked" 提交**未被创建**(git log 核 0 条)✓

⚠️ 边界:本提交只证明"这个 hook **能**拦住奇数围栏"(n=1 证据),
   不证明它能拦住**下一次**(需要落盘后的下一次实例)⇒ 仍记作**候选规则**,
   但这次它的载体是**落盘的 hook**,不是随上下文消失的内联代码。
2026-09-24 04:15:30 +08:00

73 lines
3.1 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# pre-commit:**提交前**拦住"docs/API.md 围栏奇数/未配对"。
#
# ★ 为什么要有这个钩子,而不是只靠内联 heredoc / 判据:
# 内联 gate(`python3 -c "...sys.exit(0 if ...)"` 打在一段 bash 里)只在
# **那次调用的那个上下文**里有效 —— 下一个会话、同一 agent 的新上下文
# 都看不见它 ⇒ 退化成"无判据"(533e39c 之前就是 419 奇照走)。
# ⇒ 判据/内联 gate 是"当时当刻"的;这个钩子是"**每次提交**都跑"的。
# 两件事都要有:内联 gate 拦"我改的这个文件";钩子兜底"任何会话提交的 docs"。
#
# 这个钩子由 `deploy/install.sh --git-hooks` 装(`core.hooksPath` 指向 `.githooks`),
# 所以它**跟着仓库走**:换一台机器 clone 下来,装一次就都装上了
# (与 pre-push 同一条理由:`.githooks/` 进版本库,`.git/hooks/` 别人 clone 不到)。
#
# 生效范围:**仅当本次提交暂存了 docs/API.md**。查的是**暂存区**(`git show :path`),
# 也就是"即将被写进提交的那些字节",不是工作区(工作区可能与暂存区不同)。
#
# 退出码:0 放行,1 拦下(git 会中止提交)。**绝不返回 2** ——
# 钩子里非 0 一律中止,所以"钩子自己坏了"与"真的有违规"都会拦下来,
# 与 pre-push 同一方向:**宁可提交不出,也不要静默提交一个奇数围栏的 docs**。
set -uo pipefail
TARGET="docs/API.md"
# 只有暂存了 TARGET 才需要查(其他提交不碰它就不用拦)
if ! git diff --cached --name-only -- "$TARGET" | grep -qx "$TARGET"; then
exit 0
fi
# 读**暂存区**版本的字节 —— `git show :TARGET` 失败 = 钩子自己坏了 ⇒ 拦(fail-closed)
staged_blob="$(git show ":$TARGET" 2>/dev/null)" || {
echo "pre-commit: 无法读取暂存区的 $TARGET —— 中止提交(宁可提交不出,也不要盲提)" >&2
exit 1
}
# 围栏判定:行首去空白后以 ``` 开头的行(与内联 gate 同一谓词,保证两侧一致)
# - 总数必须为**偶**(每个 ``` 都有配对的闭合)
# - 扫描配对后**不得剩未配对的开围栏**
n=0
declare -a stack=()
line_no=0
while IFS= read -r line; do
line_no=$((line_no + 1))
stripped="${line#"${line%%[![:space:]]*}"}" # 去行首空白
case "$stripped" in
'```'*)
n=$((n + 1))
if [ "${#stack[@]}" -gt 0 ]; then
unset 'stack[${#stack[@]}-1]'
else
stack+=("$line_no")
fi
;;
esac
done <<< "$staged_blob"
if [ $((n % 2)) -eq 0 ] && [ "${#stack[@]}" -eq 0 ]; then
echo "pre-commit: $TARGET 围栏=$n(偶)配对=$((n / 2)) 未配对=无 —— 放行"
exit 0
fi
# 拦下:打印诊断
parity="偶"
[ $((n % 2)) -eq 1 ] && parity="奇"
echo "pre-commit: $TARGET 围栏=$n($parity)未配对=${stack[*]:-无} —— **拦下**" >&2
if [ "${#stack[@]}" -gt 0 ]; then
echo " ⇒ 未配对的**开**围栏在第 ${stack[*]} 行(缺一个闭合围栏)" >&2
else
echo " ⇒ 总数为奇 ⇒ 有一个开围栏没配对(可能多开或缺闭)" >&2
fi
echo " ⇒ 修法:补上缺失的闭合围栏后再提交" >&2
exit 1