1. 从一次“手滑提交”说起:为什么你需要认识 Pre-commit 钩子
我从事后端开发这些年,最怕听到的一句话不是“线上挂了”,而是“我刚刚不小心提交了”。尤其是当你在一支多人协作的团队里,一次不经意的提交,可能把调试代码、临时日志、甚至带敏感信息的配置一起推到了远程仓库。更糟的是,等你在仓库里发现了问题,已经有人基于这个污染的分支开发了几天,回滚成本高到让人头痛。
Git Pre-commit 钩子就是专门为这类场景设计的。它属于 Git Hooks——一套在特定 Git 事件发生时自动触发的脚本机制。Pre-commit 是其中触发时机最早的一个:在你执行git commit命令、提交还没真正生成之前,Git 会先检查.git/hooks/pre-commit这个脚本。如果脚本以非零状态退出,整个提交就被拦截,相当于给代码质量装了一道“安检闸门”。
我可以直白地告诉你它能做什么:检查代码风格、运行单元测试、扫描敏感信息、校验提交信息格式、阻止超大文件入库。它不能做什么,你也要清楚:它不是 CI(持续集成)的替代品,不能保证代码逻辑正确,更不是万能的质量保险。它的定位很明确——在代码进入提交历史之前,做最后一道低成本、高效率的防线。
这篇文章适合谁?如果你是刚接触 Git 的新手,我会从钩子机制的原理讲起,你不用担心跟不上;如果你已经在团队里负责代码质量和工程效率,后面的框架化配置和团队协作策略应该能直接给你启发。无论你在哪个阶段,这套东西都能帮你在提交代码时少踩几个坑。
2. 钩子机制拆解:Git Hooks 到底是什么在背后工作
想真正掌控 pre-commit,先得理解它所在的整个体系。Git Hooks 是 Git 内建的事件通知机制,原理不复杂:当某类 Git 操作发生时,Git 会在特定路径下查找对应名称的可执行脚本,找到了就执行,然后根据执行结果决定是否继续原操作。这套机制的设计思路很像生活中的“门铃”——有人按门铃,主人决定是否开门。
2.1 钩子的存放位置与执行逻辑
每个 Git 仓库默认都带一个.git/hooks目录,里面有一堆以.sample结尾的示例文件。这些示例脚本里包含英文注释和可参考的 shell 代码,但没有被启用——只要你把文件名里的.sample去掉并赋予执行权限,钩子就激活了。
需要特别注意:.git/hooks目录位于仓库的内部结构里,不会被 Git 追踪,也不会跟随仓库推送。这意味着你在一台机器上配置的钩子,换一台机器克隆仓库后并不存在。好在你并不需要手动到.git目录里折腾——后面我们要讲的 pre-commit 框架,正是为了解决这个痛点而生的。
钩子的种类不止 pre-commit 一个,Git 总共内置了十几种钩子,分布在提交、合并、推送等各个阶段。以提交生命周期为例,执行git commit时,钩子按这种顺序被触发:
| 钩子名称 | 触发时机 | 作用 | 中断后果 |
|---|---|---|---|
| pre-commit | 提交前 | 检查暂存区内容,校验代码质量 | 阻止提交 |
| prepare-commit-msg | 生成提交信息前 | 填充默认提示、关联任务ID | 不影响提交 |
| commit-msg | 用户编辑完提交信息后 | 校验提交信息格式 | 阻止提交 |
| post-commit | 提交完成后 | 通知、记录、触发后续流程 | 不影响提交 |
2.2 pre-commit 在提交流程中的精确位置
我把一次标准提交拆开来看,你就能理解 pre-commit 的特殊之处。当你敲下git commit,Git 的提交过程分几个阶段:先检查是否有文件被暂存,然后调用 pre-commit 钩子,接着生成默认提交信息,调用 prepare-commit-msg 钩子,打开编辑器让你填写提交信息,再调用 commit-msg 钩子验证信息,最后创建提交对象,调用 post-commit 钩子。
pre-commit 是在提交对象生成之前运行的第一个钩子。它的输入是暂存区(index)里的内容,不是整个工作区。这个设计本身就是有深意的:它让你只对“即将进入提交历史”的内容做检查,而不是让你被迫处理工作区里那些还没准备好的修改。
它的执行机制对效率问题的解决也值得留意:如果暂存区里没有文件,pre-commit 不会运行;如果多个文件需要检查,脚本内部通常会做增量处理,只检查变更的部分。相比在 CI 上运行整个测试套件,这种“只查增量”的策略让它快得惊人,所以它才能心安理得地卡在你每次提交之前。
3. 为什么需要在提交前自动拦截:人肉检查的三大失效场景
你可能觉得,自己小心一点不就行了吗?我在没有引入 pre-commit 之前也这么想,但现实反复打脸。人肉检查在几种场景下注定失效,这不是自律问题,是认知资源的天然限制。
3.1 代码风格检查:最容易产生团队摩擦的环节
团队的代码风格约定通常写在文档里:缩进用空格还是 Tab,字符串用单引号还是双引号,行尾是否加逗号。文档写得再详细,人也不可能在每次写代码时逐条对照——你专注业务逻辑的时候,根本没精力在意行尾分号。于是风格问题只能靠 Code Review 时人肉发现。
Code Review 本身是件高成本的事。评审者要切换上下文,逐行阅读他人代码,寻找逻辑问题已经够费力了,还要花时间在“这里该加个空格”这种问题上,不仅浪费评审精力,还容易引发同事之间的摩擦。我用一个类比来说明:这就像你让校对员在审长篇小说时,还要顺带检查标点符号有没有用错——真正重要的剧情反而没精力细看。
pre-commit 把风格检查变成自动化:提交那一刻,工具按统一规则跑一遍,不合规的文件直接拦截并指出具体行号。开发者不用记规则,评审者不用盯格式,标准在机器层面强制执行。
3.2 敏感信息泄露:一次提交就可能追悔莫及
人肉检查最容易失效的是敏感信息泄漏。数据库密码、API Token、私钥、内部服务器地址,这些东西在本地配置里出现很正常。问题在于:你这次提交的代码里可能只包含改动的一部分,没意识到某个配置文件里夹带了生产环境的凭据。
Git 的机制让这个问题变得隐蔽而危险——只要一次提交进入历史,即使后面删除,它仍然存在于 commit 历史中。你推送到远程仓库之后,任何有仓库访问权限的人都能翻出这个凭据。密码轮换的成本高,泄露后的后果更严重。如果你的项目是开源的,那几乎是灾难级别的。
我在实际工作中还遇到过另一种情况:工单系统链接、内部域名、甚至是同事的私人邮箱,被无意中写进代码注释里提交到公开仓库。这种信息看起来不那么“敏感”,但对安全攻防演练来说,都是很好的情报来源。pre-commit 可以挂载敏感信息扫描工具,在提交前检查暂存区内容,发现疑似密钥、Token、私钥就直接拦截。机器不会累,也不会因为“赶时间”而放过一个可能出事的提交。
3.3 提交信息与分支规范:让历史变得可追溯
还有一类问题,单独看不致命,积累多了会让整个仓库的历史像一团乱麻。比如提交信息全是fix、update、aaa,或者干脆是默认的Merge remote-tracking branch。再比如有人把所有修改堆在main分支上开发,从来不开功能分支。
等到你需要排查线上问题、定位某个变更引入的行为时,这种乱象的代价就显现了。你对着一条fix bug的提交,根本不知道它改了什么,只能一行行翻 diff。
pre-commit 可以配合 commit-msg 钩子对提交信息做格式校验,强制团队遵循 Conventional Commits 这类规范:feat: 添加用户注册功能、fix: 修复登录态失效问题、refactor: 重构订单查询逻辑。这样,提交历史本身就变成了一份可读的变更日志。还可以在设计分支策略时配合钩子做分支名校验,让整个团队的工作流保持统一。
4. 从零手写一个 Pre-commit 钩子:核心逻辑与完整实现
理论讲清楚了,现在就动手。先从最基础的开始:不借助任何框架,自己写一个可用的 pre-commit 脚本。这能帮你建立对机制本身的理解,等后面接触框架时,你就知道每个环节在做什么、为什么能那么配置。
4.1 创建第一个可用的钩子脚本
先做最简验证。进入你的仓库,初始化钩子目录,然后写一个最平凡的脚本:
# 进入你的项目仓库 cd /path/to/your/project # 手动创建一个钩子脚本(如果 .git/hooks 下已有同名的示例文件,可以先看一下) cat > .git/hooks/pre-commit << 'EOF' #!/bin/sh echo "Pre-commit hook is running..." exit 0 EOF # 赋予执行权限 chmod +x .git/hooks/pre-commit # 测试一下 git add . git commit -m "test pre-commit hook"执行提交时,你应该看到Pre-commit hook is running...的输出,提交正常完成。这里的关键点在于exit 0和exit 1的区别:exit 0表示检查通过,交继续;exit 1表示检查失败,Git 立即中止提交,暂存区内容保持不变。
4.2 在钩子里读取暂存区内容
现在把钩子变得有用一点。核心问题是:钩子如何知道哪些文件被暂存了?答案是git diff系列命令。
#!/bin/sh # 列出本次提交暂存的所有文件,只保留 .js 后缀的文件 staged_js_files=$(git diff --cached --name-only --diff-filter=ACM | grep "\.js$") if [ -z "$staged_js_files" ]; then echo "No staged JavaScript files. Skipping checks." exit 0 fi # 对每个 JS 文件检查是否包含 console.log(这是最常见的调试残留) for file in $staged_js_files; do if grep -n "console\.log" "$file" > /dev/null 2>&1; then echo "错误:$file 中包含 console.log,请先移除调试代码" grep -n "console\.log" "$file" exit 1 fi done echo "JavaScript 文件检查通过" exit 0git diff --cached是关键:它列出的是“暂存区与 HEAD 之间的差异”,只覆盖你马上要提交的内容。--diff-filter=ACM的意思是只处理新增(Added)、已修改(Modified)、已复制(Copied)的文件,跳过已删除的文件(Deleted),因为删除的文件没有内容可检查。
你注意到脚本里有个细节:grep -n "console\.log" "$file"中的反斜杠转义。console.log 里的点号在正则里匹配任意字符,所以加了转义让它匹配字面量点号,避免误伤consoleXlog之类的伪命中。这种小细节在真实场景里会让钩子少很多误报。
4.3 编写钩子时的方法论:失败策略与白名单设计
从使用者的角度想一下:如果钩子总是误报,你会怎么反应?大概率是气得执行git commit --no-verify强行绕过。所以钩子设计的第一原则是:能不误报就不误报,宁可漏报也不能让开发者觉得它在“找茬”。
具体来说,我总结了几个实用原则:
- 白名单优先:默认只检查约定范围内的文件类型,而不是对所有文件做全量扫描。比如
.js、.ts、.py各自对应不同的检查规则。 - 清楚指示修改位置:任何拦截信息都要包含文件名和行号,最好是定位到具体内容。让开发者能立刻知道改哪里。
- 提供明确的逃逸通道:有时候你确实需要临时跳过检查。Git 给你提供了
--no-verify参数,但日志要保留下来,在钩子里打印一行警告,提醒开发者“你跳过了检查”,让他心里有数。 - 优雅处理不可用依赖:脚本依赖某个工具时,先检查它是否存在于环境中。如果工具缺失,可以提示是安装还是跳过,避免脚本直接报错阻塞整个提交。
我写过一个钩子的完整流程,大体上分四步:读取暂存文件列表、过滤出应由本钩子负责的文件类型、对每个文件执行体检、汇总结果并给出明确的成功或失败指示。这种结构无论检查规则怎么换,骨架都够用。
5. 迈向工程化:用 pre-commit 框架统一管理和分发钩子
自己手写钩子,问题很快会出现:你在这个仓库写了规则,另一个仓库需要复制;同事克隆项目时,钩子又不见了。每个人都把钩子放在自己的.git/hooks里,规则就永远无法统一。
工程化的解法是引入 pre-commit 框架——严格说,这是一个特定工具,名字就叫pre-commit,由 Python 生态里的 asottile 开发维护。它的核心思路是:把钩子配置写进仓库根目录的.pre-commit-config.yaml文件,这个文件跟随仓库一起提交和分发。任何人克隆仓库后,只需执行一条命令,框架就会按配置文件下载并安装钩子到本地。
5.1 安装与初始化配置
安装框架本身很直接,看你所在的环境:
# macOS 用户 brew install pre-commit # 使用 pip(需要有 Python 环境) pip install pre-commit # 使用 Homebrew 也支持 brew install pre-commit # 安装后验证 pre-commit --version然后在你项目的根目录创建一个.pre-commit-config.yaml文件。一个最小可用配置长这样:
repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files每段钩子配置都声明了一个远程代码仓库,rev固定为某个版本号,hooks下列出要启用的具体检查项。配置完成后执行:
# 安装钩子到 .git/hooks pre-commit install # 如果想在安装前先手工跑一次,看看会不会误报 pre-commit run --all-filespre-commit install做了一件很精妙的事:它会把框架自己的脚本安装为.git/hooks/pre-commit。之后每次git commit,实际执行的是框架,再由框架读取你的 YAML 配置,按顺序运行每个检查工具。框架还内置了缓存机制,相同工具第二次运行时直接从缓存加载,速度提升明显。
5.2 常用钩子配置解析:从格式检查到安全扫描
我整理了一份从个人项目到团队级项目都适用的钩子组合,按检查维度分类:
| 检查类型 | 钩子 ID | 来源仓库 | 说明 |
|---|---|---|---|
| 格式基础 | trailing-whitespace | pre-commit-hooks | 清除行尾空格 |
| 格式基础 | end-of-file-fixer | pre-commit-hooks | 确保文件以换行符结尾 |
| 格式基础 | check-yaml | pre-commit-hooks | 校验 YAML 文件语法 |
| 代码质量 | eslint | 独立仓库配置 | JavaScript/TypeScript 静态检查 |
| 代码质量 | ruff | 独立仓库配置 | Python 代码检查与格式化 |
| 安全扫描 | detect-secrets | yelp/detect-secrets | 检测密钥和敏感信息 |
| 安全扫描 | gitleaks | gitleaks/gitleaks | 检测硬编码密码与密钥 |
| 提交规范 | commitizen | commitizen-tools | 交互式生成符合规范的信息 |
| 大文件 | check-added-large-files | pre-commit-hooks | 阻止提交超过阈值的大文件 |
配置上用additional_dependencies给某个钩子加装依赖条件,用exclude排除特定路径(比如vendor/或生成的静态文件)。一个多仓库多层级的完整配置骨架:
repos: # 第一层:通用基础检查 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files args: ['--maxkb=512'] # 第二层:语言特化的代码检查 - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-eslint rev: v9.2.0 hooks: - id: eslint additional_dependencies: - eslint@9.2.0 - typescript - typescript-eslint # 第三层:安全与敏感信息检查 - repo: https://github.com/gitleaks/gitleaks rev: v8.18.4 hooks: - id: gitleaks5.3 配置背后的设计逻辑:为什么每个钩子都要独立成仓库
你可能会疑惑:为什么 pre-commit 框架不把所有检查工具打包在一起,而是要求每个钩子声明独立的远程仓库?原因在于依赖隔离和更新策略。
每个钩子工具都有自己的依赖环境,比如 eslint 需要 Node.js,ruff 需要 Python 特定解释器。如果全部打包在一起,版本冲突几乎是必然的。独立仓库让每个钩子在自己的“沙盒”里运行,互不干扰。版本号用rev明确锁定,保证团队所有人用完全相同的工具版本,避免“我本地没问题,你那里报错”的版本漂移问题。
这和 Docker 容器化的思路很相似:每个服务独立封装、锁定版本、按需启动。pre-commit 框架本质上就是一个钩子的编排引擎,它只负责拉取、调度、报告结果,具体的检查逻辑全部交给独立的仓库实现。
6. 团队落地与规模化策略:让钩子成为工程文化的基石
个人项目用 pre-commit 很简单,难的是让它在团队里真正起作用。如果只是配置文件放上去,很多人会直接--no-verify绕过,等于没装。我见过不少团队在引入自动化检查后,代码质量并没有大幅改善,问题就出在没有配套的流程设计。
6.1 渐进式引入:先并行观察,避免一刀切
最忌讳的做法是把所有钩子一次性打开,立刻开始拦截所有人的提交。结果必然是:老代码大面积误报、有人被逼着改一堆历史问题、怨声载道,然后钩子被集体绕过。
我的建议是分三步走:
- 第一步,并行运行:先把配置放进仓库,镜子模式跑一轮,让人工和钩子同时检查,人工评审时对比钩子报告,观察误报率。这个阶段钩子不拦截任何提交。
- 第二步,小范围试点:先在一个子团队、或仅对新增文件生效的范围内开启拦截。让先导部队跑一两周,收集反馈,调整规则和排除项。
- 第三步,全面启用:确认误报少、速度可接受后,再全员开启。这个阶段才真正把钩子写进团队的工作流规范里。
渐进式引入的核心思想是:让开发者先看到钩子的价值,而不是先感受到它带来的束缚。这种体验管理对于工程效率工具的落地非常重要。
6.2 配合 CI 做双保险:本地钩子不是终点
pre-commit 在本地运行,天然有一个漏洞:开发者可以用--no-verify跳过。这不是道德问题,而是流程设计上必须考虑的边界。所以我的原则是:本地钩子负责“快”和“省”,CI 负责“强制”和“兜底”。
具体落地方式是在 CI 流水线里加一个独立的 pre-commit 检查步骤(很多托管平台和自建 CI 都支持):
# 以 GitHub Actions 为例的 CI 配置片段 jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - uses: pre-commit/action@v3.0.1这个 CI 任务在代码推到远端后、合并到主分支前运行。本地没查出来的问题,到 CI 这里会被拦截。两套机制一快一稳,互相兜底。
另一个坑是“本地和 CI 环境不一致”导致的误报。比如 Python 项目,本地是 3.12 版本,CI 是 3.8 版本,某个语法在本地合法但 CI 上报错。这种情况下,尽量让钩子固定在统一运行的容器里,或者在 CI 和本地都使用相同的运行时版本。我在实操中会选择把 local hooks 也统一在 CI 上跑,确保一致性。
6.3 处理高频摩擦点:大文件、权限与绕过
团队落地过程中,有几个高频摩擦点值得提前应对:
第一个是大文件拦截。check-added-large-files默认阈值是 500KB,对游戏项目、机器学习模型仓库来说显然不合适。这类项目的二进制资产体积大,直接一刀切会导致误报。我的方案是把大文件检查的重点放在“临时添加的构建产物”上,同时对模型权重、报表等明确需要入库的大文件在配置里显式排除。
第二个是工具链差异。Windows 开发和 macOS 开发环境对 shell 脚本的支持不一致。pre-commit 框架用 Python 实现,天然跨平台,所以团队统一用框架而不是自制 shell 脚本,是更省心的选择。
第三个是“凭什么我不能提交”的情绪问题。解决办法是钩子在拦截信息里写清楚原因和修改建议。一条好的拦截消息应该像这样:
check-added-large-files........................................................Failed - hook id: check-added-large-files - exit code: 1 dist/app.bundle.js (1.2MB) is larger than 512KB它告诉开发者具体是哪个文件超了、超了多少、阈值是多少。人面对清晰的反馈更容易接受,而不会觉得工具在刁难人。
7. 常见问题与排查技巧实录:我踩过的那些坑
工具用久了,总会遇到各种莫名其妙的现象。这里把我实际踩过的问题整理成速查手册,按“现象 - 原因 - 解决”的结构记录下来,省得你再走一遍弯路。
7.1 钩子配置了却不生效
最常见的原因不是配置写错,而是压根没有跑pre-commit install。框架安装钩子用的是这个命令,但它只更新.git/hooks/pre-commit文件,而这个文件不进版本库。如果你换了新电脑、重新克隆了仓库,就必须重新执行一次pre-commit install。团队里建议把它写进 README 或项目脚手架脚本,或者加到依赖安装流程的后置命令里。
另一个隐蔽原因是项目里已经有一个旧的.git/hooks/pre-commit。我之前遇到过:旧脚本是个自制的 shell 脚本,没有执行权限,新装的 pre-commit 框架试图覆盖时没成功,导致两边都没生效。排查方法是执行cat .git/hooks/pre-commit看看当前安装的到底是什么内容。
7.2 跑得很慢:每个文件都要重新下载环境
第一次运行 pre-commit 时,框架会到对应仓库下载钩子代码并构建运行环境,这个过程可能很慢。有人以为卡死了,直接 Ctrl+C。正确的做法是让它跑完——后续运行都在本地缓存里,速度会快很多。还可以用PRE_COMMIT_HOME环境变量指定一个全局共享的缓存目录,让多个项目复用同一套钩子环境。
检查速度慢的另一个来源是某些钩子是无差别全量扫描,不遵循“只查暂存区”的原则。比如 gitleaks 这种安全扫描器,对全历史扫描确实慢。优化思路是让它在 git 历史层操作(它本来就该如此),而在 CI 上做全量扫描时,接受这个成本。
7.3 Git 相关高频问题解答:从 merge 到 commit --amend
这里我综合整理 Git 使用中提到频率最高的一批问题,单独做个解答。这些问题和 pre-commit 钩子配合使用时,也常常一并出现:
| 问题 | 原因与解法 |
|---|---|
git merge冲突了怎么办 | 冲突标记手改,git add后git merge --continue;pre-commit 钩子会再次检查合并结果 |
git commit --amend想改上次提交的信息 | git commit --amend -m "新的信息";注意 amend 会触发 pre-commit 和 commit-msg 钩子 |
checkout分支后代码变来变去 | 切换分支时 Git 会自动把工作区内容换成目标分支的版本;注意未提交的改动会被带过去 |
git revert和git reset有什么区别 | revert 生成一个反向提交,适合推送到共享分支;reset 直接移动 HEAD,适合本地未推送的修改 |
| 提交时提示 “CRLF will be replaced by LF” | 是行尾符转换提示,用git config core.autocrlf设置统一策略 |
| 某次提交后想找回删掉的代码 | git reflog找到历史 commit hash,git cherry-pick或git checkout恢复 |
git clone失败提示连接问题 | 检查网络环境和代理配置;仓库地址是否正确,认证方式是否有效 |
这些答案结合钩子的场景来看,尤其要注意git commit --amend。很多人以为 amend 只是“修改标题”,不会触发检查,但实际上 pre-commit 钩子和 commit-msg 钩子会照常运行。如果你之前用--no-verify跳过了检查,amend 时会突然报错,别慌——这是正常行为,它只是把该做的检查补上。
7.4 钩子误报怎么办:动态排除与白名单策略
最后一个高频问题:钩子太严格了,误杀了合法内容。比如.env.example文件里包含占位的假密码,被安全扫描器误报。处理方式是在配置里加exclude规则:
- id: detect-secrets exclude: ^\.env\.example$但更精细的方案是给某一行加“豁免注释”。以 detect-secrets 为例,它支持用pragma: allowlist secret的形式把某一行标记为可接受的占位值。这种“代码行级豁免”比路径排除更精确,也更容易 Code Review 时审查。
我认为这个问题不能只当技术问题处理。培训团队时,要反复强调:钩子的存在是保护所有人的共同资产。如果它误报了,正确的处理方式是反馈给维护者调整规则,而不是一怒之下--no-verify绕过。每次绕过都在削弱整支队伍的工程防线。
8. 我的一些实操经验与扩展建议
最后说说我个人的体会。
引入 pre-commit 钩子最值的投入,不是配置那几十行 YAML,而是后续的规则维护和团队适应期管理。我花了一个多月时间,才把我们团队的钩子规则打磨到“几乎零误报、速度接受、大家不问为什么”的状态。这个过程里我得到的最深刻的教训是:任何自动化质量工具,本质上都是“约束”,而好的约束设计,应该让人感受到的是效率,而不是控制。
有几个具体的经验值得你借鉴。第一,钩子配置里每个参数都要写注释说明为什么。两个月后你自己回看都不知道exclude: ^docs/是为什么加的,更别提新同事。第二,对异常情况保持零容忍。如果钩子有一天意外通过了明显违规的文件,不要觉得“终于可以放松了”,恰恰相反,这说明配置有问题,要立刻排查。防线的作用,恰恰体现在它保持沉默的时刻。第三,别把所有的检查都塞进 pre-commit。它适合快速轻量级检查,跑完整套单元测试、构建二进制这种重活,应该交给 CI 去异步执行,否则开发者的每一次提交都变成煎熬。
扩展方面,你可以研究commitlint来做提交信息的格式校验,把 Conventional Commits 规范彻底执行起来;可以给git push阶段再加一个pre-push钩子,在推送前跑一遍完整的测试套件;还能把 pre-commit 集成到 Git 的 GUI 客户端里,让不熟悉命令行的同事也能享受到同样的保护。
说到底,工具只是起点。真正让代码质量稳定下来的,是最初那一刻你对“提交”这件事的敬畏心——而 pre-commit,就是帮你守住这份敬畏心的一道自动闸门。我至今记得第一次被自己的钩子拦住那次提交:报错信息里写的文件,正是我那天赶工时留下的临时调试代码。那一刻我意识到,这道闸门拦下的不是我的进度,而是别人被迫处理我烂摊子的时间。