采用 pre-commit 的原因,并不在于 Git hook 这个东西本身有多新。很多团队早就把脚本塞进 .git/hooks 里用了很多年。真正的差别在于,大多数这类脚本都停留在私人地带,跟着机器漂移,仓库里一旦牵涉到多种语言或多种工具,就会很快散掉。pre-commit 把这种松散习惯改写成仓库内部的契约:hook 来源、钉住的修订号、文件过滤规则、执行阶段,全都写进一份可以评审的 .pre-commit-config.yaml,不再藏在口耳相传的环境说明里。[1]

这个变化比名字本身更重要。一次像样的 pre-commit 迁移,重心并不落在“原来没有检查,现在检查更多”这一层,真正变化的是约束从手册纪律进入版本化策略。项目根目录成了代码卫生的正门:行尾空白、YAML 校验、Ruff、密钥扫描、生成文件整理,以及团队希望在评审前先跑一遍的其他工具,都会作为仓库本身的依赖关系显露出来。[1][2][4]

图像说明:题图采用的是一个沉浸式工作台现场,避开 logo、截图、图表或说明图。这更贴近本文讨论的场景,因为 pre-commit 最强的那一层,始终发生在普通开发者的桌面循环里,文件从编辑器出来,先经过本地校验,再变成 pull request。

真正需要迁移的,是从零散脚本进入可评审的 hook 清单

在 pre-commit 的采用过程中,最关键的文件落在 .pre-commit-config.yaml.git/hooks 里的那段脚本退到后面。[1] 官方文档写得很清楚:这份配置描述了应该克隆哪些仓库、每个仓库的 rev 应该固定在哪个版本、以及要运行其中哪个 hook id。[1] 这件事单看像是语法问题,和常见替代方案放在一起,分量就完全不同了。零散模式里,Black 有一套装法,ESLint 有另一套装法,密钥扫描也许只存在于 CI,某位维护者电脑里还留着一段早就没人看得懂的 shell 片段。到了 pre-commit 这里,仓库本身会把该跑什么直接说出来。

这种可评审性,才是治理层面最值钱的部分。rev 被钉住之后,hook 版本不再是机器事实,而会变成代码评审的一部分。[1][4] 格式化器升级、一条新的禁用文件规则、更窄的 files 正则,这些变化都会作为可见 diff 落下来。连例外路径也是明写的。配置可以用 types 缩小适用面,用 types_or 放宽匹配,补 additional_dependencies,也可以把 hook 移到不同 stages 里。[1] 这就是 pre-commit 比自定义 Git-hook 脚本更耐用的原因。它给的是一套策略结构,不只是一个塞命令的地方。

官方 hooks 目录把这个判断又往前推了一步。它现在仍然把 pre-commit-hooks 放在很显眼的位置,那是一组刻意保持朴素的通用检查,例如 check-yamlend-of-file-fixertrailing-whitespace。[2] 这其实给出了一条很实用的迁移线索。稳妥的采用通常从这些枯燥但稳定的文件卫生规则起步,语言层面的 linter 与 formatter 往后接。很多团队一上来就堆五个慢工具,却没有卫生底线,最后往往只剩争论,没有一致性。

最深的一层价值,在于跨语言工具的隔离安装不再变成机器表演

pre-commit 最强的地方,并不只是“提交前跑一下工具”。更深的价值在于,它把这些工具如何到达本地这件事一起标准化了。[1] 官方文档明确写到,这个框架支持许多语言写成的 hooks,不要求 root 权限,开发者即便没有装 Node,只要改动了需要 JavaScript hook 的文件,pre-commit 也会自己把对应运行时拉下来并构建好。[1] 这件事放在真实仓库里,分量远比第一眼看上去更重。

很多仓库正是在这一层失去控制。一个 Python 服务慢慢长出一点前端目录,再加一点 Terraform、一些 shell 脚本和 Markdown 校验,最后“把检查跑起来”就意味着六套包管理器和一页维基说明。pre-commit 给出的立场更窄,也更实用:每个 hook 都有自己的运行环境,第一次运行会慢一些,之后就复用已经装好的环境。[1][4] 换来的结果是,仓库可以描述一条跨语言的质量边界,而不用把“如何把边界装出来”这件事重新摊给每一位贡献者。

也正因为这样,即便一个团队已经有很强的 CI,pre-commit 仍然值得保留。CI 负责证明分支在共享基础设施上是干净的。pre-commit 负责把反馈缩短到开发者自己的笔记本上。这是两种不同的工作。它最适合抓住格式抖动、明显的密钥泄露、无效配置文件这一类问题,在它们变成远端失败之前就把它们拦下来,同时又让 CI 用 pre-commit run --all-files 把同一份配置在全树范围内再跑一遍。[1][4]

