概述

代码质量问题越早发现,修复成本越低。在提交阶段拦截比在 CI 阶段拦截快,在 CI 阶段拦截比在生产环境出问题后修复快。Git Hooks 提供了在提交和推送的关键节点自动执行检查的能力——格式化代码、运行 lint、验证提交信息、执行测试。从 Git Hooks 原理到工具链实践,构建一套从本地到 CI 的代码质量自动化体系。

参考来源:Git Hooks 官方文档pre-commit 官网

一、Git Hooks 体系

1.1 Hooks 概览

Git Hooks 是 Git 在特定操作(如 commit、push、merge)发生时自动执行的脚本,存放在 .git/hooks/ 目录中:

Hook 名称触发时机常见用途是否可跳过
pre-commitcommit 执行前代码格式化、lint 检查--no-verify
prepare-commit-msg编辑提交信息前自动生成提交信息模板-
commit-msg提交信息写入后验证提交信息格式--no-verify
post-commitcommit 完成后通知、统计
pre-pushpush 执行前运行测试、防止推送到 protected 分支--no-verify
pre-merge-commitmerge 完成前合并前检查-
post-mergemerge 完成后恢复依赖、更新子模块
pre-rebaserebase 执行前防止 rebase 已推送的提交-
post-checkout切换分支后恢复环境

1.2 原生 Hook 示例

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

set -euo pipefail

echo "执行提交前检查..."

# 1. 检查是否有调试代码遗留
if git diff --cached | grep -E '^\+.*\b(debugger|console\.log|print\(|fmt\.Println)\b'; then
    echo "✗ 发现调试代码,请移除后再提交"
    exit 1
fi

# 2. 检查是否有 TODO/FIXME 未处理
if git diff --cached | grep -E '^\+.*\b(TODO|FIXME|HACK|XXX)\b'; then
    echo "⚠ 发现 TODO/FIXME,确认是否需要处理"
    # 不阻止提交,仅提醒
fi

# 3. 检查行尾空格
if git diff --cached | grep -E '^\+.*[ \t]+$'; then
    echo "✗ 发现行尾空格,请清理"
    # 自动修复
    git diff --cached --name-only | xargs sed -i 's/[[:space:]]*$//'
    echo "已自动清理行尾空格,请重新 git add"
    exit 1
fi

# 4. 检查文件权限
while IFS= read -r file; do
    if [[ -f "$file" ]] && [[ -x "$file" ]] && [[ "$file" != *.sh ]]; then
        echo "✗ $file 不应有执行权限"
        exit 1
    fi
done < <(git diff --cached --name-only --diff-filter=ACM)

echo "✓ 提交前检查通过"

1.3 原生 Hook 的局限

问题1:.git/hooks/ 不在版本控制中,无法团队共享
问题2:每个开发者需要手动安装 hook
问题3:Hook 之间的依赖和执行顺序难以管理
问题4:跨语言项目需要配置多种工具,手动管理复杂

解决方案就是使用 Hook 管理工具:pre-commit(Python 生态)和 Husky(Node.js 生态)。

二、pre-commit

2.1 安装

# 通过 pip 安装
pip install pre-commit

# macOS
brew install pre-commit

# 验证
pre-commit --version

2.2 配置文件

在项目根目录创建 .pre-commit-config.yaml

# .pre-commit-config.yaml
default_language_version:
  python: python3
  node: 20.0.0

# 排除路径
exclude: |
  (?x)^(
      vendor/.*|
      third_party/.*|
      .*\.pb\.go|
      .*\.gen\.go|
      docs/.*
  )$  

