强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Git 完全指南 / 13 - Git Hooks:客户端钩子、服务端钩子、自动化

第十三章:Git Hooks

Git Hooks 让你在特定事件发生时自动执行脚本,是实现开发工作流自动化的基石。


13.1 Hooks 概述

Git Hooks 是存储在 .git/hooks/ 目录下的脚本,在特定 Git 操作触发时自动执行。

# 查看 hooks 目录
$ ls -la .git/hooks/
total 52
-rwxr-xr-x 1 user user  478 Jan 10 10:00 applypatch-msg.sample
-rwxr-xr-x 1 user user  896 Jan 10 10:00 commit-msg.sample
-rwxr-xr-x 1 user user 4726 Jan 10 10:00 fsmonitor-watchman.sample
-rwxr-xr-x 1 user user  189 Jan 10 10:00 post-update.sample
-rwxr-xr-x 1 user user  424 Jan 10 10:00 pre-applypatch.sample
-rwxr-xr-x 1 user user 1643 Jan 10 10:00 pre-commit.sample
-rwxr-xr-x 1 user user  416 Jan 10 10:00 pre-merge-commit.sample
-rwxr-xr-x 1 user user 1374 Jan 10 10:00 pre-push.sample
-rwxr-xr-x 1 user user 4898 Jan 10 10:00 pre-rebase.sample
-rwxr-xr-x 1 user user  544 Jan 10 10:00 pre-receive.sample
-rwxr-xr-x 1 user user 1239 Jan 10 10:00 prepare-commit-msg.sample
-rwxr-xr-x 1 user user 3610 Jan 10 10:00 update.sample

⚠️ 只有不带 .sample 后缀的 hook 才会被执行。


13.2 客户端 Hooks

13.2.1 pre-commit — 提交前检查

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

# 运行代码检查
echo "Running lint..."
npm run lint
if [ $? -ne 0 ]; then
    echo "❌ Lint failed. Commit aborted."
    exit 1
fi

# 运行测试
echo "Running tests..."
npm test
if [ $? -ne 0 ]; then
    echo "❌ Tests failed. Commit aborted."
    exit 1
fi

# 检查是否包含调试语句
if git diff --cached --name-only | xargs grep -l "console.log\|debugger\|TODO" 2>/dev/null; then
    echo "⚠️  Warning: Found debug statements or TODOs"
    # exit 1  # 取消注释则阻止提交
fi

echo "✅ All pre-commit checks passed."

13.2.2 commit-msg — 提交信息规范

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

# 检查提交信息格式(Conventional Commits)
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,72}"

if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then
    echo "❌ Invalid commit message format."
    echo ""
    echo "Expected: <type>(<scope>): <description>"
    echo "Example:  feat(auth): add login page"
    echo ""
    echo "Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert"
    exit 1
fi

echo "✅ Commit message format is valid."

13.2.3 prepare-commit-msg — 修改提交信息

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

# 自动添加分支名到提交信息
branch_name=$(git symbolic-ref --short HEAD)
commit_file="$1"

if [[ "$branch_name" =~ ^(feature|bugfix|hotfix)/([A-Z]+-[0-9]+) ]]; then
    issue_id="${BASH_REMATCH[2]}"
    sed -i.bak -e "1s/^/[$issue_id] /" "$commit_file"
fi

13.2.4 pre-push — 推送前检查

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

# 检查是否有未运行的测试
untested=$(git log --oneline origin/main..HEAD --no-merges | head -5)
if [ -n "$untested" ]; then
    echo "⚠️  The following commits haven't been tested:"
    echo "$untested"
    read -p "Continue with push? [y/N] " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi

13.2.5 post-checkout / post-merge

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

# 切换分支后自动安装依赖
if [ -f package.json ]; then
    echo "📦 Checking dependencies..."
    npm install --silent
fi

# 检查是否有数据库迁移
if [ -d migrations ]; then
    echo "🔄 Checking for pending migrations..."
    npm run migrate:status
fi
#!/bin/bash
# .git/hooks/post-merge

# 合并后自动安装依赖
if [ -f package.json ]; then
    changed_files=$(git diff HEAD@{1} --name-only)
    if echo "$changed_files" | grep -q "package.json\|package-lock.json"; then
        echo "📦 Dependencies changed, running npm install..."
        npm install
    fi
