Skip to content

自定义钩子

概述

自定义 Git Hooks 允许你根据项目需求编写特定的自动化脚本。本章将介绍如何编写实用的钩子脚本,以及推荐一些常用的钩子工具。

编写钩子脚本

基本原则

bash
#!/bin/bash
# 钩子脚本的基本结构

# 1. 设置错误时退出
set -e

# 2. 输出提示信息
echo "正在执行钩子检查..."

# 3. 执行检查逻辑
# ...

# 4. 返回结果
exit 0  # 成功
# exit 1  # 失败

获取 Git 信息

bash
#!/bin/bash

# 获取当前分支
BRANCH=$(git branch --show-current)
echo "当前分支: $BRANCH"

# 获取暂存的文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
echo "暂存的文件:"
echo "$STAGED_FILES"

# 获取暂存文件的内容差异
DIFF=$(git diff --cached)
echo "差异内容:"
echo "$DIFF"

# 获取最近一次提交信息
LAST_MSG=$(git log -1 --pretty=%B)
echo "最近提交: $LAST_MSG"

# 获取提交哈希
COMMIT_HASH=$(git rev-parse HEAD)
echo "提交哈希: $COMMIT_HASH"

颜色输出

bash
#!/bin/bash

# 定义颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 使用颜色
echo -e "${GREEN}成功: 检查通过${NC}"
echo -e "${YELLOW}警告: 请注意${NC}"
echo -e "${RED}错误: 检查失败${NC}"

pre-commit 示例

检查代码风格

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

set -e

echo "检查代码风格..."

# 获取暂存的 JS/TS 文件
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$' || true)

if [ -z "$FILES" ]; then
    echo "没有需要检查的文件"
    exit 0
fi

# 运行 ESLint
echo "运行 ESLint..."
npx eslint $FILES --max-warnings=0

echo "代码风格检查通过!"
exit 0

检查调试代码

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

echo "检查调试代码..."

# 获取暂存的文件
FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [ -z "$FILES" ]; then
    exit 0
fi

# 检查调试代码
PATTERNS=(
    "console\.log"
    "console\.debug"
    "debugger"
    "print\("
    "var_dump"
    "dd\("
)

FOUND=0
for pattern in "${PATTERNS[@]}"; do
    MATCHES=$(git diff --cached -S "$pattern" --name-only)
    if [ -n "$MATCHES" ]; then
        echo "发现调试代码模式: $pattern"
        echo "文件: $MATCHES"
        FOUND=1
    fi
done

if [ $FOUND -eq 1 ]; then
    echo ""
    echo "错误: 请移除调试代码后再提交"
    exit 1
fi

exit 0

检查文件命名

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

echo "检查文件命名规范..."

# 获取暂存的文件
FILES=$(git diff --cached --name-only --diff-filter=ACM)

ERRORS=0

for FILE in $FILES; do
    # 检查文件名是否包含空格
    if [[ "$FILE" =~ [[:space:]] ]]; then
        echo "错误: 文件名包含空格 - $FILE"
        ERRORS=$((ERRORS + 1))
    fi
    
    # 检查文件名是否包含中文
    if [[ "$FILE" =~ [\u4e00-\u9fa5] ]]; then
        echo "错误: 文件名包含中文 - $FILE"
        ERRORS=$((ERRORS + 1))
    fi
    
    # 检查文件名是否以小写字母开头
    BASENAME=$(basename "$FILE")
    if [[ ! "$BASENAME" =~ ^[a-z] ]]; then
        echo "警告: 文件名应以小写字母开头 - $FILE"
    fi
done

if [ $ERRORS -gt 0 ]; then
    exit 1
fi

exit 0

检查敏感信息

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

echo "检查敏感信息..."

# 敏感信息模式
PATTERNS=(
    "password\s*=\s*['\"][^'\"]+['\"]"
    "api[_-]?key\s*=\s*['\"][^'\"]+['\"]"
    "secret[_-]?key\s*=\s*['\"][^'\"]+['\"]"
    "token\s*=\s*['\"][^'\"]+['\"]"
    "-----BEGIN.*PRIVATE KEY-----"
    "aws_access_key_id"
    "aws_secret_access_key"
)

DIFF=$(git diff --cached)