repos:
  # === 通用检查 ===
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace        # 清除行尾空格
      - id: end-of-file-fixer          # 确保文件以空行结尾
      - id: check-yaml                 # YAML 语法检查
      - id: check-json                 # JSON 语法检查
      - id: check-merge-conflict       # 检查合并冲突标记
      - id: check-added-large-files    # 防止提交大文件
        args: ['--maxkb=500']
      - id: check-case-conflict        # 检查文件名大小写冲突
      - id: check-symlinks             # 检查符号链接
      - id: detect-private-key         # 检测私钥
      - id: mixed-line-ending          # 统一行尾
        args: ['--fix=lf']
      - id: requirements-txt-fixer     # 修复 requirements.txt

  # === Python 项目 ===
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff                        # Lint
        args: ['--fix']
      - id: ruff-format                 # 格式化

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]
        args: ['--config-file=pyproject.toml']

  # === Go 项目 ===
  - repo: https://github.com/dnephin/pre-commit-golang
    rev: v0.5.1
    hooks:
      - id: go-fmt
      - id: go-imports
      - id: go-mod-tidy
      - id: go-unit-tests
        args: ['-short']

  - repo: https://github.com/golangci/golangci-lint
    rev: v1.59.0
    hooks:
      - id: golangci-lint
        args: ['--config=.golangci.yml']

  # === JavaScript/TypeScript 项目 ===
  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v9.3.0
    hooks:
      - id: eslint
        files: \.(js|ts|jsx|tsx)$
        types: [file]
        args: ['--fix']

  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v4.0.0-alpha.8
    hooks:
      - id: prettier
        types_or: [javascript, ts, css, html, json, yaml, markdown]

  # === Shell 脚本 ===
  - repo: https://github.com/scop/pre-commit-shfmt
    rev: v3.8.0
    hooks:
      - id: shfmt
        args: ['-i', '4', '-w']

  - repo: https://github.com/koalaman/shellcheck-precommit
    rev: v0.10.0
    hooks:
      - id: shellcheck
        args: ['-x']

  # === 安全检查 ===
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

  # === 提交信息检查 ===
  - repo: https://github.com/compilerla/conventional-pre-commit
    rev: v3.2.0
    hooks:
      - id: conventional-pre-commit
        stages: [commit-msg]
        args: [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert]

  # === Dockerfile ===
  - repo: https://github.com/hadolint/hadolint
    rev: v2.12.0
    hooks:
      - id: hadolint-docker
        args: ['--ignore', 'DL3008']

  # === Markdown ===
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.41.0
    hooks:
      - id: markdownlint
        args: ['--fix']

2.3 安装与使用

# 安装 hook 到 .git/hooks/
pre-commit install

# 同时安装 commit-msg hook
pre-commit install --hook-type commit-msg

# 手动运行所有检查
pre-commit run --all-files

# 只运行指定 hook
pre-commit run trailing-whitespace --all-files

# 只检查暂存的文件
pre-commit run

# 更新 hook 版本
pre-commit autoupdate

# 清理缓存
pre-commit clean
pre-commit gc

# 查看状态
pre-commit run --show-stages

2.4 本地 Hook 配置

开发者可以在 .pre-commit-config.yaml 之外添加本地自定义 hook:

repos:
  # ... 其他 hook ...

  # 本地自定义 hook
  - repo: local
    hooks:
      - id: go-build
        name: Go Build
        entry: go build ./...
        language: system
        files: \.go$
        pass_filenames: false

      - id: go-test
        name: Go Test
        entry: go test -short ./...
        language: system
        files: \.go$
        pass_filenames: false
        stages: [push]  # 只在 pre-push 时运行

      - id: custom-check
        name: Custom Check
        entry: ./scripts/custom-check.sh
        language: script
        files: \.(go|py|js|ts)$

2.5 CI 中运行 pre-commit

# .github/workflows/pre-commit.yml
name: Pre-commit Checks
on:
  pull_request:
  push:
    branches: [main]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      - name: Install pre-commit
        run: pip install pre-commit
      - name: Cache pre-commit
        uses: actions/cache@v4
        with:
          path: ~/.cache/pre-commit
          key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
      - name: Run pre-commit
        run: pre-commit run --all-files

三、Husky

3.1 安装

# 初始化 npm 项目(如果还没有)
npm init -y

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

# 初始化 Husky
npx husky init
# 这会创建 .husky/ 目录并在 package.json 中添加 prepare 脚本

3.2 配置 Hook

# 创建 pre-commit hook
echo 'npx lint-staged' > .husky/pre-commit

# 创建 commit-msg hook
echo 'npx --no-install commitlint --edit $1' > .husky/commit-msg

# 创建 pre-push hook
echo 'npm test' > .husky/pre-push

3.3 lint-staged

lint-staged 只检查暂存区的文件,避免对整个项目运行检查:

// package.json
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,html,json,md,yaml,yml}": [
      "prettier --write"
    ],
    "*.py": [
      "ruff check --fix",
      "ruff format"
    ],
    "*.go": [
      "gofmt -w",
      "goimports -w"
    ],
    "*.sh": [
      "shfmt -w",
      "shellcheck"
    ],
    "Dockerfile": [
      "hadolint"
    ]
  }
}

3.4 完整的 Node.js 项目配置

