跳到主要内容

Git 钩子

Git 钩子(Hooks)是在特定事件发生时自动执行的脚本。它们可以用于自动化代码检查、强制执行开发规范、部署应用等场景。理解和使用钩子能显著提升团队的开发效率和代码质量。

钩子的概念

Git 钩子本质上是存储在 .git/hooks 目录下的可执行脚本。当特定的 Git 操作触发时,Git 会自动调用相应的钩子脚本。

钩子存储位置

# 查看钩子目录
ls -la .git/hooks/

# 输出示例
total 16
drwxr-xr-x 1 user group 512 Jan 15 10:00 .
drwxr-xr-x 1 user group 512 Jan 15 10:00 ..
-rw-r--r-- 1 user group 478 Jan 15 10:00 applypatch-msg.sample
-rw-r--r-- 1 user group 457 Jan 15 10:00 commit-msg.sample
-rw-r--r-- 1 user group 896 Jan 15 10:00 fsmonitor-watchman.sample
-rw-r--r-- 1 user group 189 Jan 15 10:00 post-update.sample
-rw-r--r-- 1 user group 424 Jan 15 10:00 pre-applypatch.sample
-rw-r--r-- 1 user group 1643 Jan 15 10:00 pre-commit.sample
...

.sample 结尾的是示例文件,默认不生效。要启用某个钩子,需要去掉 .sample 后缀并添加执行权限。

钩子的执行时机

客户端钩子

客户端钩子在你的本地仓库中运行,用于影响个人的开发流程。

提交相关钩子

pre-commit

在输入提交信息之前运行,用于检查即将提交的内容。

典型用途

  • 代码风格检查
  • 静态分析
  • 运行测试
  • 检查敏感信息

示例:代码风格检查

#!/bin/bash
# .git/hooks/pre-commit

echo "🔍 运行代码检查..."

# 获取所有暂存的 .js 文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$')

if [ -z "$STAGED_FILES" ]; then
echo "✅ 没有需要检查的 JS 文件"
exit 0
fi

# 运行 ESLint
echo "$STAGED_FILES" | xargs npx eslint

if [ $? -ne 0 ]; then
echo "❌ 代码检查失败,请修复后再提交"
echo "💡 提示:使用 'eslint --fix' 自动修复部分问题"
exit 1
fi

echo "✅ 代码检查通过"
exit 0

示例:检查敏感信息

#!/bin/bash
# .git/hooks/pre-commit

# 检查是否包含敏感信息
PATTERNS=(
"password\s*=\s*['\"][^'\"]+['\"]"
"api_key\s*=\s*['\"][^'\"]+['\"]"
"secret\s*=\s*['\"][^'\"]+['\"]"
"-----BEGIN.*PRIVATE KEY-----"
"aws_access_key_id"
"aws_secret_access_key"
)

FOUND=0

for pattern in "${PATTERNS[@]}"; do
if git diff --cached | grep -iEq "$pattern"; then
echo "❌ 检测到敏感信息:$pattern"
FOUND=1
fi
done

if [ $FOUND -eq 1 ]; then
echo "🚫 提交被阻止,请移除敏感信息"
exit 1
fi

exit 0

prepare-commit-msg

在默认提交信息生成后、编辑器启动前运行。

典型用途

  • 自动生成提交信息模板
  • 在合并提交时添加信息

示例:自动添加分支名到提交信息

#!/bin/bash
# .git/hooks/prepare-commit-msg

# 获取当前分支名
BRANCH_NAME=$(git branch --show-current)

