什么是 Git Hooks
Git Hooks 是 Git 在特定事件发生时(提交前、推送前、合并后等)自动执行的脚本。它们存放在仓库的 .git/hooks/ 目录中。
# 查看所有可用的 hooks 示例
ls .git/hooks/
# 输出:applypatch-msg.sample commit-msg.sample pre-commit.sample ...
# .sample 结尾的是示例,去掉 .sample 后缀并给予执行权限即可激活
Hooks 可以是任何可执行脚本:Shell、Python、Node.js 等。钩子脚本返回非零退出码时,相应的 Git 操作会被中止。
常用客户端 Hooks
pre-commit — 提交前检查
在执行 git commit 时,创建提交对象之前运行。最常用于代码检查、格式化、单元测试。
#!/bin/sh
# .git/hooks/pre-commit
# 运行 ESLint 检查
npm run lint
if [ $? -ne 0 ]; then
echo "❌ ESLint 检查失败,提交已中止"
exit 1
fi
echo "✅ Lint 检查通过"
commit-msg — 验证提交信息格式
在提交信息被保存后运行,接收提交信息文件路径作为参数。用于验证 Conventional Commits 等格式规范。
#!/bin/sh
# .git/hooks/commit-msg
# $1 是包含提交信息的临时文件路径
commit_msg=$(cat "$1")
pattern='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .+'
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "❌ 提交信息不符合 Conventional Commits 规范"
echo " 示例: feat: add login page"
exit 1
fi
pre-push — 推送前运行
在 git push 将数据发送到远程之前运行。常用于在推送前运行完整测试套件,防止推送失败的代码。
#!/bin/sh
# .git/hooks/pre-push
echo "🧪 运行测试套件..."
npm test
if [ $? -ne 0 ]; then
echo "❌ 测试失败,推送已中止"
exit 1
fi
prepare-commit-msg — 自动修改提交信息
在提交信息编辑器打开之前运行。常用于自动在提交信息中插入 Jira ticket 号(从分支名中提取)。
#!/bin/sh
# .git/hooks/prepare-commit-msg
# 从分支名中提取 Jira issue 号(如 feature/PROJ-123-login)
branch=$(git symbolic-ref --short HEAD)
jira_ticket=$(echo "$branch" | grep -oE '[A-Z]+-[0-9]+')
if [ -n "$jira_ticket" ]; then
# 在提交信息末尾追加 Jira ticket 号
echo -e "\n\nJira: $jira_ticket" >> "$1"
fi
服务端 Hooks(简介)
| Hook | 触发时机 | 常见用途 |
|---|---|---|
pre-receive | 接收 push 之前 | 验证所有 commit 的权限、格式 |
update | 每个分支更新时 | 保护特定分支(如禁止 force push) |
post-receive | push 完成后 | 触发部署、发送通知 |
服务端 hooks 存在于 Git 服务器仓库中,对所有推送者生效,无法被客户端绕过。GitHub/GitLab 提供了 Webhooks 和 Protected Branch 等更友好的替代方案。
创建一个实用的 pre-commit Hook
以下脚本检查是否有 console.log 语句被遗忘在 JS 文件中:
#!/bin/sh
# .git/hooks/pre-commit
# 获取暂存的 JS/TS 文件
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$')
if [ -z "$staged_files" ]; then
exit 0
fi
# 检查是否包含 console.log
found=$(echo "$staged_files" | xargs grep -l 'console\.log')
if [ -n "$found" ]; then
echo "❌ 发现遗留的 console.log:"
echo "$found"
echo "请移除后再提交。"
exit 1
fi
exit 0
# 激活 hook(给予执行权限)
chmod +x .git/hooks/pre-commit
Husky — 团队共享 Git Hooks
.git/hooks/ 目录不会被 Git 追踪(不在版本控制中),所以每个开发者需要手动配置 hooks,无法团队共享。Husky 解决了这个问题:它将 hooks 存放在项目根目录的 .husky/ 目录中,可以提交到 Git,团队所有成员自动共享。
# 安装 Husky
npm install husky --save-dev
# 初始化 Husky(创建 .husky/ 目录和 prepare 脚本)
npx husky init
# 此时 package.json 中会有:
# "scripts": { "prepare": "husky" }
# 其他开发者 npm install 后自动安装 hooks
# .husky/ 目录结构
.husky/
├── pre-commit # 可以提交到 Git!
└── commit-msg
# 添加 pre-commit hook
echo "npx lint-staged" > .husky/pre-commit
chmod +x .husky/pre-commit
# 添加 commit-msg hook(使用 commitlint)
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
chmod +x .husky/commit-msg
lint-staged — 只检查暂存的文件
如果 pre-commit 每次都对整个项目运行 lint,速度会很慢。lint-staged 只对已 git add 的文件(staged files)运行检查,大幅提升速度。
# 安装 lint-staged
npm install lint-staged --save-dev
在 package.json 中配置:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss}": [
"stylelint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
commitlint — 验证提交信息
commitlint 检查提交信息是否符合 Conventional Commits 规范,配合 Husky 的 commit-msg hook 使用。
# 安装 commitlint 及 Conventional Commits 规范配置
npm install --save-dev @commitlint/cli @commitlint/config-conventional
创建配置文件 commitlint.config.js:
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// 可选:自定义规则
'subject-max-length': [2, 'always', 72],
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'test', 'chore', 'perf', 'ci', 'revert'
]]
}
};
完整配置示例
将以上所有工具整合在一起的完整 package.json:
{
"name": "my-project",
"scripts": {
"prepare": "husky",
"lint": "eslint src --ext .js,.ts",
"test": "jest --passWithNoTests"
},
"devDependencies": {
"@commitlint/cli": "^18.0.0",
"@commitlint/config-conventional": "^18.0.0",
"eslint": "^8.0.0",
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"prettier": "^3.0.0"
},
"lint-staged": {
"*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,json,md}": ["prettier --write"]
}
}
.husky/pre-commit 文件内容:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
.husky/commit-msg 文件内容:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "${1}"
绕过 Hooks(紧急情况):在紧急情况下,可以用 git commit --no-verify 跳过 hooks。但这应该是极少数例外,不能成为常态。团队应在 README 中明确说明 hooks 的用途和绕过策略。