Skip to content

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
fi

commit-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 0

post-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 0

post-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
fi

pre-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 0

update

在每个引用更新前执行。

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 0

post-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)简化钩子管理
  • 服务端钩子用于强制执行策略