马上告诉你的钩子和最终拦住你的钩子,不在同一个地方
一句话总结
客户端钩子(commit-msg、pre-commit、pre-push)提供的是快速反馈,服务器钩子和 CI 才负责强制。客户端钩子只要一个 --no-verify 就能绕过,而且不会随 clone 一起带过来,所以不能指望它具有强制力而把规则挂在这里。
为什么需要它
团队里会产生规则:提交标题要用规定的格式,禁止提交超过 100KB 的文件,禁止直接向 main push。写在文档里没人遵守,在评审中指出时提交已经堆了一堆,改起来很麻烦。
把脚本放进 .git/hooks/,git 会在特定时刻执行它,比如提交之前、写完消息之后、push 之前。退出码不是 0,该操作就会停止。
但 .git/hooks/ 有一个大问题:.git 目录不会随 clone 一起带过来。 不管我做得多好,别人的机器上都没有。于是各个团队都附上了“请运行安装脚本”之类的说明,结果没有人运行。
工作原理
从 git 2.9 起有了 core.hooksPath 配置,用来改变查找钩子的位置。
git config core.hooksPath .githooks
这样 .githooks/ 就成了被仓库跟踪的普通目录,钩子脚本就会被提交并共享。 不过 core.hooksPath 这项配置本身位于 .git/config 中,所以每个人仍然需要自己打开一次。把这一行写进 README 或引导(bootstrap)脚本,是如今的惯例。
常用的三个钩子如下。
| 钩子 | 时机 | 接收什么 | 阻止的话 |
|---|---|---|---|
pre-commit |
提交即将创建之前 | 无(直接查看索引) | 无法提交 |
commit-msg |
消息写完之后 | 消息文件路径($1) | 无法提交 |
pre-push |
push 之前 | 远程名称和地址($1、$2)+ 通过标准输入传入的引用列表 | 无法 push |
pre-commit 有一个陷阱:必须检查索引,而不是工作树。 被提交的是索引中的内容,而工作树在那之后可以随意变化。如果把机密写进文件并 git add,再把文件改干净,那么读取工作树的钩子什么也抓不到,机密照样被提交。索引的内容用 git show :<경로>(占位符为路径)或 git cat-file -p <blob> 读取。
pre-push 通过标准输入逐行接收 로컬참조 로컬해시 원격참조 원격해시(占位符依次为本地引用、本地哈希、远程引用、远程哈希)。用法是:只要有一行的 원격참조(即远程引用)是 refs/heads/main 就阻止。
客户端钩子挡不住的事
有三种情况。
第一,--no-verify。git commit --no-verify 会跳过 pre-commit 和 commit-msg,git push --no-verify 会跳过 pre-push。这不是缺陷,而是设计——客户端钩子是我在自己机器上开启的工具,紧急时必须能够关掉。
第二,没有打开配置的人。core.hooksPath 需要每个人各自打开。
第三,通过其他途径进来的提交。在网页界面上直接修改、别的工具推送进来,或者使用不支持钩子的客户端,钩子根本不会执行。
所以真正强制规则的地方是服务器:服务器端的 pre-receive、update 钩子,托管服务的分支保护,以及 CI 检查。在那些地方没有 --no-verify 这类东西。代价是反馈慢——要等到已经堆了提交并 push 之后才会被拒绝。
总结起来,同一条规则要挂在两个地方。客户端钩子是 30 秒内告诉你的一方,服务器是最终拦住的一方。 只挂在客户端,就会被绕过;只挂在服务器,大家每次都要 push 之后才知道。
在现场相遇的样子
- 把机密扫描器挂在
pre-commit上,大多数事故在提交之前就能被抓住。即便如此,也要另外准备好清理历史的流程——因为仍有能绕过的途径。 - 用
commit-msg统一标题格式,就可以从历史中自动生成发布说明。 - 钩子太慢的话,大家就会养成用
--no-verify的习惯。每次提交都要运行的检查,应当只看改动过的文件,并在 1 秒之内结束。 - 把钩子放进仓库时,务必确认执行权限(
chmod +x)。没有权限的话,git 会悄悄跳过——不报任何错误,规则就这样消失了。
下一项实验要做什么
用 core.hooksPath 把钩子放进仓库,亲手实现标题规则,以及大文件和机密的拦截。用只有索引中含有机密的状态,重现只看工作树的钩子为什么会被绕过,并用一个 --no-verify 让三个钩子全部放行。再 clone 一份,确认钩子文件会随之带过来,而配置不会;最后用 pre-push 阻止直接向 main push。