四类钩子:格式化能改代码、测试能判对错、审计只记录、阻断直接退。 顺序有意义:能改的放前面,只判的放后面,最后才是阻断。 钩子世界只有一种语言:退出码。零放行,非零拦住。
上一篇说钩子靠系统执行。这一篇讲钩子具体怎么写、怎么串。 Demo 写了四个真钩子脚本:去行尾空白、跑测试、记审计日志、拦禁区路径。 然后用一个 runner 按顺序串起来跑两遍——一遍干净、一遍危险。 两遍的输出都在下面:第一遍四个退出码全零,第二遍在最后一个退出码二,整条链断。
一、四类钩子各管一段
| 类型 | 干什么 | 退出码 | 能不能改代码 |
|---|---|---|---|
| 格式化 | 去行尾空白、跑 formatter | 一般零 | 能 |
| 测试 | 跑验证,看结果 | 失败非零 | 不能 |
| 审计 | 记一条日志 | 永远零 | 不能 |
| 阻断 | 路径与动作黑名单 | 命中退出二 | 不能 |
「能不能改代码」这一列最关键。
只有格式化能改。
这个限制不是技术问题,是逻辑问题:
如果测试钩子会顺手修正代码,那「测试通过」就不再是「代码对」的证据——
因为它可以把自己测的东西改到通过。
审计钩子的特点是「永远返回零」。
它管的是「有没有发生」,不是「许不许发生」。
Demo 里 audit.log 记了两条,两条都记下来了,包括那条被拦的。
这一点很重要:被拦的动作更该记。
二、钩子的执行时机
时机有三种,作用是递进的:
| 时机 | 适合放什么 | 拦得住吗 |
|---|---|---|
| 改完文件立刻 | 格式化 | 不用拦,它只修正 |
| 提交之前 | 测试、阻断 | 能拦,退出码非零就不提交 |
| 推送之前 | 审计、通知 | 能拦,但已经晚了一步 |
越晚越贵。 改完立刻发现问题,成本是一行;提交前发现,成本是一次 amend; 推送后才发现,成本是一次回滚加一次道歉。 所以顺序要这样排: 能修的(格式化)最先跑,因为它让后面看到的都是干净版本; 只判的(测试)其次; 阻断最后,因为它一旦退出,后面都不用跑了。 Demo 第二遍就是这个顺序跑下来的: 格式化退出零、测试退出零、审计退出零、阻断退出二。 前三个都执行了,第四个断掉——这符合预期,因为阻断本来就该在最后。
三、阻断要有明确的退出码
钩子不读日志,只读退出码。
零是放行,非零是拦住——就这么一条规则。
所以自己写的钩子脚本,结束前必须把退出码说清楚。
Python 里是 sys.exit(0) 和 sys.exit(1);shell 里是 exit 0 和 exit 1。
不写的话,退出码是最后一条命令的——那可能是个巧合的零。
退出码还能分级。
Demo 里阻断用二,测试用一。
这个区分在真机上很有用:一表示「检查没过」,二表示「根本不许做」——
前者可以改了再试,后者应该停下来问人。
还有一个坑:钩子自己崩了怎么办?
崩了就是非零退出码,等于拦住。
这个默认行为是对的——宁可拦住,也不要放一个不知道自己有没有检查的东西过去。
四、动手 Demo:四类钩子:格式化、测试、审计、阻断
四个真脚本加一个 runner,串起来跑两遍。
第一遍是干净的改动,四个全过;第二遍要改 data/,被最后一个拦住。
这个 runner 复刻的是真机上 .git/hooks/pre-commit 的行为:
按顺序执行、任一环非零就停、把退出码传出去。
沙箱里没装 hook,所以用显式 runner 跑——语义一样。
STEPS = [("fmt", ["python", "hooks/fmt.py", "src/main.py"]),
("test", ["python", "hooks/test.py"]),
("audit", ["python", "hooks/audit.py", msg]),
("block", ["python", "hooks/block.py"] + paths)]
for name, cmd in STEPS:
r = subprocess.run(cmd, capture_output=True)
if r.returncode != 0:
rc_all = r.returncode
break
sys.exit(rc_all)
实跑输出:
--------------------------------------------------------------------
四类钩子:格式化、测试、审计、阻断
--------------------------------------------------------------------
① 四个钩子,各管一段
--------------------------------------------------------------------
hooks/fmt.py
hooks/test.py
hooks/audit.py
hooks/block.py
四类钩子的共同点:都是脚本,都用退出码说话。零放行,非零拦住——钩子世界只有这一种语言。
② 一次干净的提交:四个钩子全过
--------------------------------------------------------------------
[fmt] 退出码 0 | 格式化:去掉行尾空白 2 行
[test] 退出码 0 | 测试:通过 2 / 失败 0
[audit] 退出码 0 | 审计:已记一条
[block] 退出码 0 | 阻断:检查通过
汇总退出码 0
③ 一次危险的提交:被最后一个钩子拦住
--------------------------------------------------------------------
[fmt] 退出码 0 | 格式化:去掉行尾空白 0 行
[test] 退出码 0 | 测试:通过 2 / 失败 0
[audit] 退出码 0 | 审计:已记一条
[block] 退出码 2 | 阻断:data/stopwords.txt 在禁区里
汇总退出码 2
前两个跑完了,第三个记了审计,第四个退出码二,整条链断。
这是钩子链的默认语义:任一环非零,后面不再执行。
! 所以顺序有意义:格式化和测试要跑在前面——它们能「改」代码,让后面的检查看到干净的版本。
阻断放最后,因为它一旦退出,前面都不用跑了。
④ 审计钩子真写了一条日志
--------------------------------------------------------------------
audit.log 里 2 条:
{"when": "13:43:24", "what": "feat: 加一个分支"}
{"when": "13:43:28", "what": "chore: 顺手改数据"}
审计钩子的作用是「事后能查」。
它永远返回零,从不拦人——因为它管的是「有没有发生」,不是「许不许发生」。
⑤ 四类钩子各管什么
--------------------------------------------------------------------
| 类型 | 干什么 | 退出码 | 能不能改代码 |
|---|---|---|:---:|
| 格式化 | 去行尾空白、跑 formatter | 一般零 | 能 |
| 测试 | 跑验证,看结果 | 失败非零 | 不能 |
| 审计 | 记一条日志 | 永远零 | 不能 |
| 阻断 | 路径与动作白名单 | 命中退出二 | 不能 |
! 只有第一个能改代码。
这个区别很重要:格式化可以悄悄地改,测试和阻断只能判——所以别把「修正」写进测试钩子,
那会让结果不可复现。
第一遍四个零,第二遍最后一个二。
注意第二遍的格式化报「去掉行尾空白 0 行」——
因为第一遍已经去过了。这说明钩子是幂等的:跑两遍和跑一遍一样。
audit.log 两条都记下来了,包括那条被拦的。
被拦的动作更该记——因为「有人试图改数据」这件事,比「改了数据」更值得知道。
第二遍整条链在第四个断掉,但前三个都跑完了。
这是有意的顺序:格式化和测试应该在阻断之前跑完,
因为你要看的是「代码在干净状态下测过没有」,不是「它有没有碰禁区」。
最后那张表只有一行值得背:只有格式化能改代码。
测试和阻断只能判——判和改分开,证据才成立。
30 秒实验
- 写一个最小的钩子:只做一件事,只返回一个退出码。 先跑通它,再加第二件事。
- 看一下你的钩子有没有显式
exit。 没有的话,它的退出码是碰运气。 - 给「不许做」和「做得不对」用不同的退出码。 前者该停下来问人,后者可以改了再试。
常见误区
- 「钩子里顺手把问题修了就完了。」 只有格式化能修。 测试钩子修代码,等于自己给自己判卷。
- 「审计钩子没必要,出事了看 git log。」 git log 里没有「试图做但被拦住」的记录。 而那恰恰是最该看的。
- 「钩子慢,跑一次要十几秒。」 那就只跑相关的。 慢到被人跳过,比没有更糟——因为你会以为它在跑。
自测三问(点开对答案)
1. 四类钩子里哪一类能改代码?为什么其他三类不能? —— **只有格式化能。** **如果测试钩子会修代码,「测试通过」就不再是「代码对」的证据**—— **它可以把自己测的东西改到通过。** 2. 钩子的执行顺序为什么是「格式化、测试、审计、阻断」? —— **能改的放前面,让后面看到干净版本;只判的其次;阻断最后,因为它一旦退出后面都不用跑了。** 3. 钩子崩了会怎样?这个默认行为合理吗? —— **崩了就是非零退出码,等于拦住。** **合理**——宁可拦住,也不要放一个不知道自己有没有检查的东西过去。下一篇 7.3 讲最后一道闸:审批点怎么设计,以及为什么审批点太多等于没有。
- ☐ (本篇完,下一篇 7.3 审批点怎么设计)