git hook 什么时候触发、顺序如何、退出码会怎样影响命令?这篇给仓库装一组只记录自己被调用的 hook,逐个动作观察,覆盖客户端提交、合并、变基、推送,以及服务端的 pre-receive、update、post-receive。
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)。
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] yhook 需要可执行位,否则被忽略(只给 hint);clone 不复制 .git/hooks,要共享用 core.hooksPath 指向仓库里或共享目录。