# 如果分支名符合 feature/xxx 格式,添加到提交信息
if [[ $BRANCH_NAME =~ ^feature/ ]]; then
# 提取功能名称
FEATURE_NAME=${BRANCH_NAME#feature/}

# 在提交信息文件开头添加引用
echo "[$FEATURE_NAME] $(cat $1)" > "$1"
fi

exit 0

commit-msg

在提交信息输入后运行,可以验证提交信息格式。

典型用途

  • 强制提交信息格式规范
  • 添加 Signed-off-by 等信息

示例:验证提交信息格式

#!/bin/bash
# .git/hooks/commit-msg

COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")

# Conventional Commits 格式
# type(scope): subject
PATTERN="^(feat|fix|docs|style|refactor|test|build|ci|chore|revert)(\(.+\))?:\s.{1,50}"

if ! [[ $COMMIT_MSG =~ $PATTERN ]]; then
echo "❌ 提交信息格式不正确"
echo ""
echo "正确格式: <type>(<scope>): <subject>"
echo ""
echo "类型说明:"
echo " feat - 新功能"
echo " fix - Bug 修复"
echo " docs - 文档更新"
echo " style - 代码格式(不影响功能)"
echo " refactor - 重构"
echo " test - 测试相关"
echo " build - 构建相关"
echo " ci - CI 配置"
echo " chore - 其他杂项"
echo ""
echo "示例:"
echo " feat(auth): 添加 JWT 认证功能"
echo " fix: 修复登录验证的空指针异常"
exit 1
fi

# 检查提交信息长度
FIRST_LINE=$(echo "$COMMIT_MSG" | head -n1)
if [ ${#FIRST_LINE} -gt 72 ]; then
echo "❌ 提交信息首行过长(最多 72 字符)"
exit 1
fi

exit 0

post-commit

在提交完成后运行,不会影响提交结果。

典型用途

  • 发送通知
  • 触发自动化任务
  • 更新文档

示例:提交后通知

#!/bin/bash
# .git/hooks/post-commit

# 获取提交信息
COMMIT_MSG=$(git log -1 --pretty=%B)
COMMIT_HASH=$(git log -1 --pretty=%h)

# 发送通知(macOS)
if command -v osascript &> /dev/null; then
osascript -e "display notification \"提交 $COMMIT_HASH 完成\" with title \"Git\""
fi

# 或者发送到 Slack(需要配置 webhook)
# curl -X POST -H 'Content-type: application/json' \
# --data "{\"text\":\"新提交: $COMMIT_MSG\"}" \
# "$SLACK_WEBHOOK_URL"

exit 0

其他客户端钩子

pre-push

在推送前运行,可以阻止推送。

示例:推送前运行测试

#!/bin/bash
# .git/hooks/pre-push

# 获取要推送的分支
BRANCH=$(git branch --show-current)

# 只在 main 分支推送时运行完整测试
if [ "$BRANCH" = "main" ]; then
echo "🧪 运行完整测试套件..."
npm test

if [ $? -ne 0 ]; then
echo "❌ 测试失败,禁止推送到 main 分支"
exit 1
fi
fi

exit 0

post-checkout

在切换分支或检出文件后运行。

示例:自动安装依赖

#!/bin/bash
# .git/hooks/post-checkout

# 参数:$1=新HEAD, $2=旧HEAD, $3=是否切换分支(1/0)

if [ "$3" -eq 1 ]; then
# 检查 package.json 是否有变化
if git diff --name-only "$2" "$1" | grep -q "package.json\|package-lock.json"; then
echo "📦 检测到依赖变化,正在安装..."
npm install
fi

# 检查 Python 依赖
if git diff --name-only "$2" "$1" | grep -q "requirements.txt\|pyproject.toml"; then
echo "📦 检测到 Python 依赖变化,正在安装..."
pip install -r requirements.txt
fi
fi

exit 0

post-merge

在合并完成后运行。

示例:合并后更新依赖

#!/bin/bash
# .git/hooks/post-merge

# 检查依赖文件是否有变化
if git diff --name-only ORIG_HEAD HEAD | grep -q "package.json\|package-lock.json"; then
echo "📦 更新依赖..."
npm install
fi

exit 0

服务端钩子

服务端钩子在远程仓库上运行,用于强制执行项目规范。

pre-receive

在接收推送前运行,可以拒绝整个推送。

示例:禁止强制推送到 main 分支

#!/bin/bash
# 服务端 .git/hooks/pre-receive

while read oldrev newrev refname; do
# 检查是否是 main 分支
if [ "$refname" = "refs/heads/main" ]; then
# 检查是否是强制推送(非快进)
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
if ! git merge-base --is-ancestor "$oldrev" "$newrev"; then
echo "❌ 禁止强制推送到 main 分支"
exit 1
fi
fi
fi
done

exit 0

update

在更新每个分支前运行,可以针对特定分支设置不同规则。

示例:限制推送到特定分支

#!/bin/bash
# 服务端 .git/hooks/update

REFNAME=$1
OLDREV=$2
NEWREV=$3

# 获取推送者
PUSHER=$(git log -1 --format='%cn' "$NEWREV" 2>/dev/null || echo "unknown")

# 保护 release 分支
if [[ $REFNAME =~ refs/heads/release/ ]]; then
# 只有特定用户可以推送
ALLOWED_USERS=("admin" "release-manager")

if [[ ! " ${ALLOWED_USERS[@]} " =~ " $PUSHER " ]]; then
echo "❌ 只有管理员和发布经理可以推送到 release 分支"
exit 1
fi
fi

exit 0

post-receive

在推送接收完成后运行,常用于触发部署。

示例:自动部署

#!/bin/bash
# 服务端 .git/hooks/post-receive

while read oldrev newrev refname; do
BRANCH=$(echo "$refname" | sed 's|refs/heads/||')

if [ "$BRANCH" = "main" ]; then
echo "🚀 触发生产环境部署..."
# 调用部署脚本
/var/www/deploy.sh production
elif [ "$BRANCH" = "staging" ]; then
echo "🚀 触发预发布环境部署..."
/var/www/deploy.sh staging
fi
done

exit 0

钩子管理工具

直接管理 .git/hooks 目录的缺点是钩子脚本不会被 Git 跟踪。使用管理工具可以解决这个问题。

Husky

Husky 是最流行的 Git 钩子管理工具,可以让钩子脚本参与版本控制。

安装

# npm
npm install husky --save-dev

# 初始化 Husky
npx husky init

配置

Husky 8.x 使用 .husky/ 目录管理钩子:

# 创建 pre-commit 钩子
echo "npm test" > .husky/pre-commit

# 创建 commit-msg 钩子
echo "npx commitlint --edit \$1" > .husky/commit-msg

package.json 配置

{
"scripts": {
"prepare": "husky install",
"test": "jest",
"lint": "eslint ."
}
}

lint-staged

lint-staged 只对暂存的文件运行检查,提高效率。

安装

npm install lint-staged --save-dev

配置

// package.json
{
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.css": ["prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}

结合 Husky

# .husky/pre-commit
npx lint-staged

commitlint

commitlint 用于验证提交信息格式。

安装

npm install @commitlint/cli @commitlint/config-conventional --save-dev

配置

// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
],
'subject-max-length': [2, 'always', 50]
}
};

