5.4 Git Hook 钩子


5.4 Git Hook 钩子

本节摘要:Git Hook 是挂在 Git 事件上的自动化脚本:提交前跑检查、推送前拦强推、接收推送后触发部署。本节先讲 Hook 的位置、激活方式与退出码约定,再分类讲透客户端钩子与服务器端钩子,现场写一个 pre-commit 质量闸门脚本,最后解决"怎么让全团队共用钩子"的协作问题。

上手前先明确

阅读完本节,你应当能够:

  1. 说清 Hook 的本质、存放位置、为什么 .git/hooks 不被版本控制。
  2. 理解退出码约定:0 通过,非 0 中止操作。
  3. 区分客户端钩子(pre-commit、commit-msg、pre-push 等)与服务器端钩子(pre-receive、update、post-receive)的触发时机与用途。
  4. 手写一个 pre-commit 脚本做代码风格检查,并正确激活它。
  5. 用 core.hooksPath 把钩子脚本纳入版本控制,实现团队共享。

一、问题与直觉

你的团队提交的代码风格五花八门:有人忘了格式化,有人提交信息写得像"更新",有人把打印语句留在代码里。靠人工 review 拦截?几十个提交根本盯不过来,而且"风格问题"卡住合并又显得吹毛求疵。你想的是:这些检查能不能让工具自动做,连提交的机会都不给不合规代码?

再想一层:你推了代码到远程,希望服务器自动跑测试、自动部署。靠人每次手动触发?漏一次就上线了坏版本。你希望的是:代码一到远程,检查就自动开始

这两个需求,Git Hook 都能满足。Hook(钩子)是 Git 在执行关键动作前后预留的"挂钩点":钩子上挂了可执行脚本,动作发生时就自动调用。它把"人记得做"变成"系统强制做",把"事后补救"变成"事前拦截"。

类比工厂质检线:产品流经每个工位,工位上都有传感器(Hook)——一个工位检查外观(提交格式),一个工位做功能测试(单元测试),一个工位做包装(部署)。只要任何传感器报警,产品就停在原地,不进下一个流程。Git Hook 就是把质检线装进 Git 操作本身。

二、核心原理

Hook 长什么样,藏在哪

每个仓库的 .git/hooks 目录里都躺着十几个示例脚本,以 .sample 结尾:

.git/hooks/ ├── pre-commit.sample ├── commit-msg.sample ├── pre-push.sample ├── pre-receive.sample ├── update.sample ├── post-receive.sample └── ...

激活一个钩子只需两步:把文件名去掉 .sample 后缀,再给它可执行权限(Linux/macOS 用 chmod +x)。示例脚本本身就是注释详尽的模板,直接改就能用。

关键事实:.git/hooks 目录不被版本控制。你克隆一个仓库,不会自动拿到它的钩子——钩子不是仓库内容的一部分。这既是设计(钩子可能依赖本机环境),也是团队协作的痛点(怎么让大家都装上同样的钩子),后文 5.4.3 专门解决。

退出码:钩子的"生死开关"

钩子脚本的退出码决定操作是否继续:

  • 退出码 0:检查通过,操作继续。
  • 非零退出码:操作被中止,Git 报错退出。

这个约定是 Hook 全部权力的来源。pre-commit 检查到违规,返回 1,提交就被拦下;pre-receive 检查到推送不合规,返回 1,整个推送被拒。写得再好的检查,如果忘了在违规时返回非零,就等于没写——这是写钩子最常犯的低级错误。

客户端钩子:把守本地操作

钩子 触发时机 典型用途
pre-commit commit 生成提交对象之前 跑 linter、格式化检查、单测
prepare-commit-msg 默认提交消息生成后、编辑器打开前 按分支名自动注入 issue 编号
commit-msg 编辑完提交消息后、提交定稿前 校验提交消息格式
post-commit 提交完成后 发通知、记录日志
pre-rebase rebase 开始前 拦阻断不该变基的分支
post-rewrite amend/rebase 等改写完成后 同步 issue 状态、通知
pre-push push 发送引用之前 推前跑测试、拦强推历史改写
post-merge merge 成功后 安装新依赖、跑测试

前四个(pre-commit、prepare-commit-msg、commit-msg、post-commit)构成"提交四连",覆盖了提交生命周期的每个节点。pre-push 是把关"最后出门"的闸门。

图 5-4 客户端钩子的触发时间线

图 5-4 客户端钩子的触发时间线

服务器端钩子:把守团队大门

