Skip to content

服务端钩子

服务端钩子在远程仓库服务器上执行,用于实施团队级别的策略,比客户端钩子更难绕过(不能用 --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
done

update:分支更新检查

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 0

post-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-receive

pre-receive vs update 的区别

对比项pre-receiveupdate
执行次数每次推送执行一次每个引用执行一次
用途整体推送的校验针对特定引用的校验
输入方式stdin 读取所有引用命令行参数
适合场景全局策略针对不同分支的策略

总结

钩子触发时机可以拒绝常见用途
pre-receive接收推送前全局策略检查
update每个引用更新前分支级别策略
post-receive推送接收后触发 CI/CD、通知

服务端钩子是强制执行团队规范的最后防线,不能被单个开发者绕过(除非服务器管理员)。结合合理的钩子策略,可以显著提升代码质量和流程合规性。