Git 服务器搭建完全指南 / 第 8 章 - 服务端钩子
第 8 章 - 服务端钩子
Git 钩子(Hooks)是在特定事件发生时自动执行的脚本。服务端钩子运行在 Git 服务器上,可用于代码质量检查、自动部署、通知推送等自动化工作流。
8.1 钩子类型总览
8.1.1 服务端钩子
| 钩子 | 触发时机 | 用途 | 可阻止推送 |
|---|---|---|---|
pre-receive | 接收推送前,最先执行 | 全局策略检查 | ✅ |
update | 每个分支更新前 | 分支级检查 | ✅ |
post-receive | 推送完成后 | 通知、部署、日志 | ❌ |
8.1.2 执行顺序
客户端 git push
│
▼
pre-receive ← 所有引用的总体检查(一次执行)
│
▼
update ← 每个被更新的分支各执行一次
│
▼
post-receive ← 推送完成后的操作(一次执行)
8.1.3 客户端钩子(参考)
| 钩子 | 触发时机 |
|---|---|
pre-commit | git commit 前 |
commit-msg | 编辑完提交信息后 |
pre-push | git push 前 |
8.2 pre-receive 钩子
pre-receive 在服务器接收推送数据前执行,用于全局策略检查。任何非零退出码都会拒绝整个推送。
8.2.1 输入格式
通过标准输入接收三元组(每行一个):
<旧引用SHA> <新引用SHA> <引用名>
示例:
0000000000000000000000000000000000000000 abc1234567890 refs/heads/feature/new-login
abc1234567890 def9876543210 refs/heads/main
def9876543210 0000000000000000000000000000000000000000 refs/heads/old-branch
8.2.2 禁止强制推送
#!/bin/bash
# pre-receive: 禁止对 main 分支强制推送
protected_branch="refs/heads/main"
while read oldrev newrev refname; do
if [ "$refname" = "$protected_branch" ]; then
# 检查是否为非快进推送
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
# 检查 oldrev 是否是 newrev 的祖先
if ! git merge-base --is-ancestor "$oldrev" "$newrev" 2>/dev/null; then
echo "ERROR: 非快进推送到 $protected_branch 被禁止"
echo "请使用 'git pull --rebase' 合并最新代码后再推送"
exit 1
fi
fi
fi
done
exit 0
8.2.3 提交信息规范检查
#!/bin/bash
# pre-receive: 检查提交信息格式
while read oldrev newrev refname; do
# 跳过删除操作
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
continue
fi
# 跳过新分支(从零开始)
if [ "$oldrev" = "0000000000000000000000000000000000000000" ]; then
oldrev=$(git rev-parse "$newrev^" 2>/dev/null || echo "4b825dc642cb6eb9a060e54bf899d15f4f20c54f")
fi
# 遍历所有新的提交
git rev-list "$oldrev".."$newrev" 2>/dev/null | while read commit; do
msg=$(git log --format=%B -n 1 "$commit")
subject=$(echo "$msg" | head -1)
# 检查格式: type(scope): description
if ! echo "$subject" | grep -qE '^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\(.+\))?: .{1,72}$'; then
echo "ERROR: 提交信息格式错误"
echo " Commit: $commit"
echo " Subject: $subject"
echo ""
echo " 期望格式: type(scope): description"
echo " 类型: feat|fix|docs|style|refactor|test|chore|perf|ci|build"
echo " 示例: feat(auth): add OAuth2 login support"
exit 1
fi
done
[ $? -ne 0 ] && exit 1
done
exit 0
8.2.4 文件大小和类型检查
#!/bin/bash
# pre-receive: 检查大文件和敏感文件
MAX_FILE_SIZE=$((50 * 1024 * 1024)) # 50MB
BLOCKED_EXTENSIONS="exe,dll,so,dylib,jar,war,ear,zip,tar,gz,rar,7z"
SENSITIVE_PATTERNS="(\.env|\.pem|\.key|\.p12|\.pfx|id_rsa|id_ed25519)"
while read oldrev newrev refname; do
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
continue
fi
if [ "$oldrev" = "0000000000000000000000000000000000000000" ]; then
oldrev="4b825dc642cb6eb9a060e54bf899d15f4f20c54f"
fi
# 获取变更的文件
git diff --name-status "$oldrev".."$newrev" | while read status filepath; do
# 检查敏感文件
if echo "$filepath" | grep -qEi "$SENSITIVE_PATTERNS"; then
echo "ERROR: 检测到敏感文件: $filepath"
echo "请将敏感文件添加到 .gitignore"
exit 1
fi
# 检查文件扩展名
ext="${filepath##*.}"
if echo "$BLOCKED_EXTENSIONS" | grep -qw "$ext"; then
echo "ERROR: 禁止推送二进制文件: $filepath (.$ext)"
exit 1
fi
# 检查文件大小(仅新增和修改的文件)
if [ "$status" = "A" ] || [ "$status" = "M" ]; then
size=$(git cat-file -s "$newrev":"$filepath" 2>/dev/null || echo 0)
if [ "$size" -gt "$MAX_FILE_SIZE" ]; then
echo "ERROR: 文件过大: $filepath ($((size / 1024 / 1024))MB > 50MB)"
echo "请使用 Git LFS 管理大文件"
exit 1
fi
fi
done
[ $? -ne 0 ] && exit 1
done
exit 0
8.3 update 钩子
update 钩子在每个被更新的分支上分别执行,参数通过命令行传入。
8.3.1 参数说明
#!/bin/bash
# update <refname> <oldrev> <newrev>
refname="$1"
oldrev="$2"
newrev="$3"
echo "Branch: $refname"
echo "Old: $oldrev"
echo "New: $newrev"
8.3.2 分支命名规范检查
#!/bin/bash
# update: 检查分支命名规范
refname="$1"
oldrev="$2"
newrev="$3"
# 跳过标签
[[ "$refname" == refs/tags/* ]] && exit 0
# 分支命名规范: feature/*, bugfix/*, hotfix/*, release/*, dev, main
valid_patterns=(
"^refs/heads/main$"
"^refs/heads/dev$"
"^refs/heads/develop$"
"^refs/heads/feature/[a-z0-9._-]+$"
"^refs/heads/bugfix/[a-z0-9._-]+$"
"^refs/heads/hotfix/[a-z0-9._-]+$"
"^refs/heads/release/[0-9]+\.[0-9]+$"
)
valid=false
for pattern in "${valid_patterns[@]}"; do
if [[ "$refname" =~ $pattern ]]; then
valid=true
break
fi
done
if [ "$valid" = false ]; then
echo "ERROR: 分支名不符合规范: $refname"
echo ""
echo "允许的分支命名模式:"
echo " main"
echo " dev / develop"
echo " feature/<name>"
echo " bugfix/<name>"
echo " hotfix/<name>"
echo " release/<version>"
echo ""
echo "示例: feature/user-login, bugfix/fix-crash, release/2.1"
exit 1
fi
exit 0
8.4 post-receive 钩子
post-receive 在推送完成后执行,用于通知、自动部署、日志记录等。由于推送已完成,退出码不影响推送结果。
8.4.1 自动部署
#!/bin/bash
# post-receive: 推送到 main 分支时自动部署
DEPLOY_DIR="/var/www/app"
DEPLOY_BRANCH="refs/heads/main"
LOG_FILE="/var/log/git-deploy.log"
while read oldrev newrev refname; do
if [ "$refname" = "$DEPLOY_BRANCH" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - Deploy triggered by push to main" >> "$LOG_FILE"
# 导出代码到部署目录
GIT_WORK_TREE="$DEPLOY_DIR" git checkout -f main >> "$LOG_FILE" 2>&1
# 执行部署脚本
if [ -x "$DEPLOY_DIR/scripts/deploy.sh" ]; then
cd "$DEPLOY_DIR"
./scripts/deploy.sh >> "$LOG_FILE" 2>&1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') - Deploy completed" >> "$LOG_FILE"
# 通知(可选)
echo "✅ 已自动部署 main 分支到 $DEPLOY_DIR"
fi
done
8.4.2 Webhook 通知
#!/bin/bash
# post-receive: 发送 Webhook 通知
WEBHOOK_URL="https://hooks.example.com/git-push"
BOT_NAME="Git Bot"
while read oldrev newrev refname; do
branch="${refname#refs/heads/}"
# 获取提交信息
if [ "$newrev" != "0000000000000000000000000000000000000000" ]; then
author=$(git log -1 --format='%an' "$newrev")
message=$(git log -1 --format='%s' "$newrev")
commit_count=$(git rev-list "$oldrev".."$newrev" --count 2>/dev/null || echo "?")
else
author="system"
message="Branch deleted"
commit_count=0
fi
# 发送通知到企业微信/钉钉/Slack
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"msgtype\": \"markdown\",
\"markdown\": {
\"content\": \"**Git 推送通知**\n> 仓库: $(basename $(pwd) .git)\n> 分支: $branch\n> 作者: $author\n> 提交数: $commit_count\n> 信息: $message\"
}
}" > /dev/null 2>&1 &
done
8.4.3 邮件通知
#!/bin/bash
# post-receive: 发送邮件通知
REPO_NAME=$(basename $(pwd) .git)
NOTIFY_EMAIL="[email protected]"
while read oldrev newrev refname; do
branch="${refname#refs/heads/}"
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
continue
fi
# 生成变更摘要
summary=$(git log --format="- %s (%an)" "$oldrev".."$newrev" 2>/dev/null | head -20)
# 发送邮件
cat << EMAIL | mail -s "[Git] ${REPO_NAME}: ${branch} updated" "$NOTIFY_EMAIL"
仓库: ${REPO_NAME}
分支: ${branch}
时间: $(date '+%Y-%m-%d %H:%M:%S')
提交记录:
${summary}
EMAIL
done
8.4.4 综合 post-receive 钩子
#!/bin/bash
# post-receive: 综合钩子模板
REPO_NAME=$(basename $(pwd) .git)
LOG_FILE="/var/log/git-${REPO_NAME}.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$REPO_NAME] $1" >> "$LOG_FILE"
}
while read oldrev newrev refname; do
branch="${refname#refs/heads/}"
# === 日志记录 ===
if [ "$newrev" != "0000000000000000000000000000000000000000" ]; then
author=$(git log -1 --format='%an <%ae>' "$newrev")
message=$(git log -1 --format='%s' "$newrev")
log "PUSH: $branch by $author - $message"
else
log "DELETE: branch $branch"
fi
# === 自动部署 (仅 main 分支) ===
if [ "$branch" = "main" ] && [ "$newrev" != "0000000000000000000000000000000000000000" ]; then
log "DEPLOY: Starting auto-deploy for main"
DEPLOY_DIR="/var/www/${REPO_NAME}"
mkdir -p "$DEPLOY_DIR"
GIT_WORK_TREE="$DEPLOY_DIR" git checkout -f main 2>> "$LOG_FILE"
log "DEPLOY: Completed"
fi
# === Webhook 通知 ===
if command -v curl &> /dev/null && [ -n "${WEBHOOK_URL:-}" ]; then
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"repo\":\"$REPO_NAME\",\"branch\":\"$branch\",\"author\":\"${author:-system}\"}" \
> /dev/null 2>&1 &
fi
done
exit 0
8.5 Gitea/Forgejo/GitLab Webhooks
除了原生 Git 钩子,现代 Git 平台还提供 Web 界面配置的 Webhooks。
8.5.1 Gitea Webhook 配置
# 通过 API 创建 Webhook
curl -s -X POST -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$GITEA_URL/api/v1/repos/owner/repo/hooks" \
-d '{
"type": "slack",
"active": true,
"config": {
"url": "https://hooks.slack.com/services/xxx/yyy/zzz",
"content_type": "json"
},
"events": ["push", "pull_request", "issues"]
}'
8.5.2 通用 Webhook 接收器
#!/usr/bin/env python3
# webhook-receiver.py - 通用 Git Webhook 接收器
from flask import Flask, request, jsonify
import subprocess
import json
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
@app.route('/webhook', methods=['POST'])
def handle_webhook():
# 验证签名(如设置了 secret)
signature = request.headers.get('X-Gitea-Signature', '')
if WEBHOOK_SECRET:
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({"error": "Invalid signature"}), 403
payload = request.json
event = request.headers.get('X-Gitea-Event', 'unknown')
print(f"Event: {event}")
print(f"Repo: {payload.get('repository', {}).get('full_name', 'N/A')}")
if event == 'push':
branch = payload.get('ref', '').replace('refs/heads/', '')
pusher = payload.get('pusher', {}).get('login', 'unknown')
commits = len(payload.get('commits', []))
print(f"Push: {branch} by {pusher} ({commits} commits)")
# 触发 CI/CD 或部署
if branch == 'main':
subprocess.Popen(['./deploy.sh',
payload['repository']['full_name']])
return jsonify({"status": "ok"}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000)
8.6 钩子部署指南
8.6.1 裸仓库钩子
# 将钩子脚本放在仓库的 hooks/ 目录
cd /opt/git/project.git/hooks/
# 创建 pre-receive 钩子
cat > pre-receive << 'SCRIPT'
#!/bin/bash
# your pre-receive logic here
SCRIPT
chmod +x pre-receive
# 创建 post-receive 钩子
cat > post-receive << 'SCRIPT'
#!/bin/bash
# your post-receive logic here
SCRIPT
chmod +x post-receive
8.6.2 批量部署钩子
#!/bin/bash
# deploy-hooks.sh - 批量部署钩子到所有仓库
GIT_ROOT="/opt/git"
HOOK_SOURCE="/opt/git-hooks"
for repo in $(find "$GIT_ROOT" -name "*.git" -type d); do
echo "Deploying hooks to: $repo"
for hook in pre-receive update post-receive; do
if [ -f "$HOOK_SOURCE/$hook" ]; then
cp "$HOOK_SOURCE/$hook" "$repo/hooks/$hook"
chmod +x "$repo/hooks/$hook"
fi
done
done
echo "Hooks deployed to all repositories"
8.6.3 GitLab 服务端钩子
# /etc/gitlab/gitlab.rb
gitaly['custom_hooks_dir'] = "/opt/gitlab-custom-hooks"
# 创建钩子目录结构
sudo mkdir -p /opt/gitlab-custom-hooks/pre-receive.d
sudo mkdir -p /opt/gitlab-custom-hooks/post-receive.d
# 放置钩子脚本
sudo cp pre-receive.sh /opt/gitlab-custom-hooks/pre-receive.d/
sudo chmod +x /opt/gitlab-custom-hooks/pre-receive.d/*.sh
# 应用配置
sudo gitlab-ctl reconfigure
8.7 钩子调试
#!/bin/bash
# 调试 pre-receive 钩子
# 在钩子开头添加调试输出
exec 2>/tmp/hook-debug.log
set -x
# 测试推送并查看日志
# git push origin main
# cat /tmp/hook-debug.log
# 手动测试钩子
echo "abc123 def456 refs/heads/main" | /opt/git/project.git/hooks/pre-receive
8.8 扩展阅读
本章小结
| 学到了什么 | 关键要点 |
|---|---|
| 钩子类型 | pre-receive(全局检查)、update(分支检查)、post-receive(后续操作) |
| pre-receive | 禁止强制推送、提交规范检查、文件大小/类型检查 |
| update | 分支命名规范检查、分支级权限控制 |
| post-receive | 自动部署、Webhook 通知、邮件通知 |
| Webhooks | Gitea/GitLab Web 界面配置的 HTTP 回调 |
| 部署方式 | 裸仓库 hooks/ 目录、GitLab custom_hooks_dir |
下一章:第 9 章 - 镜像与备份 — 配置仓库镜像同步和完善的备份策略。