钩子 触发时机 典型用途
pre-receive 服务器收到推送、引用更新前 全局策略:拒大文件、拦强推 main、校验提交信息
update 对每个被推送的引用各执行一次 精确到分支/标签的细粒度控制
post-receive 服务器更新完所有引用后 触发 CI 构建、自动部署、发通知

pre-receive 是服务器端最强的一道闸——它能基于推送内容、推送者、引用名拒绝整个推送,是团队策略的"最后防线";update 更细,可以单独允许或拒绝某个分支的更新;post-receive 不做拦截,只做事后动作(部署、通知)。服务器端钩子的好处是没法被绕过——客户端钩子用户可以 git commit --no-verify 跳过,服务器端钩子在远端执行,想绕只能不推送。

平台的分支保护:服务器端钩子的"托管版"

用 GitHub/GitLab 这类托管平台时,你通常没有直接写 pre-receive 的权限——平台把这类能力封装成了"分支保护规则":禁止直接推 main、要求 PR 通过后才允许合并、要求提交信息含特定前缀、要求有 CI 通过标记等。理解钩子原理后,再看平台这些设置就一目了然:它们本质上是平台托管的 pre-receive 钩子,用图形界面代替了手写脚本。自建 Git 服务器(如 GitLab 自托管)则既可以用分支保护,也可以直接写 pre-receive。两种方式思路一致,只是封装程度不同。

三、工程实践要点

手写一个 pre-commit 质量闸门

场景:提交前检查暂存区里所有 .txt 文件是否含 "TODO" 字样,有就拦下。完整脚本:

#!/bin/sh # 找出暂存区中所有 .txt 文件 TXT_FILES=$(git diff --cached --name-only -- '*.txt') if [ -z "$TXT_FILES" ]; then exit 0 fi # 逐个检查是否含 TODO for FILE in $TXT_FILES; do if grep -q "TODO" "$FILE"; then echo "错误: 文件 $FILE 包含 TODO,请处理后再提交。" exit 1 fi done exit 0

激活三步:把脚本存成 .git/hooks/pre-commit、去掉后缀、chmod +x 赋可执行权限。之后每次 commit,脚本自动跑——有人提交含 TODO 的文件,提交当场被拦。把 grep -q "TODO" 换成 eslint 或测试命令,就是一个团队级质量闸门。

第二个钩子:commit-msg 提交信息格式闸

提交信息质量是团队协作的隐形成本——"update"这种消息三个月后没人看得懂。用 commit-msg 钩子强制格式:

#!/bin/sh # 强制提交信息第一行以 类型冒号 开头,如 feat: 添加登录 MSG=$(head -1 "$1") echo "$MSG" | grep -E "^(feat|fix|docs|refactor|chore):" >/dev/null if [ $? -ne 0 ]; then echo "提交信息必须以 feat 冒号 或 fix 冒号 等开头" exit 1 fi exit 0

这个钩子拿到 Git 传入的提交信息临时文件路径(脚本里的 $1),检查第一行是否符合规范。它比 pre-commit 更"软性"但同样有效——格式问题在提交源头就被拦截,而不是等 review 时被人在 PR 里反复留言"消息规范点"。

写钩子的调试套路

钩子"不生效"或"误杀"时,按顺序排查:

  1. 激活了吗:文件名是否去掉 .sample?可执行权限是否给了?ls -l 看一眼。
  2. 路径对了吗:脚本是否存在 .git/hooks 下?Windows 上 CRLF 换行会坑 sh 解释器,用 LF 保存。
  3. 退出码对吗:把脚本里所有出口分支检查一遍,违规路径是不是都 return 非零了。
  4. 直接跑一遍sh .git/hooks/pre-commit 手动执行,看输出和退出码,排错一半问题出在这里。
  5. 看 Git 怎么说:提交失败时 Git 会把钩子的 stdout/stderr 原样打出来——那行红字就是你的脚本在说话。

⚠️ 常见坑:Windows 上写钩子脚本最容易栽在换行符上——脚本文件如果保存成 CRLF 换行,sh 解释器可能报错。用 LF 换行保存,或第一行明确 #!/bin/sh。同时记得检查"有没有文件可执行"这个前置条件,很多"钩子不生效"其实是权限问题。

怎么让全团队共用钩子

钩子不被版本控制的特性,带来一个协作难题。三种主流解法:

