bigywater.tech
Git · 67

hooks:触发时机、顺序与退出码的影响

git hook 什么时候触发、顺序如何、退出码会怎样影响命令?这篇给仓库装一组只记录自己被调用的 hook,逐个动作观察,覆盖客户端提交、合并、变基、推送,以及服务端的 pre-receive、update、post-receive。

4 个
git commit 依次触发的 hook:pre-commit、prepare-commit-msg、commit-msg、post-commit
--no-verify
只跳过 pre-commit 和 commit-msg,prepare-commit-msg 与 post-commit 仍会运行
update 每个 ref 一次
服务端 hook 里 pre-receive 与 post-receive 各一次,update 对每个 ref 都运行

提交

实测 · 本机执行输出
  pre-commit
  prepare-commit-msg .git/COMMIT_EDITMSG message
  commit-msg .git/COMMIT_EDITMSG
  post-commit
  (prepare-commit-msg 的参数:消息文件 + 来源 message;commit-msg 的参数:消息文件;post-commit 没有参数)

commit 触发 pre-commit(无参数)→ prepare-commit-msg(消息文件 + 来源)→ commit-msg(消息文件)→ post-commit。前三个在提交完成前,后者在提交之后。

退出码

实测 · 本机执行输出
  pre-commit 退出 1     → git commit 退出码 1,提交数 1(原来 1);触发的 hook: pre-commit
  commit-msg 退出 1     → git commit 退出码 1,提交数 1(原来 1);触发的 hook: pre-commit → prepare-commit-msg → commit-msg
  post-commit 退出 1    → git commit 退出码 0,提交数 2(原来 1);触发的 hook: pre-commit → prepare-commit-msg → commit-msg → post-commit
  pre-commit 与 commit-msg 都退出 1,但 --no-verify:退出码 0,提交数 2;触发的 hook: prepare-commit-msg → post-commit(prepare-commit-msg 和 post-commit 不受 --no-verify 影响)

pre-commit 和 commit-msg 非 0 会中止提交,post-commit 的退出码被忽略(提交已经存在)。--no-verify 跳过前两者,但不跳过 prepare-commit-msg 与 post-commit。

改写说明

实测 · 本机执行输出
  $ git log -1 --format=%B | head -3
  原始说明
  [审核通过]

commit-msg 收到消息文件的路径,改写它就改写了最终的提交说明(例如追加 Change-Id)。

其它客户端 hook

实测 · 本机执行输出
  git checkout side:
    post-checkout <sha40> <sha40> 1
  git merge side(非快进,产生合并提交):
    pre-merge-commit
    prepare-commit-msg .git/MERGE_MSG merge
    commit-msg .git/MERGE_MSG
    post-merge 0
  git rebase main(topic 上有 1 个提交要搬):
    pre-rebase main
    post-checkout <sha40> <sha40> 1
    prepare-commit-msg .git/COMMIT_EDITMSG message
    post-commit
    post-rewrite rebase
        stdin: <sha40> <sha40>
  (post-checkout 的三个参数:旧 HEAD、新 HEAD、1=切换分支 / 0=检出文件;post-rewrite 的 stdin 是“旧 sha 新 sha”)

checkout 触发 post-checkout(旧 HEAD、新 HEAD、分支标志);非快进合并触发 pre-merge-commit、prepare-commit-msg、commit-msg、post-merge;rebase 触发 pre-rebase、对每个重放的提交触发 prepare-commit-msg 和 post-commit、最后 post-rewrite(stdin 是“旧 新”对应)。

推送

实测 · 本机执行输出
  pre-push origin /work/l63r.git
      stdin: refs/heads/main <sha40> refs/heads/main <sha40>
  pre-push 退出 1:git push 退出码 1,远程 main 仍是旧的: 1 个提交

pre-push 收到远程名与 URL,stdin 每行 本地ref 本地sha 远程ref 远程sha;非 0 阻止推送。

服务端

实测 · 本机执行输出
  pre-receive
      stdin: <sha40> <sha40> refs/heads/main
      stdin: <sha40> <sha40> refs/heads/topic
  update refs/heads/main <sha40> <sha40>
  update refs/heads/topic <sha40> <sha40>
  post-receive
      stdin: <sha40> <sha40> refs/heads/main
      stdin: <sha40> <sha40> refs/heads/topic
  post-update refs/heads/main refs/heads/topic
  (update 对每个被更新的 ref 各运行一次,参数是 “ref 旧值 新值”;pre-receive / post-receive 只运行一次,stdin 每行 “旧值 新值 ref”;新建分支的旧值是全 0)
  update 非 0 只拒绝那个 ref;pre-receive 非 0 则整个推送被拒绝:
  update 对 topic 退出 1 的推送结果:
    remote: error: hook declined to update refs/heads/topic
       <sha7>..<sha7>  main -> main
     ! [remote rejected] topic -> topic (hook declined)
  远程 main 是否更新: 2 个提交;远程 topic: 1 个提交(topic 没更新)
  pre-receive 退出 1:
     ! [remote rejected] main -> main (pre-receive hook declined)

顺序是 pre-receive(一次,可拒绝整个推送)→ update(每个 ref 一次,可拒绝单个)→ post-receive → post-update。

坑

实测 · 本机执行输出
  去掉可执行位的 pre-commit:
    hint: The '.git/hooks/pre-commit' hook was ignored because it's not set as executable.
    hint: You can disable this warning with `git config set advice.ignoredHook false`.
    [main 8748eb4] x
     1 file changed, 1 insertion(+), 1 deletion(-)
  clone 出来的仓库 .git/hooks 里有 pre-commit 吗: False(clone 不复制 hook;里面只有 *.sample 示例文件:14 个)
  设置 core.hooksPath 指向一个共享目录后: 来自 core.hooksPath 的 pre-commit | [main 602b4c5] y

hook 需要可执行位,否则被忽略(只给 hint);clone 不复制 .git/hooks,要共享用 core.hooksPath 指向仓库里或共享目录。

分步演示

hooks
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。所有 hook 都是同一个小脚本,把自己的名字、参数(有 stdin 的再加 stdin)追加到日志;服务端是本机的裸仓库;输出里的 hash 与路径做了替换
    • 没有验证:pre-auto-gc、post-index-change、reference-transaction、push-to-checkout、fsmonitor-watchman 等其它 hook;applypatch-msg、pre-applypatch 等 git am 相关 hook;hook 的环境变量(GIT_DIR 等);Windows 上 hook 的行为;pre-commit 里对暂存区而非工作区的检查技巧
    京ICP备2026067502号-1京公网安备11010502063487号