必须用pre-receive,因其在所有分支推送前统一校验,能一次性拦截非法提交;update按分支单独触发且仅获单ref信息,易漏检多分支推送中的问题commit。

服务端钩子该用 pre-receive 还是 update?
必须用 pre-receive,不能用 update。前者在所有分支推送前统一校验,能一次性拦截非法提交;后者按分支单独触发,如果一次推送多个分支,update 可能漏检——比如某分支的提交不含任务单号,但因其他分支合法而被放行。
更关键的是:update 只拿到单个 ref 的新旧 commit ID,无法遍历该 ref 新增的所有 commit;而 pre-receive 能读取 stdin 里的全部推送记录,配合 git rev-list <old>..<new></new></old> 才能逐条检查每个新增 commit 的 message。
怎么从 commit message 提取并验证任务单号?
别依赖正则硬匹配「JIRA-123」这种格式——实际中常有变体:开头带空格、含括号、混用短横/下划线、大小写不敏感,甚至写在 message body 里。稳妥做法是:
- 用
git log --format=%B <commit></commit>获取完整 message(含 subject + body) - 用
grep -i -o -E '[A-Z]{2,}-[0-9]+' | head -n 1提取首个疑似任务号(避免误匹配 URL 或邮箱) - 再调用公司任务系统 API 或本地白名单文件校验该编号是否存在——仅靠正则匹配会放行伪造的编号
示例片段(bash):
while read oldrev newrev refname; do<br> if [[ "$refname" =~ ^refs/heads/ ]]; then<br> range="$oldrev..$newrev"<br> git rev-list --format=%H $range 2>/dev/null | grep '^commit ' | cut -d' ' -f2 | while read commit; do<br> msg=$(git log -n1 --format=%B $commit)<br> ticket=$(echo "$msg" | grep -i -o -E '[A-Z]{2,}-[0-9]+' | head -n1)<br> if [[ -z "$ticket" ]] || ! check_ticket_exists "$ticket"; then<br> echo "ERROR: commit $commit missing valid ticket number"<br> exit 1<br> fi<br> done<br> fi<br>done
钩子报错后用户看到的提示为什么总不清晰?
Git 服务端钩子输出到 stderr 的内容,会被 Git 客户端原样显示——但很多人在钩子里只写 echo "invalid",没加换行或说明上下文,导致用户看到类似:
remote: invalid<br>remote: error: failed to push some refs
根本不知道哪条 commit、哪个分支出问题。
改进方式:
- 每条错误前加
echo "remote: [ERROR] ..."(Git 会识别remote:前缀并高亮) - 把违规 commit 的 short hash 和对应 message subject 一起输出:
git log -n1 --format="%h %s" $commit - 最后补一句可操作指引,比如:
remote: Please include a valid ticket like 'PROJ-456' in your commit message.
为什么本地开发时测试钩子总失败,但线上又正常?
最常见原因是环境变量缺失:pre-receive 钩子运行在 bare repo 下,PATH 往往极简(可能不含 curl、jq 或 Python),且没加载用户 shell 配置。直接写 python3 check.py 很可能报 command not found。
解决方法:
- 所有外部命令用绝对路径,如
/usr/bin/python3、/usr/bin/curl - 钩子开头显式设置 PATH:
export PATH="/usr/bin:/bin:/usr/local/bin" - 避免依赖未预装的工具——比如用 bash 内置 regex 替代
grep -P,或把 Python 脚本打包成可执行二进制(如 PyInstaller)
另外注意:bare repo 默认不带工作区,git config 读取的是仓库级配置,不是用户全局配置——若钩子依赖某些 Git 设置(如 core.autocrlf),得显式传参或提前在 bare repo 中配置好。


















