Appearance
服务端钩子
服务端钩子在远程仓库服务器上执行,用于实施团队级别的策略,比客户端钩子更难绕过(不能用 --no-verify)。
pre-receive:接收前检查
当服务端接收到推送时,pre-receive 是第一个执行的钩子。若返回非零退出码,整个推送被拒绝。
bash
#!/bin/sh
# hooks/pre-receive(服务端)
# 通过 stdin 读取推送信息
while read old_sha new_sha ref_name; do
# 阻止强制推送到主分支
if [[ "$ref_name" == refs/heads/main ]] || [[ "$ref_name" == refs/heads/master ]]; then
# 检查是否是强制推送(old_sha 不是 new_sha 的祖先)
if ! git merge-base --is-ancestor "$old_sha" "$new_sha" 2>/dev/null; then
echo "❌ 错误:禁止对 $ref_name 执行强制推送!"
exit 1
fi
fi
done
exit 0高级检查:大文件检测
bash
#!/bin/sh
# 检查推送中是否包含超大文件(>10MB)
MAX_SIZE=$((10 * 1024 * 1024)) # 10MB
while read old_sha new_sha ref_name; do
# 获取新推送的所有 blob 对象
git diff --raw "$old_sha" "$new_sha" | while read mode_old mode_new sha_old sha_new status path; do
if [ -n "$sha_new" ] && [ "$sha_new" != "0000000000000000000000000000000000000000" ]; then
SIZE=$(git cat-file -s "$sha_new")
if [ "$SIZE" -gt "$MAX_SIZE" ]; then
echo "❌ 错误:文件 '$path' 大小为 $((SIZE/1024/1024))MB,超过限制(10MB)"
echo " 请使用 Git LFS 管理大文件"
exit 1
fi
fi
done
doneupdate:分支更新检查
update 钩子针对每个被更新的引用单独执行,可以对不同分支设置不同策略:
bash
#!/bin/sh
# hooks/update(服务端)
# 参数:$1=引用名,$2=旧 SHA-1,$3=新 SHA-1
REF_NAME="$1"
OLD_SHA="$2"
NEW_SHA="$3"
# 强制推送检查(更细粒度的控制)
case "$REF_NAME" in
refs/heads/main|refs/heads/master|refs/heads/develop)
# 保护分支,检查是否是 fast-forward
if ! git merge-base --is-ancestor "$OLD_SHA" "$NEW_SHA" 2>/dev/null; then
echo "❌ 不允许对受保护分支 $REF_NAME 进行强制推送"
exit 1
fi
;;
refs/heads/release/*)
# release 分支只允许特定用户推送
PUSHER=$(git log --format="%ae" -1 "$NEW_SHA")
echo "release 分支推送者:$PUSHER"
;;
esac
# 检查提交信息规范
while IFS= read -r commit; do
MSG=$(git log --format="%s" -1 "$commit")
if ! echo "$MSG" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)"; then
echo "❌ 提交 $commit 的信息不符合 Conventional Commits 规范:"
echo " $MSG"
exit 1
fi
done < <(git rev-list "$OLD_SHA".."$NEW_SHA")
exit 0post-receive:接收后触发(CI/CD)
推送成功接收后执行,常用于触发 CI/CD、发送通知等。退出码不影响推送结果。
bash
#!/bin/sh
# hooks/post-receive(服务端)
while read old_sha new_sha ref_name; do
BRANCH=$(echo "$ref_name" | sed 's|refs/heads/||')
echo "接收到推送:分支 $BRANCH"
case "$BRANCH" in
main)
echo "触发生产环境部署..."
# 触发 CI/CD 系统
curl -X POST "https://ci.example.com/deploy" \
-H "Content-Type: application/json" \
-d "{\"branch\":\"main\",\"commit\":\"$new_sha\"}"
;;
develop)
echo "触发测试环境部署..."
# 更新测试环境
cd /path/to/staging
git pull origin develop
npm install
pm2 restart app
;;
esac
# 发送 Slack/钉钉通知
COMMIT_MSG=$(git log --format="%s" -1 "$new_sha")
AUTHOR=$(git log --format="%an" -1 "$new_sha")
# curl -X POST "$WEBHOOK_URL" -d "..."
done
exit 0服务端钩子的配置
在 GitLab/GitHub 等平台: 大多数平台不允许直接设置服务端 Git 钩子,而是提供了替代方案:
- GitHub:分支保护规则(Branch Protection Rules)、GitHub Actions
- GitLab:服务端钩子(管理员权限)、CI/CD Pipeline
- Gitea:通过 Web 界面配置 Git 钩子
自建 Git 服务器:
bash
# 将钩子脚本放在裸仓库的 hooks/ 目录
/path/to/repo.git/hooks/pre-receive
/path/to/repo.git/hooks/update
/path/to/repo.git/hooks/post-receive
# 赋予执行权限
chmod +x /path/to/repo.git/hooks/pre-receivepre-receive vs update 的区别
| 对比项 | pre-receive | update |
|---|---|---|
| 执行次数 | 每次推送执行一次 | 每个引用执行一次 |
| 用途 | 整体推送的校验 | 针对特定引用的校验 |
| 输入方式 | stdin 读取所有引用 | 命令行参数 |
| 适合场景 | 全局策略 | 针对不同分支的策略 |
总结
| 钩子 | 触发时机 | 可以拒绝 | 常见用途 |
|---|---|---|---|
pre-receive | 接收推送前 | ✅ | 全局策略检查 |
update | 每个引用更新前 | ✅ | 分支级别策略 |
post-receive | 推送接收后 | ❌ | 触发 CI/CD、通知 |
服务端钩子是强制执行团队规范的最后防线,不能被单个开发者绕过(除非服务器管理员)。结合合理的钩子策略,可以显著提升代码质量和流程合规性。