TT Lab
开始
学习 学习路径 课程

Git 实战

马上告诉你的钩子和最终拦住你的钩子,不在同一个地方

在 TT Lab 中继续学习

一句话总结

客户端钩子(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 之后才知道。

在现场相遇的样子

下一项实验要做什么

用 core.hooksPath 把钩子放进仓库,亲手实现标题规则,以及大文件和机密的拦截。用只有索引中含有机密的状态,重现只看工作树的钩子为什么会被绕过,并用一个 --no-verify 让三个钩子全部放行。再 clone 一份,确认钩子文件会随之带过来,而配置不会;最后用 pre-push 阻止直接向 main push。

官方文档是 githooks、git-config、Pro Git 8.3 Git Hooks。