方案 做法 优点 缺点
手动复制 文档指导大家拷贝到各自 .git/hooks 零依赖 靠自觉,容易漏装
core.hooksPath 把钩子放进仓库内目录(如 scripts/hooks),git config core.hooksPath 指向它 钩子随仓库版本控制,克隆即得 需要每台机器执行一次配置命令
Hook 管理工具 用 husky(JavaScript)、pre-commit 框架(Python)等 自动安装、多语言、配置共享 引入第三方依赖

core.hooksPath 是官方推荐方案:Git 2.9 起支持,让 Git 从一个"受版本控制"的目录读钩子,而不是固定读 .git/hooks。团队克隆仓库后只需跑一条配置命令,钩子就位。

客户端钩子能被绕过,别指望它强制

git commit --no-verify 可以跳过 pre-commit、commit-msg 等客户端钩子——这是设计的逃生门,但也被想偷懒的人利用。所以真正强制的策略必须放服务器端(pre-receive/update),客户端钩子是"给自觉的人提效率",服务器端钩子才是"给不自觉的人上保险"。

💡 关键直觉:把钩子想成"流程的哨兵"。客户端哨兵守在每个人家门口(防个人手滑),服务器端哨兵守在团队大门(防集体失控)。哨兵要站在合适的位置,才知道该拦什么。

常见问题与 FAQ

"钩子能用什么语言写?" 任何可执行脚本都行——sh、bash、Python、Ruby 都常见。Git 只要求"是可执行文件 + 遵守退出码约定",不限定语言。小检查用 sh,复杂的用 Python 更可读。

"写坏了一个钩子,会不会把仓库弄坏?" 不会。钩子失败只会中止对应操作(提交、推送),仓库数据和历史不受影响。而且钩子在 .git/hooks 里,删掉文件就等于禁用。

"--no-verify 能跳过所有钩子吗?" 能跳过客户端钩子(pre-commit、commit-msg、pre-push 等),跳不过服务器端钩子——服务器端在远端执行,本地命令管不着。所以强制策略放服务器端才可靠。

"钩子会影响性能吗?" 看脚本复杂度。pre-commit 跑全量 linter 在大仓库可能要几秒,觉得烦就把检查范围缩小到"本次变更的文件"(用 git diff 筛选),而不是全库扫描。

"pre-push 能阻止强推吗?" 能检测但拦不彻底。pre-push 在本地执行,可以检查本次推送是否有非快进更新并中止它——但用户仍能绕过。真正防强推的是服务器端策略(pre-receive 检查引用更新,或平台的分支保护规则)。本地钩子是"提醒",远端钩子是"执法"。

"钩子脚本里的命令找不到怎么办?" 钩子继承的执行环境可能不含你的 PATH(尤其是 GUI 客户端触发的提交)。脚本里尽量写命令的完整路径,或第一行用 #!/bin/sh 并由脚本自身设置 PATH。这也是 Windows 上钩子常"神秘失败"的根源之一。

"pre-commit 里能跑 git 命令吗?" 能,但要小心——钩子运行时仓库处于中间状态。比如 pre-commit 里跑 git status 是安全的,但跑 git commit 会造成递归调用,跑 git rebase 会跟正在进行的提交抢锁。经验法则是:钩子里的 git 命令只读、不改,写入动作留给 Git 主流程去完成。

"服务器端钩子在哪配置?" 在托管仓库的那台服务器上配置,对应仓库的 hooks 目录。GitHub/GitLab 这类托管平台通常不开放直接写 pre-receive,而是提供受管版本(如 GitHub Actions 的 branch protection)——这是平台的封装,思路同源。

本章回顾

  • Hook 的本质:Git 关键动作上挂的可执行脚本,自动化执行检查或后处理。
  • 位置与激活:.git/hooks 下去掉 .sample 后缀 + 赋可执行权限,两步启用。
  • 退出码铁律:0 通过、非零中止;忘了返回非零等于没写检查。
  • 客户端钩子:pre-commit、commit-msg、pre-push 等,管个人本地流程。
  • 服务器端钩子:pre-receive、update、post-receive,管团队大门,不可绕过。
  • 团队共享:core.hooksPath 把钩子纳入版本控制,是官方推荐方案。
  • 绕过与强制:客户端钩子可 --no-verify 跳过,强制策略必须放服务器端。
  • 性能与语言:缩小检查范围省时间,sh/Python 都能写,CRLF 换行是 Windows 常见坑。

下一节给高频命令起"捷径名"——Git 别名,把又长又难记的命令变成顺手的关键词。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U