// package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "prepare": "husky",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "test": "jest",
    "test:watch": "jest --watch"
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "eslint": "^9.0.0",
    "prettier": "^3.0.0",
    "@commitlint/cli": "^19.0.0",
    "@commitlint/config-conventional": "^19.0.0"
  },
  "lint-staged": {
    "*.{js,ts}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yaml}": ["prettier --write"]
  }
}
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',     // 新功能
        'fix',      // 修复 bug
        'docs',     // 文档变更
        'style',    // 代码格式(不影响功能)
        ' refactor', // 重构(不影响功能)
        'perf',     // 性能优化
        'test',     // 测试
        'build',    // 构建系统或外部依赖变更
        'ci',       // CI 配置变更
        'chore',    // 杂项(不修改源码或测试)
        'revert'    // 回滚
      ]
    ],
    'subject-max-length': [2, 'always', 72],
    'body-max-line-length': [1, 'always', 100],
  }
};

3.5 .husky 目录结构

.husky/
├── _/                    # Husky 内部文件(不要修改)
│   ├── h
│   └── husky.sh
├── pre-commit            # pre-commit hook
├── commit-msg            # commit-msg hook
├── pre-push              # pre-push hook
└── post-merge            # post-merge hook
#!/usr/bin/env sh
# .husky/pre-commit
# Husky v9+ 格式(不需要 source husky.sh)

# 运行 lint-staged
npx lint-staged

# 运行类型检查
npx tsc --noEmit

# 检查是否有 console.log
if git diff --cached | grep -E '^\+.*console\.log'; then
    echo "✗ 请移除 console.log"
    exit 1
fi

四、commitlint

4.1 提交信息规范

Conventional Commits 是最广泛使用的提交信息规范:

<type>(<scope>): <subject>

<body>

<footer>
类型说明示例
feat新功能feat(auth): 添加 OAuth2 登录
fixBug 修复fix(api): 修复用户列表分页错误
docs文档docs: 更新 README 安装步骤
style格式style: 统一缩进为 2 空格
refactor重构refactor(db): 重构连接池管理
perf性能perf(query): 优化慢查询 N+1 问题
test测试test(auth): 添加登录单元测试
build构建build: 升级 Go 到 1.22
ciCIci: 添加 GitHub Actions 工作流
chore杂项chore: 更新依赖版本
revert回滚revert: 回滚 feat(auth) 提交

4.2 commitlint 配置

// 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']
    ],
    // 类型必须小写
    'type-case': [2, 'always', 'lower-case'],
    // 类型不能为空
    'type-empty': [2, 'never'],
    // 作用域格式
    'scope-case': [2, 'always', 'lower-case'],
    // 主题不能为空
    'subject-empty': [2, 'never'],
    // 主题不能以 . 结尾
    'subject-full-stop': [2, 'never', '.'],
    // 主题大小写
    'subject-case': [0],  // 禁用(允许中文)
    // 主题最大长度
    'subject-max-length': [2, 'always', 72],
    // 主题最小长度
    'subject-min-length': [2, 'always', 3],
    // 头部最大长度
    'header-max-length': [2, 'always', 100],
    // body 每行最大长度
    'body-max-line-length': [1, 'always', 100],
    // footer 最大行长度
    'footer-max-line-length': [1, 'always', 100],
  }
};

4.3 验证提交信息

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

# 验证提交信息
echo "feat: add new feature" | npx commitlint
# ✓ 通过

echo "update something" | npx commitlint
# ✗ 失败:缺少 type

echo "FEAT: add feature" | npx commitlint
# ✗ 失败:type 必须小写

# 从文件验证
npx commitlint --edit .git/COMMIT_EDITMSG

# 从 git log 验证
git log --format=%s | npx commitlint

4.4 交互式提交工具 commitizen

# 安装
npm install --save-dev commitizen cz-conventional-changelog

# 配置
echo '{"config": {"commitizen": {"path": "cz-conventional-changelog"}}}' >> package.json

# 使用交互式提交
npx cz
# ? Select the type of change that you're committing:
# ❯ feat:     A new feature
#   fix:      A bug fix
#   docs:     Documentation only changes
#   ...
# ? What is the scope of this change (e.g. component or file name)?
# ? Write a short, imperative tense description of the change:
# ? Provide a longer description of the change:
# ? Are there any breaking changes?
# ? Does this change affect any open issues?

五、自动化测试触发

5.1 pre-commit 运行快速测试

#!/usr/bin/env sh
# .husky/pre-commit

