Appearance
Git Hooks
概述
Git Hooks 是 Git 提供的脚本机制,允许在特定事件发生时自动执行自定义脚本。通过 Hooks,可以在提交、推送等操作前后自动执行代码检查、测试、部署等任务。
什么是 Git Hooks
Git Hooks 是存储在 .git/hooks 目录下的脚本文件,当特定 Git 事件触发时自动执行。
Hooks 的特点
- 自动化:无需手动执行,事件触发时自动运行
- 可定制:可以使用任何脚本语言编写
- 本地执行:在开发者本地机器上运行
- 非强制:可以通过
--no-verify跳过
Hooks 目录结构
bash
.git/
└── hooks/
├── applypatch-msg.sample
├── pre-applypatch.sample
├── post-applypatch.sample
├── pre-commit.sample
├── pre-merge-commit.sample
├── prepare-commit-msg.sample
├── commit-msg.sample
├── post-commit.sample
├── pre-rebase.sample
├── post-checkout.sample
├── post-merge.sample
├── pre-push.sample
├── pre-receive.sample
├── update.sample
├── post-receive.sample
├── post-update.sample
├── push-to-checkout.sample
└── pre-auto-gc.sample客户端钩子
客户端钩子在开发者的本地仓库中执行,用于提交和合并等操作。
提交相关钩子
pre-commit
在提交信息编辑前执行,用于检查即将提交的内容。
bash
#!/bin/bash
# .git/hooks/pre-commit
# 检查是否有调试代码
if git diff --cached | grep -E 'console\.(log|debug)|debugger'; then
echo "错误: 发现调试代码,请移除后再提交"
exit 1
fi
# 检查是否有 TODO
if git diff --cached | grep -E 'TODO|FIXME'; then
echo "警告: 发现 TODO/FIXME 注释"
fi
# 检查代码风格
npm run lint
exit $?prepare-commit-msg
在提交信息编辑器启动前执行,用于生成默认提交信息。
bash
#!/bin/bash
# .git/hooks/prepare-commit-msg
# 获取当前分支名
BRANCH=$(git branch --show-current)
# 如果是 feature 分支,添加分支信息
if [[ $BRANCH == feature/* ]]; then
echo "[$BRANCH] " > "$1"
fi
# 如果是 merge 提交,不修改
if [ "$2" = "merge" ]; then
exit 0
ficommit-msg
在提交信息编辑后执行,用于验证提交信息格式。
bash
#!/bin/bash
# .git/hooks/commit-msg
# 读取提交信息
COMMIT_MSG=$(cat "$1")
# 检查提交信息格式
# 要求格式: type(scope): description
PATTERN='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}'
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "错误: 提交信息格式不正确"
echo "正确格式: type(scope): description"
echo "类型: feat, fix, docs, style, refactor, test, chore"
echo "示例: feat(auth): 添加用户登录功能"
exit 1
fi
exit 0post-commit
在提交完成后执行。
bash
#!/bin/bash
# .git/hooks/post-commit
# 获取提交信息
COMMIT_MSG=$(git log -1 --pretty=%B)
# 如果包含特定关键词,触发通知
if echo "$COMMIT_MSG" | grep -q "WIP"; then
echo "提示: 这是一个 WIP 提交"
fi
# 自动推送(可选)
# git push origin HEAD合并相关钩子
pre-merge-commit
在合并提交前执行。
bash
#!/bin/bash
# .git/hooks/pre-merge-commit
# 检查是否允许合并
BRANCH=$(git branch --show-current)
if [[ $BRANCH == "main" || $BRANCH == "master" ]]; then
echo "错误: 不允许直接在 main 分支上合并"
exit 1
fi
exit 0post-merge
在合并完成后执行。
bash
#!/bin/bash
# .git/hooks/post-merge
# 合并后自动安装依赖
if [ -f "package.json" ]; then
echo "检测到 package.json 变化,正在安装依赖..."
npm install
fi
if [ -f "requirements.txt" ]; then
echo "检测到 requirements.txt 变化,正在安装依赖..."
pip install -r requirements.txt
fi推送相关钩子
pre-push
在推送前执行。
bash
#!/bin/bash
# .git/hooks/pre-push
# 获取推送的分支
while read local_ref local_oid remote_ref remote_oid; do
BRANCH=$(echo "$remote_ref" | sed 's|refs/heads/||')
# 不允许直接推送到 main
if [[ $BRANCH == "main" || $BRANCH == "master" ]]; then
echo "错误: 不允许直接推送到 $BRANCH 分支"
echo "请通过 Pull Request 合并"
exit 1
fi
done
exit 0其他客户端钩子
post-checkout
在切换分支后执行。
bash
#!/bin/bash
# .git/hooks/post-checkout
# $1: 之前的 HEAD
# $2: 新的 HEAD
# $3: 是否是分支切换 (1) 或文件检出 (0)
if [ "$3" = "1" ]; then
# 切换分支后更新依赖
if [ -f "package.json" ]; then
npm install --quiet
fi
# 清理缓存
if [ -d "node_modules/.cache" ]; then
rm -rf node_modules/.cache
fi
fipre-rebase
在变基前执行。
bash
#!/bin/bash
# .git/hooks/pre-rebase
# 不允许变基 main 分支
if [ "$1" = "main" ] || [ "$1" = "master" ]; then
echo "错误: 不允许变基 main/master 分支"
exit 1
fi
exit 0服务端钩子
服务端钩子在远程仓库中执行,用于强制执行策略。
pre-receive
在接收推送前执行。
bash
#!/bin/bash
# 服务端: .git/hooks/pre-receive
while read oldrev newrev refname; do
BRANCH=$(echo "$refname" | sed 's|refs/heads/||')
# 不允许删除 main 分支
if [[ $BRANCH == "main" && $newrev == "0000000000000000000000000000000000000000" ]]; then
echo "错误: 不允许删除 main 分支"
exit 1
fi
# 检查提交信息格式
if [[ $BRANCH != "main" ]]; then
for commit in $(git rev-list $oldrev..$newrev); do
MSG=$(git log -1 --pretty=%B $commit)
if ! echo "$MSG" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: '; then
echo "错误: 提交 $commit 信息格式不正确"
exit 1
fi
done
fi
done
exit 0update
在每个引用更新前执行。
bash
#!/bin/bash
# 服务端: .git/hooks/update
# $1: 引用名称
# $2: 旧对象
# $3: 新对象
REFNAME="$1"
OLDREV="$2"
NEWREV="$3"
BRANCH=$(echo "$REFNAME" | sed 's|refs/heads/||')
# 不允许强制推送到 main
if [[ $BRANCH == "main" ]]; then
# 检查是否是强制推送
if ! git merge-base --is-ancestor $OLDREV $NEWREV; then
echo "错误: 不允许强制推送到 main 分支"
exit 1
fi
fi
exit 0post-receive
在推送完成后执行。
bash
#!/bin/bash
# 服务端: .git/hooks/post-receive
while read oldrev newrev refname; do
BRANCH=$(echo "$refname" | sed 's|refs/heads/||')
# 推送到 main 后自动部署
if [[ $BRANCH == "main" ]]; then
echo "正在部署..."
cd /var/www/project
git pull origin main
npm install --production
npm run build
sudo systemctl restart nginx
echo "部署完成"
fi
done钩子脚本示例
完整的 pre-commit 示例
bash
#!/bin/bash
# .git/hooks/pre-commit
set -e
echo "运行 pre-commit 检查..."
# 1. 检查调试代码
echo "检查调试代码..."
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$')
if [ -n "$FILES" ]; then
if grep -l "console\.\(log\|debug\)\|debugger" $FILES; then
echo "错误: 发现调试代码"
exit 1
fi
fi
# 2. 检查代码风格
echo "检查代码风格..."
if [ -f "package.json" ]; then
npm run lint -- --quiet
fi
# 3. 运行测试
echo "运行测试..."
if [ -f "package.json" ]; then
npm test -- --passWithNoTests
fi
# 4. 检查文件大小
echo "检查文件大小..."
MAX_SIZE=1048576 # 1MB
LARGE_FILES=$(git diff --cached --name-only | xargs -I {} du -b {} 2>/dev/null | awk -v max=$MAX_SIZE '$1 > max {print $2}')
if [ -n "$LARGE_FILES" ]; then
echo "警告: 以下文件超过 1MB:"
echo "$LARGE_FILES"
fi
echo "所有检查通过!"
exit 0完整的 commit-msg 示例
bash
#!/bin/bash
# .git/hooks/commit-msg
COMMIT_MSG=$(cat "$1")
FIRST_LINE=$(echo "$COMMIT_MSG" | head -n1)
# 检查第一行长度
if [ ${#FIRST_LINE} -gt 72 ]; then
echo "错误: 提交信息第一行不能超过 72 个字符"
exit 1
fi
# 检查提交信息格式
PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,50}'
if ! echo "$FIRST_LINE" | grep -qE "$PATTERN"; then
echo "错误: 提交信息格式不正确"
echo ""
echo "格式: <type>(<scope>): <subject>"
echo ""
echo "类型 (type):"
echo " feat - 新功能"
echo " fix - 修复 Bug"
echo " docs - 文档更新"
echo " style - 代码格式调整"
echo " refactor - 重构代码"
echo " perf - 性能优化"
echo " test - 测试相关"
echo " build - 构建相关"
echo " ci - CI 配置"
echo " chore - 其他修改"
echo " revert - 回滚提交"
echo ""
echo "示例: feat(auth): 添加用户登录功能"
exit 1
fi
# 检查是否包含问题编号(可选)
# if ! echo "$COMMIT_MSG" | grep -qE "#[0-9]+"; then
# echo "提示: 建议在提交信息中包含问题编号,如 #123"
# fi
exit 0钩子管理
安装钩子
bash
# 方法一:直接创建
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
# 方法二:使用符号链接
ln -s ../../hooks/pre-commit .git/hooks/pre-commit
# 方法三:使用脚本安装
#!/bin/bash
for hook in hooks/*; do
ln -sf "../../$hook" ".git/hooks/$(basename $hook)"
done共享钩子
Git 钩子不会被 Git 跟踪,有几种方式可以共享:
方法一:使用 core.hooksPath
bash
# 在仓库根目录创建 hooks 目录
mkdir -p .githooks
# 配置 Git 使用该目录
git config core.hooksPath .githooks方法二:使用符号链接脚本
bash
#!/bin/bash
# setup-hooks.sh
HOOKS_DIR="$(git rev-parse --show-toplevel)/hooks"
GIT_HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
for hook in $HOOKS_DIR/*; do
hook_name=$(basename $hook)
ln -sf "$hook" "$GIT_HOOKS_DIR/$hook_name"
chmod +x "$GIT_HOOKS_DIR/$hook_name"
done
echo "Hooks installed successfully!"方法三:使用 Husky(Node.js 项目)
bash
npm install husky --save-dev
npx husky install
npx husky add .husky/pre-commit "npm test"总结
| 钩子类型 | 钩子名称 | 触发时机 | 用途 |
|---|---|---|---|
| 客户端 | pre-commit | 提交前 | 代码检查 |
| 客户端 | commit-msg | 提交信息编辑后 | 验证提交信息 |
| 客户端 | pre-push | 推送前 | 运行测试 |
| 客户端 | post-checkout | 切换分支后 | 更新依赖 |
| 服务端 | pre-receive | 接收推送前 | 强制策略 |
| 服务端 | update | 引用更新前 | 权限检查 |
| 服务端 | post-receive | 推送完成后 | 自动部署 |
使用建议:
- 使用钩子自动化代码质量检查
- 将钩子脚本纳入版本控制
- 使用工具(如 Husky)简化钩子管理
- 服务端钩子用于强制执行策略