FOUND=0
for pattern in "${PATTERNS[@]}"; do
    if echo "$DIFF" | grep -qiE "$pattern"; then
        echo "警告: 可能包含敏感信息 - $pattern"
        FOUND=1
    fi
done

if [ $FOUND -eq 1 ]; then
    echo ""
    echo "错误: 检测到可能的敏感信息,请检查后再提交"
    echo "如果确认不是敏感信息,可以使用 --no-verify 跳过此检查"
    exit 1
fi

exit 0

运行测试

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

echo "运行测试..."

# 只在关键文件变化时运行测试
CHANGED_FILES=$(git diff --cached --name-only)

if echo "$CHANGED_FILES" | grep -qE '\.(js|ts|jsx|tsx)$'; then
    echo "检测到代码变化,运行测试..."
    npm test -- --passWithNoTests --silent
fi

exit 0

pre-push 示例

防止推送到受保护分支

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

PROTECTED_BRANCHES=("main" "master" "develop" "staging")

while read local_ref local_oid remote_ref remote_oid; do
    BRANCH=$(echo "$remote_ref" | sed 's|refs/heads/||')
    
    for protected in "${PROTECTED_BRANCHES[@]}"; do
        if [[ "$BRANCH" == "$protected" ]]; then
            echo "错误: 不允许直接推送到 $BRANCH 分支"
            echo "请通过 Pull Request 进行合并"
            exit 1
        fi
    done
done

exit 0

推送前运行完整测试

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

echo "推送前运行完整测试..."

# 获取推送的分支
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,运行完整测试..."
        npm run test:full
        
        if [ $? -ne 0 ]; then
            echo "错误: 测试失败,不允许推送"
            exit 1
        fi
    fi
done

exit 0

检查远程分支状态

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/||')
    
    # 获取远程分支的最新状态
    git fetch origin $BRANCH 2>/dev/null
    
    REMOTE_OID=$(git rev-parse origin/$BRANCH 2>/dev/null || echo "")
    
    if [ -n "$REMOTE_OID" ] && [ "$REMOTE_OID" != "$local_oid" ]; then
        # 检查本地是否包含远程的所有提交
        if ! git merge-base --is-ancestor $REMOTE_OID $local_oid; then
            echo "警告: 远程分支 $BRANCH 有新的提交"
            echo "请先拉取远程更改: git pull origin $BRANCH"
            echo ""
            read -p "是否继续推送? (y/N) " -n 1 -r
            echo
            if [[ ! $REPLY =~ ^[Yy]$ ]]; then
                exit 1
            fi
        fi
    fi
done

exit 0

其他实用钩子

post-merge 示例

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

echo "合并完成,检查依赖更新..."

# 检查 package.json 是否变化
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "package.json"; then
    echo "package.json 已更新,安装依赖..."
    npm install
fi

# 检查 package-lock.json 是否变化
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "package-lock.json"; then
    echo "package-lock.json 已更新,同步依赖..."
    npm ci
fi

# 检查 composer.json 是否变化
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "composer.json"; then
    echo "composer.json 已更新,安装依赖..."
    composer install
fi

# 检查数据库迁移
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "migrations/"; then
    echo "检测到数据库迁移文件,请手动执行迁移"
fi

exit 0

post-checkout 示例

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

# $1: 之前的 HEAD
# $2: 新的 HEAD  
# $3: 是否是分支切换 (1) 或文件检出 (0)

PREV_HEAD=$1
NEW_HEAD=$2
BRANCH_SWITCH=$3

if [ "$BRANCH_SWITCH" = "1" ]; then
    echo "切换分支完成"
    
    # 更新依赖
    if [ -f "package.json" ]; then
        echo "检查依赖更新..."
        npm install --prefer-offline --silent
    fi
    
    # 清理构建缓存
    if [ -d "node_modules/.cache" ]; then
        echo "清理构建缓存..."
        rm -rf node_modules/.cache
    fi
    
    # 更新环境配置
    if [ -f ".env.example" ] && [ ! -f ".env" ]; then
        echo "复制环境配置模板..."
        cp .env.example .env
    fi
fi

exit 0

prepare-commit-msg 示例

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

# $1: 包含提交信息的文件路径
# $2: 提交来源 (message, template, merge, squash, commit)
# $3: 相关提交的 SHA (amend 时)

COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2