# 快速检查(< 10 秒)
npx lint-staged

# 类型检查
npx tsc --noEmit

# 只运行受影响的测试
# 获取暂存的文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|js)$')

if [ -n "$STAGED_FILES" ]; then
    echo "运行相关测试..."
    npx jest --findRelatedTests $STAGED_FILES --passWithNoTests
fi

5.2 pre-push 运行完整测试

#!/usr/bin/env sh
# .husky/pre-push

echo "执行推送前检查..."

# 1. 完整测试套件
echo "运行测试套件..."
npm test
if [ $? -ne 0 ]; then
    echo "✗ 测试失败,推送已阻止"
    echo "如需跳过,使用: git push --no-verify"
    exit 1
fi

# 2. 检查是否在推送受保护分支
PROTECTED_BRANCHES="main master release/*"
BRANCH=$(git rev-parse --abbrev-ref HEAD)

for protected in $PROTECTED_BRANCHES; do
    if [[ "$BRANCH" == $protected ]]; then
        echo "⚠ 你正在推送受保护分支: $BRANCH"
        echo "确认推送?(y/N)"
        read -r response
        if [ "$response" != "y" ] && [ "$response" != "Y" ]; then
            echo "推送已取消"
            exit 1
        fi
    fi
done

# 3. 检查是否需要同步 main 分支
if [[ "$BRANCH" != "main" ]] && [[ "$BRANCH" != "master" ]]; then
    MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | awk '{print $NF}')
    BEHIND=$(git rev-list --count HEAD..origin/$MAIN_BRANCH 2>/dev/null || echo 0)
    if [ "$BEHIND" -gt 0 ]; then
        echo "⚠ 当前分支落后 $MAIN_BRANCH $BEHIND 个提交"
        echo "建议先 rebase: git rebase origin/$MAIN_BRANCH"
    fi
fi

echo "✓ 推送前检查通过"

5.3 Go 项目测试 Hook

#!/usr/bin/env bash
# .husky/pre-commit (Go 项目)

set -euo pipefail

# 获取暂存的 Go 文件
STAGED_GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' || true)

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

echo "执行 Go 代码检查..."

# 1. 格式化
echo "  格式化代码..."
gofmt -w $STAGED_GO_FILES
git add $STAGED_GO_FILES

# 2. goimports
if command -v goimports &>/dev/null; then
    echo "  整理 import..."
    goimports -w $STAGED_GO_FILES
    git add $STAGED_GO_FILES
fi

# 3. golangci-lint
echo "  运行 golangci-lint..."
golangci-lint run --new-from-rev=HEAD $STAGED_GO_FILES

# 4. 编译检查
echo "  编译检查..."
go build ./...

# 5. 运行相关测试
echo "  运行测试..."
for file in $STAGED_GO_FILES; do
    # 找到对应的测试文件
    dir=$(dirname "$file")
    pkg=$(basename "$dir")
    test_file="${dir}/${pkg}_test.go"
    if [ -f "$test_file" ]; then
        go test -short -count=1 "./${dir}/..."
    fi
done

echo "✓ Go 代码检查通过"

六、CI 与本地 Hook 配合

6.1 分层检查策略

本地 pre-commit:  格式化 + 快速 lint + 类型检查      (< 10s)
本地 pre-push:     完整测试套件                         (< 60s)
CI (PR):           全量 lint + 测试 + 安全扫描 + 构建    (< 5min)
CI (main):         部署到 staging + 集成测试              (< 10min)

6.2 统一配置源

避免本地和 CI 配置不一致,使用同一份配置:

# .github/workflows/quality.yml
name: Code Quality
on: [pull_request]

jobs:
  # 与 pre-commit 完全相同的检查
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.12' }
      - run: pip install pre-commit
      - uses: actions/cache@v4
        with:
          path: ~/.cache/pre-commit
          key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
      - run: pre-commit run --all-files

  # CI 独有的检查
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy
        run: |
          trivy fs --severity HIGH,CRITICAL .          
      - name: Run GoSec
        run: |
          go install github.com/securego/gosec/v2/cmd/gosec@latest
          gosec ./...          

  # 测试覆盖率
  test-coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: '1.22' }
      - run: go test -race -coverprofile=coverage.out ./...
      - name: Check coverage
        run: |
          COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
          echo "Coverage: $COVERAGE"
          # 最低覆盖率要求
          if (( $(echo "$COVERAGE < 70.0" | bc -l) )); then
            echo "✗ 覆盖率低于 70%"
            exit 1
          fi          

