Git 完全指南 / 13 - Git Hooks:客户端钩子、服务端钩子、自动化
第十三章:Git Hooks
Git Hooks 让你在特定事件发生时自动执行脚本,是实现开发工作流自动化的基石。
13.1 Hooks 概述
Git Hooks 是存储在 .git/hooks/ 目录下的脚本,在特定 Git 操作触发时自动执行。
# 查看 hooks 目录
$ ls -la .git/hooks/
total 52
-rwxr-xr-x 1 user user 478 Jan 10 10:00 applypatch-msg.sample
-rwxr-xr-x 1 user user 896 Jan 10 10:00 commit-msg.sample
-rwxr-xr-x 1 user user 4726 Jan 10 10:00 fsmonitor-watchman.sample
-rwxr-xr-x 1 user user 189 Jan 10 10:00 post-update.sample
-rwxr-xr-x 1 user user 424 Jan 10 10:00 pre-applypatch.sample
-rwxr-xr-x 1 user user 1643 Jan 10 10:00 pre-commit.sample
-rwxr-xr-x 1 user user 416 Jan 10 10:00 pre-merge-commit.sample
-rwxr-xr-x 1 user user 1374 Jan 10 10:00 pre-push.sample
-rwxr-xr-x 1 user user 4898 Jan 10 10:00 pre-rebase.sample
-rwxr-xr-x 1 user user 544 Jan 10 10:00 pre-receive.sample
-rwxr-xr-x 1 user user 1239 Jan 10 10:00 prepare-commit-msg.sample
-rwxr-xr-x 1 user user 3610 Jan 10 10:00 update.sample
⚠️ 只有不带
.sample后缀的 hook 才会被执行。
13.2 客户端 Hooks
13.2.1 pre-commit — 提交前检查
#!/bin/bash
# .git/hooks/pre-commit
# 运行代码检查
echo "Running lint..."
npm run lint
if [ $? -ne 0 ]; then
echo "❌ Lint failed. Commit aborted."
exit 1
fi
# 运行测试
echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Commit aborted."
exit 1
fi
# 检查是否包含调试语句
if git diff --cached --name-only | xargs grep -l "console.log\|debugger\|TODO" 2>/dev/null; then
echo "⚠️ Warning: Found debug statements or TODOs"
# exit 1 # 取消注释则阻止提交
fi
echo "✅ All pre-commit checks passed."
13.2.2 commit-msg — 提交信息规范
#!/bin/bash
# .git/hooks/commit-msg
# 检查提交信息格式(Conventional Commits)
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,72}"
if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then
echo "❌ Invalid commit message format."
echo ""
echo "Expected: <type>(<scope>): <description>"
echo "Example: feat(auth): add login page"
echo ""
echo "Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert"
exit 1
fi
echo "✅ Commit message format is valid."
13.2.3 prepare-commit-msg — 修改提交信息
#!/bin/bash
# .git/hooks/prepare-commit-msg
# 自动添加分支名到提交信息
branch_name=$(git symbolic-ref --short HEAD)
commit_file="$1"
if [[ "$branch_name" =~ ^(feature|bugfix|hotfix)/([A-Z]+-[0-9]+) ]]; then
issue_id="${BASH_REMATCH[2]}"
sed -i.bak -e "1s/^/[$issue_id] /" "$commit_file"
fi
13.2.4 pre-push — 推送前检查
#!/bin/bash
# .git/hooks/pre-push
# 检查是否有未运行的测试
untested=$(git log --oneline origin/main..HEAD --no-merges | head -5)
if [ -n "$untested" ]; then
echo "⚠️ The following commits haven't been tested:"
echo "$untested"
read -p "Continue with push? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
13.2.5 post-checkout / post-merge
#!/bin/bash
# .git/hooks/post-checkout
# 切换分支后自动安装依赖
if [ -f package.json ]; then
echo "📦 Checking dependencies..."
npm install --silent
fi
# 检查是否有数据库迁移
if [ -d migrations ]; then
echo "🔄 Checking for pending migrations..."
npm run migrate:status
fi
#!/bin/bash
# .git/hooks/post-merge
# 合并后自动安装依赖
if [ -f package.json ]; then
changed_files=$(git diff HEAD@{1} --name-only)
if echo "$changed_files" | grep -q "package.json\|package-lock.json"; then
echo "📦 Dependencies changed, running npm install..."
npm install
fi
fi
13.3 服务端 Hooks
13.3.1 pre-receive — 接收前检查
#!/bin/bash
# hooks/pre-receive
while read oldrev newrev refname; do
# 禁止强制推送到 main
if [ "$refname" = "refs/heads/main" ]; then
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
commits=$(git rev-list "$oldrev".."$newrev" --count)
if [ "$commits" -gt 0 ]; then
# 检查是否为 force push
merge_base=$(git merge-base "$oldrev" "$newrev")
if [ "$merge_base" != "$oldrev" ]; then
echo "❌ Force push to main is not allowed!"
exit 1
fi
fi
fi
fi
# 检查提交信息格式
for commit in $(git rev-list "$oldrev".."$newrev"); do
msg=$(git log -1 --format="%s" "$commit")
if ! echo "$msg" | grep -qE "^(feat|fix|docs|chore|refactor):"; then
echo "❌ Invalid commit message: $msg"
echo " Expected format: type(scope): description"
exit 1
fi
done
done
exit 0
13.3.2 update — 分支级检查
#!/bin/bash
# hooks/update
refname="$1"
oldrev="$2"
newrev="$3"
# 禁止删除受保护分支
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
echo "❌ Deleting $refname is not allowed!"
exit 1
fi
# 限制单次推送的文件数量
file_count=$(git diff --name-only "$oldrev" "$newrev" | wc -l)
if [ "$file_count" -gt 100 ]; then
echo "❌ Too many files changed ($file_count). Maximum is 100."
exit 1
fi
13.3.3 post-receive — 推送后操作
#!/bin/bash
# hooks/post-receive
while read oldrev newrev refname; do
branch=$(echo "$refname" | sed 's|refs/heads/||')
# 推送到 main 时触发部署
if [ "$branch" = "main" ]; then
echo "🚀 Deploying main branch..."
cd /var/www/production
git pull origin main
npm install --production
pm2 restart app
fi
# 推送到 staging 时触发测试部署
if [ "$branch" = "staging" ]; then
echo "🧪 Deploying to staging..."
cd /var/www/staging
git pull origin staging
npm install
npm test
pm2 restart staging-app
fi
done
13.4 Hooks 管理工具
13.4.1 Husky(Node.js 项目)
# 安装 husky
$ npm install husky --save-dev
# 初始化 husky
$ npx husky init
# 添加 pre-commit hook
$ npx husky add .husky/pre-commit "npm run lint && npm test"
# 添加 commit-msg hook
$ npx husky add .husky/commit-msg 'npx commitlint --edit "$1"'
// package.json
{
"scripts": {
"prepare": "husky"
},
"devDependencies": {
"husky": "^9.0.0",
"@commitlint/cli": "^19.0.0",
"@commitlint/config-conventional": "^19.0.0"
}
}
13.4.2 使用 .githooks 目录
# 配置自定义 hooks 目录
$ git config core.hooksPath .githooks
# 在项目根目录创建 .githooks 目录
$ mkdir .githooks
$ cat > .githooks/pre-commit << 'EOF'
#!/bin/bash
npm run lint
EOF
$ chmod +x .githooks/pre-commit
13.5 Hooks 完整列表
| Hook | 触发时机 | 类型 | 退出码影响 |
|---|---|---|---|
pre-commit | git commit 前 | 客户端 | 非 0 阻止提交 |
prepare-commit-msg | 编辑器打开前 | 客户端 | - |
commit-msg | 提交信息写入后 | 客户端 | 非 0 阻止提交 |
post-commit | 提交完成后 | 客户端 | - |
pre-rebase | 变基前 | 客户端 | 非 0 阻止变基 |
post-checkout | checkout 后 | 客户端 | - |
post-merge | 合并后 | 客户端 | - |
pre-push | push 前 | 客户端 | 非 0 阻止推送 |
pre-receive | 服务端接收前 | 服务端 | 非 0 拒绝推送 |
update | 每个分支更新前 | 服务端 | 非 0 拒绝该分支 |
post-receive | 接收完成后 | 服务端 | - |
业务场景
| 场景 | 推荐 Hook | 工具 |
|---|---|---|
| 代码风格检查 | pre-commit | eslint, prettier, black |
| 提交信息规范 | commit-msg | commitlint |
| 运行测试 | pre-commit | jest, pytest |
| 阻止直接推送到 main | pre-push / pre-receive | 自定义脚本 |
| 自动部署 | post-receive | CI/CD 脚本 |
| 自动安装依赖 | post-checkout, post-merge | npm install |
扩展阅读
- git-scm.com: Git Hooks
- Husky
- Commitlint
- pre-commit — Python hooks 框架
🔗 上一章:12 - 工作树 | 下一章:14 - Git LFS