# 如果已经有提交信息,不修改
if [ "$COMMIT_SOURCE" = "message" ] || [ "$COMMIT_SOURCE" = "merge" ]; then
    exit 0
fi

# 获取当前分支
BRANCH=$(git branch --show-current)

# 从分支名提取问题编号
# 例如: feature/ABC-123-new-feature -> ABC-123
ISSUE=$(echo "$BRANCH" | grep -oE '[A-Z]+-[0-9]+' | head -1)

# 如果找到问题编号,添加到提交信息
if [ -n "$ISSUE" ]; then
    echo "" >> "$COMMIT_MSG_FILE"
    echo "" >> "$COMMIT_MSG_FILE"
    echo "Refs: $ISSUE" >> "$COMMIT_MSG_FILE"
fi

exit 0

钩子工具推荐

Husky

Node.js 项目最流行的 Git Hooks 管理工具。

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

# 初始化
npx husky install

# 添加钩子
npx husky add .husky/pre-commit "npm test"

# package.json 配置
{
  "scripts": {
    "prepare": "husky install"
  }
}

配置示例

bash
# .husky/pre-commit
#!/bin/bash
. "$(dirname "$0")/_/husky.sh"

npm run lint
npm test
bash
# .husky/commit-msg
#!/bin/bash
. "$(dirname "$0")/_/husky.sh"

npx --no -- commitlint --edit "$1"

lint-staged

只检查暂存的文件,提高效率。

bash
# 安装
npm install lint-staged --save-dev

# package.json 配置
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

# 配合 Husky
# .husky/pre-commit
npx lint-staged

commitlint

验证提交信息格式。

bash
# 安装
npm install @commitlint/cli @commitlint/config-conventional --save-dev

# 配置
# 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', 50],
    'body-max-line-length': [2, 'always', 72]
  }
}

# 配合 Husky
# .husky/commit-msg
npx --no -- commitlint --edit "$1"

pre-commit

Python 项目的通用钩子框架。

bash
# 安装
pip install pre-commit

# 配置
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-json
      - id: check-merge-conflict

  - repo: https://github.com/psf/black
    rev: 23.1.0
    hooks:
      - id: black

  - repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
      - id: isort

# 安装钩子
pre-commit install

# 手动运行
pre-commit run --all-files

simple-git-hooks

轻量级的 Git Hooks 管理工具。

bash
# 安装
npm install simple-git-hooks --save-dev

# package.json 配置
{
  "simple-git-hooks": {
    "pre-commit": "npm run lint",
    "commit-msg": "node scripts/validate-commit-msg.js $1"
  },
  "scripts": {
    "postinstall": "simple-git-hooks"
  }
}

钩子脚本模板

通用 pre-commit 模板

bash
#!/bin/bash
# .githooks/pre-commit

set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

echo -e "${GREEN}运行 pre-commit 检查...${NC}"

# 获取暂存文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [ -z "$STAGED_FILES" ]; then
    exit 0
fi

# 1. 检查调试代码
echo "检查调试代码..."
DEBUG_PATTERNS="console\.(log|debug)|debugger|print\(|var_dump|dd\("
if git diff --cached | grep -qE "$DEBUG_PATTERNS"; then
    echo -e "${RED}错误: 发现调试代码${NC}"
    exit 1
fi

# 2. 检查代码风格
echo "检查代码风格..."
JS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|ts|jsx|tsx)$' || true)
if [ -n "$JS_FILES" ]; then
    npx eslint $JS_FILES --max-warnings=0 --quiet
fi

# 3. 检查敏感信息
echo "检查敏感信息..."
SENSITIVE_PATTERNS="password|secret|api_key|token"
if git diff --cached | grep -qiE "$SENSITIVE_PATTERNS"; then
    echo -e "${YELLOW}警告: 可能包含敏感信息${NC}"
fi

echo -e "${GREEN}所有检查通过!${NC}"
exit 0

总结

钩子用途推荐工具
pre-commit代码检查、格式化Husky + lint-staged
commit-msg提交信息验证Husky + commitlint
pre-push运行测试、保护分支Husky
post-merge更新依赖原生脚本
post-checkout分支切换后处理原生脚本

最佳实践

  • 使用工具管理钩子,避免手动配置
  • 只检查暂存的文件,提高效率
  • 提供清晰的错误信息和修复建议
  • 允许通过 --no-verify 跳过(紧急情况)