6.3 跳过 Hook 的管理

--no-verify 是双刃剑:有时需要跳过(如紧急修复),但滥用会让 Hook 形同虚设。

# .husky/pre-commit
#!/usr/bin/env sh

# 允许通过提交信息中的 [skip lint] 跳过
COMMIT_MSG=$(cat .git/COMMIT_EDITMSG 2>/dev/null || echo "")
if echo "$COMMIT_MSG" | grep -q '\[skip lint\]'; then
    echo "⚠ 跳过 lint 检查(提交信息包含 [skip lint])"
    exit 0
fi

npx lint-staged
# CI 中检测跳过 Hook 的提交
name: Verify Hooks
on: [pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Check for --no-verify commits
        run: |
          # 检查 PR 中的提交是否跳过了 Hook
          # 如果 pre-commit 在 CI 中发现问题但本地没有,
          # 说明开发者用了 --no-verify
          if pre-commit run --all-files 2>&1 | grep -q "Failed"; then
            echo "✗ 代码检查未通过(可能使用了 --no-verify 跳过本地检查)"
            exit 1
          fi          

七、团队规范落地

7.1 快速初始化新项目

#!/usr/bin/env bash
# setup-git-hooks.sh - 一键配置项目 Git Hooks

set -euo pipefail

PROJECT_TYPE="${1:-go}"  # go, node, python, mixed

echo "配置 Git Hooks ($PROJECT_TYPE 项目)..."

# 1. 复制配置文件
case "$PROJECT_TYPE" in
    go)
        cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-merge-conflict
      - id: check-added-large-files
        args: ['--maxkb=500']
      - id: detect-private-key

  - repo: https://github.com/dnephin/pre-commit-golang
    rev: v0.5.1
    hooks:
      - id: go-fmt
      - id: go-imports
      - id: go-mod-tidy

  - repo: https://github.com/golangci/golangci-lint
    rev: v1.59.0
    hooks:
      - id: golangci-lint

  - repo: local
    hooks:
      - id: go-build
        name: Go Build
        entry: go build ./...
        language: system
        files: \.go$
        pass_filenames: false
EOF
        ;;
    node)
        npm install --save-dev husky lint-staged eslint prettier \
            @commitlint/cli @commitlint/config-conventional
        npx husky init
        echo 'npx lint-staged' > .husky/pre-commit
        echo 'npx --no-install commitlint --edit $1' > .husky/commit-msg
        ;;
    python)
        cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: ['--fix']
      - id: ruff-format
EOF
        ;;
esac

# 2. 安装 pre-commit(如果使用)
if [ -f .pre-commit-config.yaml ]; then
    pip install pre-commit 2>/dev/null || true
    pre-commit install
    pre-commit install --hook-type commit-msg
    echo "✓ pre-commit 已安装"
fi

# 3. 添加 commitlint(如果使用 Husky)
if [ -d .husky ]; then
    cat > commitlint.config.js << 'EOF'
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'subject-max-length': [2, 'always', 72],
  }
};
EOF
    echo "✓ commitlint 已配置"
fi

# 4. 添加到 .gitignore
grep -qxF '.husky/_/' .gitignore 2>/dev/null || echo '.husky/_/' >> .gitignore

echo ""
echo "Git Hooks 配置完成!"
echo "  - 提交时自动检查代码格式和 lint"
echo "  - 提交信息需遵循 Conventional Commits 规范"
echo "  - 如需跳过: git commit --no-verify"

7.2 团队 onboarding 文档

# 代码质量工具配置

## 首次设置

