搜索 K
主题
Git hooks 是在特定 Git 事件发生时自动执行的脚本。它们是 Git 内置的自动化机制,可以在提交、推送、合并等操作前后触发自定义逻辑,用于代码质量检查、规范强制、自动化测试等。
Hooks 就是 Git 操作的拦截器——在 commit/push/merge 等动作的关键节点自动运行你的脚本。
| 分类 | Hook | 触发时机 | 可阻止操作? |
|---|---|---|---|
| 提交 | pre-commit | commit 执行前 | ✅ |
prepare-commit-msg | 编辑器打开前(可修改消息) | ✅ | |
commit-msg | 用户输入提交消息后 | ✅ | |
post-commit | commit 完成后 | ❌ | |
| 推送 | pre-push | push 执行前 | ✅ |
| 合并 | post-merge | merge 完成后 | ❌ |
pre-rebase | rebase 执行前 | ✅ | |
| 检出 | post-checkout | checkout/switch 完成后 | ❌ |
| 其他 | pre-auto-gc | 自动 gc 前 | ✅ |
post-rewrite | amend/rebase 重写提交后 | ❌ | |
| 服务端 | pre-receive | 服务端接收 push 前 | ✅ |
update | 每个分支更新前 | ✅ | |
post-receive | 服务端接收 push 后 | ❌ |
# 1. 位置:.git/hooks/ 目录下
ls .git/hooks/
# pre-commit.sample
# commit-msg.sample
# pre-push.sample
# ...(Git 默认提供 .sample 文件)
# 2. 启用:去掉 .sample 后缀
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
# 3. 权限:必须可执行(Linux/macOS)
chmod +x .git/hooks/pre-commit
# 4. 退出码:
# - 返回 0 = 成功,操作继续
# - 返回非 0 = 失败,操作被阻止(仅限 pre-* hooks)
# 5. 跳过 hooks(紧急情况)
git commit --no-verify # 跳过 pre-commit 和 commit-msg
git push --no-verify # 跳过 pre-push最常用的 hook,在 git commit 执行前运行。常用于代码质量检查。
#!/bin/bash
# .git/hooks/pre-commit
# 1. 运行 linter
echo "🔍 运行代码检查..."
npx eslint --cache $(git diff --cached --name-only --diff-filter=d -- '*.ts' '*.tsx' '*.js' '*.jsx')
if [ $? -ne 0 ]; then
echo "❌ ESLint 检查失败,请修复后再提交"
exit 1
fi
# 2. 运行格式化检查
echo "🎨 检查代码格式..."
npx prettier --check $(git diff --cached --name-only --diff-filter=d -- '*.ts' '*.tsx' '*.js' '*.jsx' '*.css' '*.json')
if [ $? -ne 0 ]; then
echo "❌ 格式不规范,请运行 npx prettier --write 修复"
exit 1
fi
# 3. 检查是否有调试代码
echo "🐛 检查调试代码..."
STAGED_FILES=$(git diff --cached --name-only --diff-filter=d)
if echo "$STAGED_FILES" | xargs grep -l "console\.log\|debugger\|TODO:" 2>/dev/null; then
echo "⚠️ 发现调试代码或 TODO,确认是否需要清理"
# 这里可以选择 exit 1 阻止提交,或只是警告
fi
# 4. 检查文件大小
MAX_SIZE=5242880 # 5MB
for file in $(git diff --cached --name-only --diff-filter=d); do
SIZE=$(wc -c < "$file" 2>/dev/null || echo 0)
if [ "$SIZE" -gt "$MAX_SIZE" ]; then
echo "❌ 文件 $file 太大 ($(($SIZE/1024/1024))MB),请使用 Git LFS"
exit 1
fi
done
echo "✅ 所有检查通过"
exit 0验证提交消息格式,常用于强制 Conventional Commits 规范。
#!/bin/bash
# .git/hooks/commit-msg
MSG_FILE=$1
MSG=$(cat "$MSG_FILE")
# Conventional Commits 格式验证
# type(scope): description
PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,100}$"
# 允许 merge 和 revert 提交
if echo "$MSG" | head -1 | grep -qE "^(Merge|Revert) "; then
exit 0
fi
if ! echo "$MSG" | head -1 | grep -qE "$PATTERN"; then
echo "❌ 提交消息格式不符合 Conventional Commits 规范"
echo ""
echo "正确格式: <type>(<scope>): <description>"
echo ""
echo "type 可选值:"
echo " feat 新功能"
echo " fix Bug 修复"
echo " docs 文档变更"
echo " style 代码格式(不影响逻辑)"
echo " refactor 重构(不增加功能/不修复 bug)"
echo " perf 性能优化"
echo " test 测试相关"
echo " build 构建系统/外部依赖"
echo " ci CI 配置"
echo " chore 其他杂务"
echo ""
echo "示例:"
echo " feat(auth): 添加 OAuth2 登录"
echo " fix: 修复支付金额精度问题"
echo ""
echo "你的提交消息: $MSG"
exit 1
fi
exit 0在编辑器打开前自动修改提交消息。可用于自动添加分支名、Issue 编号等。
#!/bin/bash
# .git/hooks/prepare-commit-msg
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2 # message, template, merge, squash, commit
# 不处理 merge/squash 等自动生成的消息
if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ]; then
exit 0
fi
# 从分支名提取 Issue 编号
BRANCH=$(git branch --show-current)
ISSUE=$(echo "$BRANCH" | grep -oE '[A-Z]+-[0-9]+' | head -1)
if [ -n "$ISSUE" ]; then
# 如果消息中还没有 Issue 编号,自动添加
if ! grep -q "$ISSUE" "$COMMIT_MSG_FILE"; then
sed -i "1s/$/ [$ISSUE]/" "$COMMIT_MSG_FILE"
fi
fi在 git push 执行前运行。用于推送前的最后检查。
#!/bin/bash
# .git/hooks/pre-push
REMOTE="$1"
URL="$2"
# 1. 禁止 push 到受保护分支
CURRENT=$(git rev-parse --abbrev-ref HEAD)
PROTECTED="main master develop"
for branch in $PROTECTED; do
if [ "$CURRENT" = "$branch" ]; then
echo "❌ 禁止直接推送到 $branch!请使用 Pull Request。"
exit 1
fi
done
# 2. push 前运行测试
echo "🧪 运行测试..."
npm test -- --watchAll=false
if [ $? -ne 0 ]; then
echo "❌ 测试失败,请修复后再推送"
exit 1
fi
# 3. 检查是否有未提交的修改
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "⚠️ 有未提交的修改,建议先提交或 stash"
fi
echo "✅ 推送检查通过"
exit 0在切换分支或合并后运行。常用于自动安装依赖。
#!/bin/bash
# .git/hooks/post-checkout 或 .git/hooks/post-merge
# 检查 package.json 是否有变化
PREV_HEAD=$1
POST_HEAD=$2
IS_BRANCH_CHECKOUT=$3 # 1 = 分支切换, 0 = 文件检出
# 只在分支切换时运行(post-checkout)
if [ "$IS_BRANCH_CHECKOUT" = "0" ]; then
exit 0
fi
# 检查依赖文件是否有变化
CHANGED_FILES=$(git diff --name-only $PREV_HEAD $POST_HEAD 2>/dev/null)
if echo "$CHANGED_FILES" | grep -q "package-lock.json\|package.json"; then
echo "📦 检测到依赖变化,自动安装..."
npm install
fi
if echo "$CHANGED_FILES" | grep -q "requirements.txt\|Pipfile.lock"; then
echo "🐍 检测到 Python 依赖变化..."
pip install -r requirements.txt
fi
if echo "$CHANGED_FILES" | grep -q "Cargo.lock\|Cargo.toml"; then
echo "🦀 检测到 Rust 依赖变化..."
cargo build
fi
# 检查数据库迁移
if echo "$CHANGED_FILES" | grep -q "migrations/"; then
echo "🗄️ 检测到数据库迁移变化,请运行迁移命令"
fi在服务端接收 push 前运行。可用于强制策略检查。
#!/bin/bash
# hooks/pre-receive(服务端)
while read OLDSHA NEWSHA REFNAME; do
BRANCH=$(echo "$REFNAME" | sed 's|refs/heads/||')
# 1. 禁止 force push 到 main
if [ "$BRANCH" = "main" ]; then
# 检查是否是 force push(OLDSHA 不是 NEWSHA 的祖先)
if ! git merge-base --is-ancestor "$OLDSHA" "$NEWSHA" 2>/dev/null; then
echo "❌ 禁止 force push 到 main!"
exit 1
fi
fi
# 2. 检查提交消息格式
for sha in $(git rev-list $OLDSHA..$NEWSHA); do
MSG=$(git log --format=%s -1 $sha)
if ! echo "$MSG" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|Merge|Revert)"; then
echo "❌ 提交 $sha 的消息不符合规范: $MSG"
exit 1
fi
done
# 3. 检查大文件
for sha in $(git rev-list $OLDSHA..$NEWSHA); do
git diff-tree --no-commit-id -r $sha | while read MODE_OLD MODE_NEW SHA_OLD SHA_NEW STATUS FILE; do
SIZE=$(git cat-file -s "$SHA_NEW" 2>/dev/null || echo 0)
if [ "$SIZE" -gt 10485760 ]; then # 10MB
echo "❌ 文件 $FILE ($((SIZE/1024/1024))MB) 超过大小限制"
exit 1
fi
done
done
done
exit 0push 成功后触发。常用于自动部署、通知等。
#!/bin/bash
# hooks/post-receive(服务端)
while read OLDSHA NEWSHA REFNAME; do
BRANCH=$(echo "$REFNAME" | sed 's|refs/heads/||')
# 推送到 main 时触发部署
if [ "$BRANCH" = "main" ]; then
echo "🚀 触发生产环境部署..."
/opt/deploy/deploy-production.sh &
fi
# 推送到 develop 时触发 staging 部署
if [ "$BRANCH" = "develop" ]; then
echo "🔧 触发 staging 环境部署..."
/opt/deploy/deploy-staging.sh &
fi
# 发送通知
AUTHOR=$(git log -1 --format='%an' $NEWSHA)
SUBJECT=$(git log -1 --format='%s' $NEWSHA)
curl -s -X POST "https://hooks.slack.com/services/xxx" \
-d "{\"text\": \"[$BRANCH] $AUTHOR: $SUBJECT\"}" > /dev/null
done.git/hooks/ 中的脚本不受版本控制(.git 目录不会被提交)。Husky 解决了这个问题,让 hooks 可以共享给整个团队。
# 安装 Husky v9+
npm install -D husky
# 初始化
npx husky init
# 创建 .husky/ 目录和 pre-commit hook
# 项目结构
# .husky/
# pre-commit ← hooks 在这里
# commit-msg
# pre-push
# package.json ← "prepare": "husky"# 安装 lint-staged
npm install -D lint-staged
# .husky/pre-commit
npx lint-staged
# package.json
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss}": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}# 安装 commitlint
npm install -D @commitlint/cli @commitlint/config-conventional
# commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'build', 'ci', 'chore', 'revert'
]],
'subject-max-length': [2, 'always', 100],
}
};
# .husky/commit-msg
npx --no -- commitlint --edit $1# .husky/pre-push
npm test -- --watchAll=false// package.json
{
"scripts": {
"prepare": "husky",
"lint": "eslint src/",
"format": "prettier --write src/",
"test": "jest",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"@commitlint/cli": "^18.0.0",
"@commitlint/config-conventional": "^18.0.0"
},
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css}": ["prettier --write"]
}
}# .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-added-large-files
args: ['--maxkb=1000']
- repo: https://github.com/psf/black
rev: 24.1.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8# 安装
pip install pre-commit
pre-commit install
# 对所有文件运行
pre-commit run --all-files
# 更新 hooks 版本
pre-commit autoupdate# lefthook.yml
pre-commit:
parallel: true
commands:
lint:
glob: "*.{ts,tsx,js,jsx}"
run: npx eslint {staged_files}
format:
glob: "*.{ts,tsx,js,jsx,css,json,md}"
run: npx prettier --check {staged_files}
typecheck:
run: npx tsc --noEmit
commit-msg:
commands:
commitlint:
run: npx commitlint --edit {1}
pre-push:
commands:
test:
run: npm test -- --watchAll=false# 安装
# Windows: scoop install lefthook
# macOS: brew install lefthook
lefthook install#!/bin/bash
# .git/hooks/pre-commit 或 .husky/pre-commit
# 检查暂存文件中是否有敏感信息
STAGED=$(git diff --cached --name-only --diff-filter=d)
PATTERNS=(
"AKIA[0-9A-Z]{16}" # AWS Access Key
"password\s*=\s*['\"][^'\"]+['\"]" # 硬编码密码
"-----BEGIN (RSA |DSA |EC )?PRIVATE KEY-----" # 私钥
"sk_live_[a-zA-Z0-9]{24,}" # Stripe 密钥
"ghp_[a-zA-Z0-9]{36}" # GitHub PAT
"xox[baprs]-[a-zA-Z0-9-]+" # Slack Token
)
FOUND=0
for file in $STAGED; do
for pattern in "${PATTERNS[@]}"; do
if git diff --cached "$file" | grep -qE "$pattern"; then
echo "❌ 在 $file 中发现疑似敏感信息: $pattern"
FOUND=1
fi
done
done
if [ "$FOUND" -eq 1 ]; then
echo ""
echo "请将敏感信息移到环境变量或 .env 文件(已加入 .gitignore)"
echo "如确认安全,使用 git commit --no-verify 跳过检查"
exit 1
fi#!/bin/bash
# .git/hooks/post-commit
# 从 Conventional Commits 自动提取变更日志
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
RANGE="HEAD"
else
RANGE="$LAST_TAG..HEAD"
fi
echo "# Changelog (since $LAST_TAG)" > CHANGELOG-DRAFT.md
echo "" >> CHANGELOG-DRAFT.md
echo "## Features" >> CHANGELOG-DRAFT.md
git log $RANGE --oneline --grep="^feat" --format="- %s (%h)" >> CHANGELOG-DRAFT.md
echo "" >> CHANGELOG-DRAFT.md
echo "## Bug Fixes" >> CHANGELOG-DRAFT.md
git log $RANGE --oneline --grep="^fix" --format="- %s (%h)" >> CHANGELOG-DRAFT.md
echo "" >> CHANGELOG-DRAFT.md
echo "## Other Changes" >> CHANGELOG-DRAFT.md
git log $RANGE --oneline --grep="^docs\|^chore\|^refactor\|^perf" --format="- %s (%h)" >> CHANGELOG-DRAFT.md#!/bin/bash
# .git/hooks/post-merge
# 检查是否有新的数据库迁移文件
CHANGED=$(git diff HEAD@{1} --name-only --diff-filter=A | grep "migrations/" || true)
if [ -n "$CHANGED" ]; then
echo "🗄️ 检测到新的数据库迁移:"
echo "$CHANGED"
echo ""
echo "请运行以下命令应用迁移:"
echo " python manage.py migrate"
echo " # 或"
echo " npx prisma migrate deploy"
fi#!/bin/bash
# .git/hooks/pre-push
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# 允许的分支名模式
PATTERN="^(main|develop|release/.*|feature/.*|fix/.*|hotfix/.*|chore/.*|docs/.*)$"
if ! echo "$BRANCH" | grep -qE "$PATTERN"; then
echo "❌ 分支名 '$BRANCH' 不符合命名规范"
echo ""
echo "允许的格式:"
echo " feature/描述 — 功能开发"
echo " fix/描述 — Bug 修复"
echo " hotfix/描述 — 紧急修复"
echo " release/v* — 发布分支"
echo " chore/描述 — 杂务"
echo " docs/描述 — 文档"
exit 1
fi#!/usr/bin/env pwsh
# .git/hooks/pre-commit(PowerShell 版本)
$staged = git diff --cached --name-only --diff-filter=d | Where-Object { $_ -match '\.(ts|tsx|js|jsx)$' }
if ($staged) {
Write-Host "🔍 运行 ESLint..." -ForegroundColor Cyan
npx eslint $staged
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ ESLint 检查失败" -ForegroundColor Red
exit 1
}
Write-Host "🎨 运行 Prettier..." -ForegroundColor Cyan
npx prettier --check $staged
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ 格式检查失败" -ForegroundColor Red
exit 1
}
}
Write-Host "✅ 检查通过" -ForegroundColor Green
exit 0# 1. 手动运行 hook 脚本
./.git/hooks/pre-commit
# 2. 查看 hook 执行过程
GIT_TRACE=1 git commit -m "test"
# 3. 临时跳过 hooks
git commit --no-verify -m "紧急修复,跳过检查"
git push --no-verify
# 4. 检查 hook 是否可执行
ls -la .git/hooks/pre-commit
# -rwxr-xr-x pre-commit ← 需要 x 权限
# 5. Windows 上的 shebang
# 第一行必须指定解释器
#!/bin/bash # Bash(推荐)
#!/usr/bin/env pwsh # PowerShell
#!/usr/bin/env python3 # Python
#!/usr/bin/env node # Node.js# 问题:pre-commit hook 运行太慢(lint 整个项目)
# 解决:只检查暂存的文件
# ❌ 慢:检查所有文件
npx eslint src/
# ✅ 快:只检查暂存的文件
npx eslint $(git diff --cached --name-only --diff-filter=d -- '*.ts' '*.tsx')
# ✅✅ 更快:使用 lint-staged(自动处理)
npx lint-staged
# 并行运行多个检查
(npx eslint --cache . &)
(npx tsc --noEmit &)
wait# 将 hooks 放在版本控制的目录中
git config core.hooksPath .githooks
# 项目结构
# .githooks/
# pre-commit
# commit-msg
# pre-push
# 这样 hooks 就跟随代码提交,团队共享
# 无需 Husky 等工具# .git/hooks/ 中的文件不会被 git add
# 新成员克隆后没有任何 hooks!
# ✅ 解决方案(选其一):
# 1. 使用 Husky(.husky/ 目录受版本控制)
# 2. 使用 core.hooksPath 指向项目中的目录
# 3. 使用 pre-commit(Python)
# 4. 提供安装脚本--no-verify 被滥用# ❌ 养成跳过 hooks 的习惯
git commit --no-verify -m "快速提交"
# ✅ 只在真正紧急时使用
# ✅ 服务端 hooks 无法跳过(最后一道防线)# Linux/macOS 上忘记 chmod
git commit -m "test"
# hook 被忽略,没有任何提示!
# ✅ 确保权限
chmod +x .git/hooks/pre-commit
chmod +x .githooks/*# Hook 的工作目录是仓库根目录
# ❌ 使用相对路径可能出错
cd src && npx eslint .
# ✅ 始终使用仓库根目录的路径
npx eslint src/# ❌ pre-commit 中运行完整测试套件(几分钟)
npm test # 200 个测试,3 分钟
# ✅ pre-commit 只做快速检查(< 10 秒)
npx lint-staged # 只检查暂存文件
# ✅ 完整测试放在 pre-push 或 CI 中可以。--no-verify 可以跳过 pre-commit、commit-msg、pre-push 等客户端 hooks。因此,关键规则应该在服务端 hooks(如 GitHub Branch Protection)中强制。
| 工具 | 语言生态 | 优势 |
|---|---|---|
| Husky | Node.js | 社区最大,配合 lint-staged |
| pre-commit | Python | 丰富的预置 hooks |
| lefthook | Go(跨语言) | 速度快,配置简单 |
根据项目的主要技术栈选择即可。
# git diff --cached 查看暂存区的变更
git diff --cached --name-only # 暂存的文件列表
git diff --cached # 暂存的具体变更
git diff --cached --diff-filter=A # 新增的文件
git diff --cached --diff-filter=M # 修改的文件
git diff --cached --diff-filter=D # 删除的文件#!/usr/bin/env node
// .git/hooks/commit-msg
const fs = require('fs');
const msg = fs.readFileSync(process.argv[2], 'utf8').trim();
const pattern = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,100}$/;
if (!pattern.test(msg.split('\n')[0])) {
console.error('❌ 提交消息格式不符合规范');
process.exit(1);
}| 目标 | Hook | 可阻止? | 常见用途 |
|---|---|---|---|
| 提交前检查 | pre-commit | ✅ | lint、格式化、安全检查 |
| 修改提交消息 | prepare-commit-msg | ✅ | 自动添加 Issue 编号 |
| 验证提交消息 | commit-msg | ✅ | 强制 Conventional Commits |
| 提交后通知 | post-commit | ❌ | 通知、日志 |
| 推送前检查 | pre-push | ✅ | 测试、分支保护 |
| 分支切换后 | post-checkout | ❌ | 自动安装依赖 |
| 合并后 | post-merge | ❌ | 自动安装依赖、迁移提醒 |
| 服务端接收前 | pre-receive | ✅ | 强制策略、权限控制 |
| 服务端接收后 | post-receive | ❌ | 自动部署、通知 |
core.hooksPath 配置 hooks 目录