fi

13.3 服务端 Hooks

13.3.1 pre-receive — 接收前检查

#!/bin/bash
# hooks/pre-receive

while read oldrev newrev refname; do
    # 禁止强制推送到 main
    if [ "$refname" = "refs/heads/main" ]; then
        if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
            commits=$(git rev-list "$oldrev".."$newrev" --count)
            if [ "$commits" -gt 0 ]; then
                # 检查是否为 force push
                merge_base=$(git merge-base "$oldrev" "$newrev")
                if [ "$merge_base" != "$oldrev" ]; then
                    echo "❌ Force push to main is not allowed!"
                    exit 1
                fi
            fi
        fi
    fi

    # 检查提交信息格式
    for commit in $(git rev-list "$oldrev".."$newrev"); do
        msg=$(git log -1 --format="%s" "$commit")
        if ! echo "$msg" | grep -qE "^(feat|fix|docs|chore|refactor):"; then
            echo "❌ Invalid commit message: $msg"
            echo "   Expected format: type(scope): description"
            exit 1
        fi
    done
done

exit 0

13.3.2 update — 分支级检查

#!/bin/bash
# hooks/update

refname="$1"
oldrev="$2"
newrev="$3"

# 禁止删除受保护分支
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
    echo "❌ Deleting $refname is not allowed!"
    exit 1
fi

# 限制单次推送的文件数量
file_count=$(git diff --name-only "$oldrev" "$newrev" | wc -l)
if [ "$file_count" -gt 100 ]; then
    echo "❌ Too many files changed ($file_count). Maximum is 100."
    exit 1
fi

13.3.3 post-receive — 推送后操作

#!/bin/bash
# hooks/post-receive

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

    # 推送到 main 时触发部署
    if [ "$branch" = "main" ]; then
        echo "🚀 Deploying main branch..."
        cd /var/www/production
        git pull origin main
        npm install --production
        pm2 restart app
    fi

    # 推送到 staging 时触发测试部署
    if [ "$branch" = "staging" ]; then
        echo "🧪 Deploying to staging..."
        cd /var/www/staging
        git pull origin staging
        npm install
        npm test
        pm2 restart staging-app
    fi
done

13.4 Hooks 管理工具

13.4.1 Husky(Node.js 项目)

# 安装 husky
$ npm install husky --save-dev

# 初始化 husky
$ npx husky init

# 添加 pre-commit hook
$ npx husky add .husky/pre-commit "npm run lint && npm test"

# 添加 commit-msg hook
$ npx husky add .husky/commit-msg 'npx commitlint --edit "$1"'
// package.json
{
  "scripts": {
    "prepare": "husky"
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "@commitlint/cli": "^19.0.0",
    "@commitlint/config-conventional": "^19.0.0"
  }
}

13.4.2 使用 .githooks 目录

# 配置自定义 hooks 目录
$ git config core.hooksPath .githooks

# 在项目根目录创建 .githooks 目录
$ mkdir .githooks
$ cat > .githooks/pre-commit << 'EOF'
#!/bin/bash
npm run lint
EOF
$ chmod +x .githooks/pre-commit

13.5 Hooks 完整列表

Hook触发时机类型退出码影响
pre-commitgit commit客户端非 0 阻止提交
prepare-commit-msg编辑器打开前客户端-
commit-msg提交信息写入后客户端非 0 阻止提交
post-commit提交完成后客户端-
pre-rebase变基前客户端非 0 阻止变基
post-checkoutcheckout 后客户端-
post-merge合并后客户端-
pre-pushpush 前客户端非 0 阻止推送
pre-receive服务端接收前服务端非 0 拒绝推送
update每个分支更新前服务端非 0 拒绝该分支
post-receive接收完成后服务端-

业务场景

场景推荐 Hook工具
代码风格检查pre-commiteslint, prettier, black
提交信息规范commit-msgcommitlint
运行测试pre-commitjest, pytest
阻止直接推送到 mainpre-push / pre-receive自定义脚本
自动部署post-receiveCI/CD 脚本
自动安装依赖post-checkout, post-mergenpm install

扩展阅读


🔗 上一章12 - 工作树 | 下一章14 - Git LFS