1. 安装依赖:
   ```bash
   # Python 工具
   pip install pre-commit

   # Node.js 工具(如果项目包含前端代码)
   npm install
  1. 安装 Git Hooks:

    pre-commit install
    pre-commit install --hook-type commit-msg
    # 或
    npm run prepare  # Husky
    
  2. 验证:

    pre-commit run --all-files
    

提交信息格式

<type>(<scope>): <subject>

<body>

<footer>

类型:feat | fix | docs | style | refactor | perf | test | build | ci | chore | revert

示例:

feat(auth): 添加 OAuth2 登录支持
fix(api): 修复用户列表分页错误 (#123)
docs: 更新部署文档

跳过检查

紧急情况可跳过:

git commit --no-verify -m "hotfix: 紧急修复生产问题"

注意:CI 会再次运行检查,跳过本地检查不代表跳过 CI。


### 7.3 渐进式推广策略

阶段1(第1-2周):观察期

  • 安装 Hook 但设为 warning 模式(不阻止提交)
  • 收集常见问题,统计违规次数

阶段2(第3-4周):格式化自动修复

  • 启用自动格式化(gofmt、prettier、ruff format)
  • 启用 trailing-whitespace、end-of-file-fixer
  • 开发者感受:提交时自动修正格式,无额外负担

阶段3(第5-6周):Lint 强制执行

  • 启用 lint 检查,阻止不合规提交
  • 提供修复指南和文档
  • 允许 [skip lint] 紧急跳过

阶段4(第7-8周):提交信息规范

  • 启用 commitlint
  • 配合 commitizen 交互式提交工具降低门槛

阶段5(持续):CI 强制执行

  • CI 运行与本地相同的检查
  • 覆盖率门槛逐步提高
  • 安全扫描纳入流水线

## 八、常见问题与解决方案

### 8.1 Hook 执行太慢

```yaml
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      # 只检查暂存文件,不检查全项目
      - id: eslint-staged
        name: ESLint (staged only)
        entry: npx eslint
        language: system
        files: \.(js|ts)$
        # pre-commit 默认只传暂存文件

      # 分离快速检查和慢速检查
      - id: type-check
        name: TypeScript Type Check
        entry: npx tsc --noEmit
        language: system
        files: \.ts$
        pass_filenames: false
        stages: [push]  # 移到 pre-push

8.2 Hook 在 CI 中失败但本地通过

# 常见原因:本地缓存导致结果不一致

# 清理 pre-commit 缓存
pre-commit clean
pre-commit gc

# 重新安装
pre-commit install
pre-commit run --all-files

# CI 中确保不使用缓存
pre-commit run --all-files --show-diff-on-failure

8.3 多语言项目配置

# .pre-commit-config.yaml - 多语言项目
repos:
  # 通用检查
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks: &default_hooks
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-merge-conflict
      - id: detect-private-key

  # Python 代码
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: ['--fix']
      - id: ruff-format

  # Go 代码
  - repo: https://github.com/dnephin/pre-commit-golang
    rev: v0.5.1
    hooks:
      - id: go-fmt
      - id: go-imports

  # Shell 脚本
  - repo: https://github.com/koalaman/shellcheck-precommit
    rev: v0.10.0
    hooks:
      - id: shellcheck

  # Dockerfile
  - repo: https://github.com/hadolint/hadolint
    rev: v2.12.0
    hooks:
      - id: hadolint

总结

Git Hooks 是代码质量自动化的第一道防线,它的价值在于在问题进入代码库之前就拦截。回顾本文核心要点:

  1. Hooks 是原生能力,工具是管理手段:Git 自带 Hooks 机制,但 .git/hooks/ 不在版本控制中。pre-commit 和 Husky 解决的是"配置共享和自动化安装"的问题
  2. pre-commit 适合多语言项目:通过 .pre-commit-config.yaml 统一管理所有语言的检查工具,社区生态丰富,Go/Python/Shell/JSON/YAML 都有现成 hook
  3. Husky + lint-staged 是 Node.js 标配:Husky 管理 Hook 安装,lint-staged 只检查暂存文件,两者配合实现高效的增量检查
  4. commitlint 规范提交信息:Conventional Commits 格式不仅让 git log 更整洁,还能自动生成 changelog、自动决定语义化版本号
  5. 分层检查避免拖慢开发:pre-commit 做格式化和快速 lint(< 10s),pre-push 做完整测试(< 60s),CI 做安全扫描和覆盖率检查(< 5min)。每层只做自己该做的事
  6. CI 是最终保障:本地 Hook 可以 --no-verify 跳过,但 CI 不能。CI 运行与本地相同的检查配置,确保即使跳过了本地检查,问题也会在 PR 阶段被发现
  7. 渐进式推广是关键:不要一次性启用所有检查。先观察→自动修复→lint 强制→提交规范→CI 强制,分阶段推进,给团队适应时间

Git Hooks 的终极目标不是阻止提交,而是让开发者每次提交的代码都自动符合团队标准。当格式化、lint、测试都变成提交时的自动行为时,开发者就不需要"记住"这些规则——规则自己会执行。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Git Hooks 官方文档 — Git-scm,参考了Git Hooks 官方文档相关内容
  2. pre-commit 官网 — Pre-commit,参考了pre-commit 官网相关内容