14、Git钩子机制:客户端钩子与服务端钩子实战
Git钩子这东西,说白了就是一套“触发器”。你在Git仓库里做某些操作时,比如提交代码、推送分支,Git会自动去检查某个脚本,然后决定让不让你继续。我刚开始接触Git钩子时,觉得它挺神秘的,后来发现其实就是一堆放在特定目录下的脚本文件。
嗯,今天我们就来聊聊客户端钩子和服务端钩子。我会结合我自己的踩坑经历,把pre-commit、commit-msg、pre-receive、post-receive这几个最常用的钩子讲透。
什么是Git钩子?
每个Git仓库下都有一个.git/hooks目录。你进去看看,里面有一堆.sample结尾的示例文件。这些就是钩子的模板。去掉.sample后缀,改成可执行文件,钩子就生效了。
钩子分两类:
- 客户端钩子:在你本地仓库触发,比如提交前、提交后、推送前。
- 服务端钩子:在远程仓库触发,比如收到推送前、收到推送后。
我个人习惯把客户端钩子用来做“质量门禁”,服务端钩子用来做“流程管控”。两者配合,基本能杜绝大部分低级错误。
核心思路:客户端钩子帮你拦住“不该提交的代码”,服务端钩子帮你拦住“不该合并的分支”。
客户端钩子实战
pre-commit:提交前的最后一道防线
pre-commit 钩子在输入提交信息之前触发。说白了,它是你代码质量的“守门员”。
我在项目中遇到过最头疼的事:同事提交了带调试日志的代码,结果CI跑了一小时才发现。后来我强制团队在pre-commit里加了检查,问题就再没出现过。
下面是一个实用的pre-commit脚本示例:
#!/bin/bash
# pre-commit 钩子:检查代码风格、禁止调试代码、禁止大文件
echo "🔍 正在运行 pre-commit 检查..."
# 1. 检查是否包含调试代码
if git diff --cached --name-only | xargs grep -l "console\.log\|debugger\|TODO\|FIXME" 2>/dev/null; then
echo "❌ 错误:提交中包含调试代码或待办事项!"
echo " 请移除 console.log / debugger / TODO / FIXME 后再提交。"
exit 1
fi
# 2. 检查是否包含大文件(超过5MB)
for file in $(git diff --cached --name-only); do
if [ -f "$file" ]; then
size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
if [ "$size" -gt 5242880 ]; then
echo "❌ 错误:文件 $file 超过 5MB!"
echo " 请使用 Git LFS 管理大文件。"
exit 1
fi
fi
done
# 3. 运行代码格式化(以Prettier为例)
if command -v prettier &> /dev/null; then
echo "🎨 正在格式化代码..."
git diff --cached --name-only --diff-filter=ACM | \
grep -E '\.(js|ts|jsx|tsx|json|css|md)$' | \
xargs prettier --write 2>/dev/null
git add -u
fi
echo "✅ pre-commit 检查通过!"
exit 0
小技巧:pre-commit 脚本里如果修改了文件,记得用 git add -u 重新暂存。不然你格式化完代码,提交的还是旧版本。这个坑我踩过不止一次。
commit-msg:规范你的提交信息
commit-msg 钩子在用户输入提交信息之后、提交完成之前触发。它接收一个参数:存放提交信息的临时文件路径。
为什么要规范提交信息?你想想看,项目上线后出问题了,你翻git log看到一堆“fix bug”、“update”、“aaa”……你根本不知道哪个提交改了什么东西。我曾经在一个遗留项目里翻了两小时log,最后放弃了,直接去问老同事。
下面是一个强制使用 Conventional Commits 规范的 commit-msg 钩子:
#!/bin/bash
# commit-msg 钩子:校验提交信息格式
commit_msg_file="$1"
commit_msg=$(cat "$commit_msg_file")
# 定义正则:type(scope): description
pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\([a-zA-Z0-9_-]+\))?: .{1,72}$"
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "❌ 提交信息格式不正确!"
echo ""
echo "正确格式示例:"
echo " feat(login): 添加登录验证码功能"
echo " fix(api): 修复用户列表分页错误"
echo " docs(readme): 更新安装说明"
echo ""
echo "允许的类型:feat, fix, docs, style, refactor, perf, test, chore, ci"
exit 1
fi
# 检查提交信息长度
if [ ${#commit_msg} -gt 100 ]; then
echo "❌ 提交信息过长(最大100字符),请精简描述。"
exit 1
fi
echo "✅ 提交信息格式正确!"
exit 0
注意:commit-msg 钩子只检查格式,不检查内容是否合理。我曾经见过有人写 fix: 修复了一个bug,格式完全正确,但等于没写。所以团队规范里还得加上“描述要具体”这条软性要求。
服务端钩子实战
pre-receive:推送前的“安检门”
pre-receive 钩子在服务端收到推送数据后、更新引用之前触发。它从标准输入读取一行行的数据,格式是:旧值 新值 引用名。
这个钩子特别适合做“分支保护”和“提交规范校验”。比如禁止直接推送到master分支,或者禁止包含敏感信息的提交。
下面是一个实用的 pre-receive 脚本:
#!/bin/bash
# pre-receive 钩子:保护主分支、禁止强制推送、检查提交信息
# 读取推送的引用信息
while read oldrev newrev refname; do
# 获取分支名
branch=${refname#refs/heads/}
# 1. 保护主分支:只允许合并请求
if [ "$branch" = "master" ] || [ "$branch" = "main" ]; then
echo "❌ 错误:禁止直接推送到 $branch 分支!"
echo " 请通过合并请求(Merge Request)提交代码。"
exit 1
fi
# 2. 禁止强制推送(非快进推送)
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
# 检查是否是快进推送
merge_base=$(git merge-base "$oldrev" "$newrev")
if [ "$merge_base" != "$oldrev" ]; then
echo "❌ 错误:禁止强制推送(非快进推送)!"
echo " 请使用 git pull --rebase 更新本地分支后再推送。"
exit 1
fi
fi
# 3. 检查新提交的提交信息格式
for commit in $(git rev-list "$oldrev..$newrev" 2>/dev/null); do
commit_msg=$(git log --format=%s -1 "$commit")
if ! echo "$commit_msg" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\(.*\))?: .+"; then
echo "❌ 错误:提交 $commit 的提交信息格式不规范!"
echo " 提交信息:$commit_msg"
echo " 请使用 Conventional Commits 规范。"
exit 1
fi
done
done
echo "✅ pre-receive 检查通过!"
exit 0
关键点:pre-receive 钩子返回非零退出码时,整个推送会被拒绝。这意味着你可以在服务端做“一票否决”。
post-receive:推送后的“自动化引擎”
post-receive 钩子在服务端更新引用之后触发。它同样从标准输入读取数据,但此时推送已经成功了。所以它适合做“通知”和“自动化部署”之类的事情。
我在项目中用 post-receive 做过这些事情:
- 自动部署到测试环境
- 发送企业微信/钉钉通知
- 更新Jira任务状态
- 触发CI/CD流水线
下面是一个简单的 post-receive 脚本:
#!/bin/bash
# post-receive 钩子:部署测试环境 + 发送通知
while read oldrev newrev refname; do
branch=${refname#refs/heads/}
# 只处理 master 分支的推送
if [ "$branch" = "master" ] || [ "$branch" = "main" ]; then
echo "🚀 检测到 $branch 分支更新,开始部署..."
# 切换到项目目录并拉取最新代码
cd /var/www/project
git pull origin "$branch"
# 执行部署脚本
if [ -f "deploy.sh" ]; then
bash deploy.sh
echo "✅ 部署完成!"
else
echo "⚠️ 未找到 deploy.sh 脚本,跳过部署。"
fi
# 发送通知(示例:写入日志)
echo "$(date): $branch 分支已更新,新提交: $newrev" >> /var/log/git-hooks.log
fi
done
exit 0
建议:post-receive 里不要做太重的操作。如果部署需要很长时间,可以考虑用消息队列异步处理。不然推送的人要等很久才能看到结果。
钩子的管理与分发
钩子脚本默认在 .git/hooks 目录下,但这个目录不会被Git跟踪。所以团队里怎么共享钩子?
我推荐两种方式:
| 方式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 项目内存储 | 在仓库根目录建 hooks/ 文件夹,存放脚本 |
版本控制、团队共享 | 需要手动复制到 .git/hooks |
| 使用工具 | 如 husky(Node.js)、pre-commit(Python) | 自动安装、跨平台 | 需要额外依赖 |
我个人更推荐用工具。比如前端项目用 husky,配合 lint-staged,体验非常好。你只需要在 package.json 里配置一下,团队其他人 npm install 后钩子就自动装好了。
避坑指南:我曾经在Windows和Mac混用的团队里吃过亏。Shell脚本在Windows上跑不起来。后来统一用Node.js写钩子,或者用跨平台的工具,才解决了这个问题。
总结
Git钩子机制,说白了就是“在关键节点插入自定义逻辑”。客户端钩子帮你管好自己,服务端钩子帮你管好团队。
几个要点再强调一下:
- pre-commit 适合做代码质量检查,越早发现问题越好
- commit-msg 适合规范提交信息,让git log变得可读
- pre-receive 适合做分支保护和强制规范,是“最后一道防线”
- post-receive 适合做自动化和通知,别放太重逻辑
- 钩子脚本要团队共享,用工具管理比手动复制靠谱
嗯,Git钩子就聊到这里。记住一点:钩子不是银弹,它只是帮你减少人为失误的工具。真正重要的是团队达成共识,然后让钩子去执行这些共识。