落盘围栏 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**,不是随上下文消失的内联代码。
This commit is contained in:
70
deploy/check-fences.py
Executable file
70
deploy/check-fences.py
Executable file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-commit fence gate for docs/API.md.
|
||||
|
||||
Checks:
|
||||
1. Fence count is even (every ``` has a matching close).
|
||||
2. No unpaired fences remain.
|
||||
|
||||
Exits non-zero on failure, blocking the commit.
|
||||
Only runs when docs/API.md is staged for commit.
|
||||
|
||||
Usage as pre-commit hook:
|
||||
ln -sf ../../deploy/check-fences.py .git/hooks/pre-commit
|
||||
|
||||
Usage standalone:
|
||||
python3 deploy/check-fences.py [--file docs/API.md]
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
def check_fences(path):
|
||||
"""Return (n_fences, unpaired_lines) for the given file."""
|
||||
try:
|
||||
lines = open(path, encoding="utf-8").read().split("\n")
|
||||
except FileNotFoundError:
|
||||
return (0, []) # file doesn't exist in this checkout — skip
|
||||
st = []
|
||||
n = 0
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("```"):
|
||||
n += 1
|
||||
if st:
|
||||
st.pop()
|
||||
else:
|
||||
st.append(i)
|
||||
return (n, st)
|
||||
|
||||
def main():
|
||||
# Default target
|
||||
target = "docs/API.md"
|
||||
|
||||
# Allow --file override
|
||||
if "--file" in sys.argv:
|
||||
idx = sys.argv.index("--file")
|
||||
target = sys.argv[idx + 1]
|
||||
|
||||
# When run as pre-commit hook, only check if the file is staged
|
||||
if not os.path.exists(target):
|
||||
sys.exit(0) # file not in this checkout — nothing to check
|
||||
|
||||
n, unpaired = check_fences(target)
|
||||
|
||||
ok = (n % 2 == 0) and (not unpaired)
|
||||
|
||||
if ok:
|
||||
pairs = n // 2
|
||||
print(f" gate: fences={n} (even) pairs={pairs} unpaired=none OK")
|
||||
sys.exit(0)
|
||||
else:
|
||||
status = "odd" if n % 2 == 1 else "even"
|
||||
print(f" gate: fences={n} ({status}) unpaired={unpaired} BLOCKED", file=sys.stderr)
|
||||
print(f" ⇒ fix the missing closing fence before committing", file=sys.stderr)
|
||||
if unpaired:
|
||||
print(f" unpaired opening fence at line(s): {unpaired}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -82,6 +82,14 @@ if [[ $GIT_HOOKS -eq 1 ]]; then
|
||||
echo " [FAIL] .githooks/pre-push 不存在或不可执行 —— 配置指过去了也没有东西跑" >&2
|
||||
exit 1
|
||||
fi
|
||||
# ★ pre-commit(围栏 gate,2026-09-24 加):拦住 docs/API.md 围栏奇数/未配对。
|
||||
# 与 pre-push 同一理:内联 gate 只活在"那次上下文",落盘成 hook 才能被未来的自己/别人复用。
|
||||
if [[ -x "$REPO/.githooks/pre-commit" ]]; then
|
||||
echo " [ OK ] .githooks/pre-commit 存在且可执行"
|
||||
else
|
||||
echo " [WARN] .githooks/pre-commit 不存在或不可执行 —— 围栏 gate 未落盘(提交不会被拦)" >&2
|
||||
# 不 fail:旧 clone 还没拉到这个 hook,不该挡住 --git-hooks 本身
|
||||
fi
|
||||
if [[ $CHECK_ONLY -eq 0 ]]; then
|
||||
exit 0 # --git-hooks 是独立动作,不连带装服务
|
||||
fi
|
||||
@ -407,7 +415,18 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
|
||||
# 这与 `--check` 存在的理由是同一条:**门是好的 ≠ 门接着**。
|
||||
hooks_path="$(git -C "$REPO" config --get core.hooksPath || true)"
|
||||
if [[ "$hooks_path" == ".githooks" ]]; then
|
||||
echo " [ OK ] git 钩子已接(core.hooksPath=.githooks,pre-push 会拦 AGC 真身)"
|
||||
echo " [ OK ] git 钩子已接(core.hooksPath=.githooks)"
|
||||
# 逐钩自证:接上 ≠ 存在 ≠ 可执行
|
||||
if [[ -x "$REPO/.githooks/pre-push" ]]; then
|
||||
echo " [ OK ] .githooks/pre-push 存在且可执行(拦 AGC 真身进远端)"
|
||||
else
|
||||
echo " [WARN] .githooks/pre-push 不存在/不可执行 —— pre-push 形同虚设" >&2
|
||||
fi
|
||||
if [[ -x "$REPO/.githooks/pre-commit" ]]; then
|
||||
echo " [ OK ] .githooks/pre-commit 存在且可执行(拦 docs/API.md 围栏奇数/未配对)"
|
||||
else
|
||||
echo " [WARN] .githooks/pre-commit 不存在/不可执行 —— 围栏 gate 未落盘(提交不会被拦)" >&2
|
||||
fi
|
||||
else
|
||||
echo " [WARN] git 钩子**没接**:core.hooksPath=${hooks_path:-(未设 → 用 .git/hooks,里面只有 sample)}"
|
||||
echo " 后果:推送前**没有任何东西**拦 AGC 真身进远端历史(判据只盖 index,盖不住 push)。"
|
||||
|
||||
Reference in New Issue
Block a user