它的执行模型是“暂存文件纪律”,再接上一段有意识的 CI 重跑

pre-commit 的一个非常好的设计,是它默认把本地运行范围压在开发者眼前那一小片改动里。官方文档说明,普通提交时的 pre-commit run 面向当前暂存文件,而 pre-commit run --all-files 则更适合做全仓库基线清理,或者放进 CI 里跑。[1] 由此,本地成本就被控制住了。一个人只改了一份 YAML,就不需要在每次提交时都把整个 monorepo 扫一遍。

很多迁移在这里停住了,这会让整套东西显得比它实际更脆。更成熟的做法,是把本地针对暂存文件的执行当作快闸口,再把同一份配置交给 CI,对全树或者平台关心的 diff 窗口重跑一次。[1][3][4] 标题里的“接力”说的就是这个关系。本地 hook 先把明显摩擦降下来;CI 再把同一套规则放到仓库尺度上重申一遍,覆盖那些没有安装本地 hook 的贡献者、机器人账号,以及跨分支合并之后才显出来的组合问题。

阶段系统让这段接力关系更清楚。default_install_hook_types、每个 hook 自己的 stages,再加上特殊的 manual 阶段,使得同一份配置文件可以同时容纳轻量的 commit-time 检查、较重的 pre-push 检查,以及那些应该存在于仓库里、但没有必要每次自动触发的工具。[1] SKIP 环境变量也很说明问题。pre-commit 允许你跳过某一个 hook,避免把整个闸口用 --no-verify 一脚踢开。[1] 这是一条现实边界,不能理解成偷懒许可。团队仍然要解释为什么跳过,但框架至少给的是手术刀,脱离铁锤式的粗暴处理。

把 hook 清单当作依赖库存来维护,整件事才会变轻

很多团队低估的一层,是后续维护。hook 框架最容易坏掉的方式,就是第一次接好了,之后很久没人再碰那些钉住的修订号。pre-commit 在这一点上其实想得很明白。它直接提供 pre-commit autoupdate,还能用 frozen 模式把修订号改写成精确提交;pre-commit.ci 则把同一逻辑继续往前推,负责自动修复和定期更新 PR。[1][3] 这等于承认了一件事实:hook 维护本身就是一类持续性的依赖管理问题。

这也是 pre-commit.ci 之所以不只是一个锦上添花工具的原因。它的配置仍然写在同一份 .pre-commit-config.yaml 里,通过 ci: 段落表达自动修复和自动更新策略,不需要额外发明第二套配置表面。[3] 服务本身可以不用,结构上的启发却很强。pre-commit 在最舒服的时候,本地 hook 行为、CI 行为、升级行为都会回到同一份仓库所有的清单里。

最合适的使用场景,大致可以压成三点:

  1. 你希望把代码质量策略留在仓库内部、留在评审里,避免散在每台机器和各种口头说明里。[1][4]
  2. 你的仓库横跨不止一种工具链或语言,不愿意让运行时安装变成贡献门槛。[1]
  3. 你愿意在 CI 里把同一套 hook 再跑一遍,不把本地方便错认成共享约束。[1][3]

它不那么合适的边界也很清楚。如果一个团队真正想要的是一套巨大的自定义任务系统,如果检查严重依赖庞大的项目状态,已经把 commit-time 反馈拖得非常难受,或者如果团队想让本地 hook 直接替代所有服务端保护,pre-commit 就不会显得舒服。它更可靠的那条路仍然很窄,也正因为窄而可靠:把 hook 策略版本化,把第一道闸口贴近编辑器,再让 CI 以仓库尺度重申同一份契约。[1][3][4]

来源

  1. pre-commit 官方文档:框架概览、.pre-commit-config.yaml 结构、钉住的 rev、针对暂存文件的执行方式、hook 阶段、SKIPinstall --install-hooksautoupdate 行为。
  2. pre-commit hooks 目录:官方列出的 pre-commit-hooks 与其他 hook 仓库,其中包括 check-yamlend-of-file-fixertrailing-whitespace 这类通用卫生检查。
  3. pre-commit.ci:可选的 ci: 配置、PR 自动修复行为,以及基于同一份 .pre-commit-config.yaml 的定期自动更新支持。
  4. Real Python 的 pre-commit 工具条目:从独立角度总结钉住版本、隔离 hook 环境、针对暂存文件的运行方式与 --all-files 用法。
  5. pre-commit/pre-commit GitHub 仓库:框架本身的权威项目主页与发布位置。
  6. pre-commit/pre-commit-hooks GitHub 仓库:迁移中常用的那组语言无关基础 hooks 的权威来源。