结合 Husky

# .husky/commit-msg
npx commitlint --edit $1

pre-commit

pre-commit 是一个多语言的钩子管理框架。

配置

# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-merge-conflict

- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.56.0
hooks:
- id: eslint
files: \.js$

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier

安装和运行

# 安装 pre-commit
pip install pre-commit

# 安装钩子
pre-commit install

# 手动运行
pre-commit run --all-files

钩子最佳实践

1. 保持钩子快速执行

钩子会在关键操作时运行,应该尽量快速:

# ❌ 不推荐:运行所有测试
npm test

# ✅ 推荐:只测试相关文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
npm test --findRelatedTests $STAGED_FILES

2. 提供清晰的错误信息

# ❌ 不推荐
exit 1

# ✅ 推荐
echo "❌ 代码检查失败"
echo "问题文件:"
echo "$PROBLEM_FILES"
echo ""
echo "修复方法:npm run lint --fix"
exit 1

3. 允许跳过钩子

有时候需要紧急提交,应该允许跳过钩子:

# 使用 --no-verify 跳过
git commit --no-verify -m "紧急修复"

# 在钩子中检查环境变量
if [ "$SKIP_HOOKS" = "1" ]; then
echo "⚠️ 钩子已跳过"
exit 0
fi

4. 钩子脚本纳入版本控制

使用 Husky 或 pre-commit 等工具,将钩子配置纳入版本控制。

5. 区分本地和全局规则

本地检查(如代码风格)适合放在 pre-commit,团队规则(如分支保护)适合放在服务端钩子。

完整示例:前端项目钩子配置

目录结构

project/
├── .husky/
│ ├── pre-commit
│ └── commit-msg
├── package.json
├── .eslintrc.js
├── .prettierrc
└── commitlint.config.js

package.json

{
"scripts": {
"prepare": "husky install",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write .",
"test": "jest"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css}": ["prettier --write"]
},
"devDependencies": {
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"eslint": "^8.56.0",
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
"prettier": "^3.1.1"
}
}

.husky/pre-commit

#!/bin/bash
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

.husky/commit-msg

#!/bin/bash
. "$(dirname "$0")/_/husky.sh"

npx commitlint --edit $1

commitlint.config.js

module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'build', 'ci', 'chore']
]
}
};

钩子命令速查表

钩子触发时机能否阻止操作典型用途
pre-commit提交前代码检查、格式化
prepare-commit-msg提交信息生成后自动生成提交信息
commit-msg提交信息输入后验证提交信息格式
post-commit提交后发送通知
pre-push推送前运行测试
post-checkout切换分支后安装依赖
post-merge合并后更新依赖
pre-receive服务端接收推送前分支保护
update服务端更新分支前权限控制
post-receive服务端接收推送后自动部署

小结

本章我们学习了:

  1. 钩子的概念:Git 特定事件触发时自动执行的脚本
  2. 客户端钩子:pre-commit、commit-msg、pre-push 等
  3. 服务端钩子:pre-receive、update、post-receive
  4. 管理工具:Husky、lint-staged、commitlint、pre-commit
  5. 最佳实践:保持快速、清晰错误信息、允许跳过、版本控制

练习

  1. 创建一个 pre-commit 钩子,检查提交的代码是否包含 console.log
  2. 使用 Husky 和 lint-staged 配置一个前端项目
  3. 创建一个 commit-msg 钩子,强制使用 Conventional Commits 格式
  4. 配置一个服务端钩子,保护 main 分支禁止强制推送

参考资料