Pre-commit 完全指南:Git 钩子自动化代码检查

在团队开发中,保证代码质量不能仅依赖开发者的自觉。本文将详细介绍如何使用 pre-commit 在代码提交前自动运行检查,确保代码符合团队规范。


一、什么是 pre-commit?

1.1 定义

pre-commit 是一个用于管理和维护多语言预提交钩子的框架。它可以在代码提交到 Git 仓库之前自动运行一系列检查,确保代码符合团队规范。

1.2 主要特点

特点 说明
多语言支持 支持 Python、JavaScript、Go、Rust 等多种语言的工具
自动安装 自动下载和安装所需的工具和依赖
缓存机制 只检查变更的文件,提高效率
丰富的 hooks 库 拥有大量预定义的钩子可供使用
多阶段支持 支持 pre-commit、pre-push、commit-msg 等多个 Git 钩子阶段

1.3 工作原理

1
git commit → 触发 .git/hooks/pre-commit → 运行检查 → 通过则提交,失败则阻止

二、安装与初始化

2.1 安装 pre-commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用 pip 安装
pip install pre-commit

# 使用 uv(推荐)
uv add pre-commit --group dev

# 使用 pipx 安装(隔离环境)
pipx install pre-commit

# 使用 Poetry
poetry add pre-commit --group dev

# 使用 Homebrew(macOS)
brew install pre-commit

# 验证安装
pre-commit --version

2.2 初始化配置

1
2
3
4
5
6
7
8
9
10
# 生成示例配置文件
pre-commit sample-config > .pre-commit-config.yaml

# 安装 Git 钩子
pre-commit install

# 安装所有类型的钩子
pre-commit install --hook-type pre-commit
pre-commit install --hook-type pre-push
pre-commit install --hook-type commit-msg

2.3 pre-commit install 的作用

pre-commit install 会在项目的 .git/hooks/ 目录下创建一个 pre-commit 脚本文件。

工作原理

  1. Git 在执行 git commit 时,会自动检查 .git/hooks/pre-commit 文件是否存在
  2. 如果存在且可执行,Git 会先运行这个脚本
  3. 脚本返回 0 表示成功,允许提交;非 0 表示失败,阻止提交
1
2
# 安装后可以查看生成的钩子文件
cat .git/hooks/pre-commit

简单说:这个命令把 pre-commit 框架”挂载”到 Git 的提交流程中。


三、Git 钩子类型

3.1 支持的钩子类型

默认 pre-commit install 只安装 pre-commit 钩子。如果你想在其他 Git 阶段运行检查,需要显式安装:

1
2
pre-commit install --hook-type pre-push      # 推送前检查
pre-commit install --hook-type commit-msg # 提交消息检查

完整的钩子类型(共 8 种)

钩子类型 触发时机 典型用途
pre-commit git commit 代码格式化、lint 检查
pre-push git push 运行测试、安全检查
commit-msg 提交消息写入后 校验提交消息格式
pre-merge-commit 合并前 检查合并冲突
post-checkout git checkout 环境初始化
post-commit 提交完成后 通知、日志记录
post-merge 合并完成后 依赖更新检查
prepare-commit-msg 编辑提交消息前 自动生成提交消息模板

四、配置文件详解

4.1 基础结构

创建 .pre-commit-config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ============================================
# 全局配置
# ============================================

# 指定各语言的默认版本
default_language_version:
python: python3.11

# 默认阶段:定义所有 hooks 默认在哪个 Git 钩子阶段运行
default_stages: [pre-commit]

# 第一个 hook 失败后是否停止运行后续 hooks
fail_fast: true

# ============================================
# repos:定义从哪里获取 hooks
# ============================================
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer

4.2 关键配置项解释

repos:hooks 来源

每个 repo 代表一个 hooks 来源。定义多个是为了从不同来源获取不同的工具:

原因 说明
不同工具由不同团队维护 Black 由 PSF 维护,isort 由 PyCQA 维护
版本独立管理 每个 repo 可以指定不同的 rev(版本)
按需选择 只引用需要的 repo
隔离环境 每个 repo 的依赖独立安装,避免冲突

id:hook 的唯一标识符

id 由仓库维护者在 .pre-commit-hooks.yaml 中定义。例如 pre-commit-hooks 仓库提供了 trailing-whitespaceend-of-file-fixer 等几十个 id。

additional_dependencies:额外依赖

pre-commit 使用独立的隔离环境,与项目虚拟环境完全分离。如果 hook 需要额外依赖,需要在此声明:

1
2
3
4
5
6
7
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies:
- django-stubs
- types-requests
1
2
3
4
5
6
项目虚拟环境(.venv)     ←  你手动安装的 mypy

不共享

pre-commit 缓存环境 ← pre-commit 自动下载的 mypy
(~/.cache/pre-commit)

执行顺序

同一阶段的 hooks 按定义顺序执行,所以通常把格式化工具放在检查工具之前:

1
2
3
4
5
6
7
8
9
10
repos:
# 1. 先格式化
- repo: https://github.com/psf/black
hooks:
- id: black

# 2. 再检查(检查已格式化的代码)
- repo: https://github.com/pycqa/flake8
hooks:
- id: flake8

4.3 阶段的确定机制

阶段的确定遵循优先级规则(从高到低):

1
2
3
4
5
6
7
8
9
10
11
12
# ① 单个 hook 显式指定 stages(最高优先级)
repos:
- repo: local
hooks:
- id: pytest
stages: [pre-push] # ✅ 使用这个

# ② 全局 default_stages(次优先级)
default_stages: [pre-commit] # 如果 hook 没指定 stages,用这个

# ③ 工具默认值(最低优先级)
# 如果都没指定,默认在几乎所有阶段运行

4.4 repo: local vs 远程仓库

类型 特点 使用场景
远程仓库 自动下载、隔离环境、版本锁定 通用工具(black、flake8)
repo: local 使用项目环境中的工具 自定义脚本、需要项目依赖的工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
repos:
# 远程仓库:自动下载、隔离环境
- repo: https://github.com/psf/black
rev: 24.3.0
hooks:
- id: black

# 本地仓库:使用项目环境中的工具
- repo: local
hooks:
- id: pytest
name: pytest
entry: uv run pytest # 使用本地安装的 pytest
language: system # 使用系统命令
stages: [pre-push] # 在 push 前运行
pass_filenames: false

注意:如果在本地和远程同时定义了相同工具(如 isort),两个都会运行!pre-commit 不会自动去重。


五、完整配置示例

5.1 企业级完整配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# .pre-commit-config.yaml

# 全局配置
default_language_version:
python: python3.11

default_stages: [pre-commit]
fail_fast: true

# CI 环境变量(可选)
ci:
autofix_prs: false
autoupdate_schedule: weekly

repos:
# ============================================
# 通用代码质量检查
# ============================================
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
exclude: '^.*\.(md|rst)$'
- id: end-of-file-fixer
- id: check-yaml
args: ['--unsafe'] # 允许自定义 YAML 标签
- id: check-json
- id: check-toml
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-merge-conflict
- id: detect-private-key
- id: debug-statements
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: mixed-line-ending
args: ['--fix=lf']
- id: name-tests-test
args: ['--pytest-test-first']
exclude: '^tests/factories/'

# ============================================
# Python 代码格式化
# ============================================

# isort - 导入排序
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort (python)
args: ['--settings-path', 'pyproject.toml']

# Black - 代码格式化
- repo: https://github.com/psf/black
rev: 24.3.0
hooks:
- id: black
name: black (python)
args: ['--config', 'pyproject.toml']

# pyupgrade - Python 语法升级
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: ['--py311-plus']

# ============================================
# Python 代码检查
# ============================================

# flake8 - 代码规范检查
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
name: flake8 (python)
additional_dependencies:
- flake8-annotations>=2.9.1
- flake8-bugbear>=23.1.14
- flake8-comprehensions>=3.10.1
args: ['--config', 'setup.cfg']

# mypy - 类型检查
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
name: mypy (python)
additional_dependencies:
- django-stubs>=4.2.0
- types-requests
- types-PyYAML
args: ['--config-file', 'pyproject.toml']
pass_filenames: false
always_run: true

# ============================================
# 本地 Hooks
# ============================================

# import-linter - 架构约束检查
- repo: local
hooks:
- id: lint-imports
name: lint-imports
entry: poetry run lint-imports
language: system
types: [python]
pass_filenames: false

# pytest - 推送前运行测试
- repo: local
hooks:
- id: pytest
name: pytest
entry: poetry run pytest --testmon -q
language: system
types: [python]
stages: [pre-push]
pass_filenames: false

5.2 本地自定义 Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
repos:
- repo: local
hooks:
# 使用 poetry 运行的自定义命令
- id: custom-lint
name: Custom Lint
entry: poetry run python scripts/custom_lint.py
language: system
types: [python]

# 使用脚本的 hook
- id: check-version
name: Check Version
entry: ./scripts/check_version.sh
language: script
files: ^(pyproject\.toml|setup\.py)$

# Django 检查
- id: django-check
name: Django Check
entry: poetry run python manage.py check
language: system
types: [python]
pass_filenames: false

六、pyupgrade - Python 语法升级

pyupgrade 是一个自动升级 Python 语法的工具:

1
2
3
4
5
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: ['--py311-plus']

自动转换示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 旧语法
from typing import Dict, List, Optional, Union

def func(items: List[str]) -> Dict[str, int]:
pass

x: Optional[str] = None
y: Union[int, str] = 1

# 转换后(Python 3.11+)
def func(items: list[str]) -> dict[str, int]:
pass

x: str | None = None
y: int | str = 1

七、常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ============ 安装/卸载 ============
pre-commit install # 安装 hooks
pre-commit uninstall # 卸载 hooks
pre-commit install --hook-type pre-push # 安装特定类型

# ============ 运行检查 ============
pre-commit run --all-files # 检查所有文件
pre-commit run # 只检查暂存文件
pre-commit run black --all-files # 运行特定 hook

# ============ 维护 ============
pre-commit autoupdate # 更新到最新版本
pre-commit clean # 清除缓存
pre-commit gc # 垃圾回收

# ============ 跳过检查(紧急情况)============
git commit --no-verify -m "紧急修复"
SKIP=flake8,mypy git commit -m "跳过特定检查"

# ============ 调试 ============
pre-commit run --hook-stage pre-commit --verbose

八、常见问题

8.1 pre-commit 太慢

1
2
3
4
5
6
7
8
9
10
# 只检查变更的文件类型
hooks:
- id: mypy
types: [python]

# 或改为检查整个项目(避免重复启动)
hooks:
- id: mypy
pass_filenames: false
always_run: true

8.2 CI 失败但本地通过

1
2
3
4
5
6
7
8
9
# 确保本地环境与 CI 一致
pre-commit run --all-files

# 更新 hooks 到最新版本
pre-commit autoupdate

# 清除缓存重试
pre-commit clean
pre-commit run --all-files

8.3 紧急情况需要跳过检查

1
2
3
4
5
# 跳过所有 pre-commit 检查
git commit --no-verify -m "hotfix: 紧急修复"

# 跳过特定检查
SKIP=flake8,mypy git commit -m "跳过特定检查"

8.4 团队成员忘记安装 hooks

README.md 中添加开发设置说明:

1
2
3
4
5
## 开发设置

1. 安装依赖
```bash
poetry install
  1. 安装 pre-commit hooks

    1
    2
    poetry run pre-commit install
    poetry run pre-commit install --hook-type pre-push
  2. 验证安装

    1
    poetry run pre-commit run --all-files
1
2
3
4
5
6

或在 `pyproject.toml` 中配置自动安装:

```toml
[tool.poetry.scripts]
setup = "scripts.setup:main"

九、与 CI/CD 集成

pre-commit 可以在 CI 中运行,作为双重保障:

9.1 GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
name: Lint

on: [push, pull_request]

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: pre-commit/action@v3.0.1

9.2 GitLab CI

1
2
3
4
5
6
7
8
9
10
11
12
13
pre-commit:
stage: check
image: python:3.11
variables:
PRE_COMMIT_HOME: "${CI_PROJECT_DIR}/.cache/pre-commit"
before_script:
- pip install pre-commit
script:
- pre-commit run --all-files
cache:
key: pre-commit-${CI_COMMIT_REF_SLUG}
paths:
- .cache/pre-commit

9.3 只检查变更文件(优化 CI)

1
2
3
4
5
6
7
script:
- |
if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
pre-commit run --from-ref origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to-ref HEAD
else
pre-commit run --all-files
fi

十、总结

核心要点

概念 说明
pre-commit Git 钩子管理框架
hooks 自动运行的检查脚本
repos hooks 的来源仓库
stages 运行的 Git 阶段
隔离环境 每个 hook 独立安装依赖

推荐配置

  1. 格式化工具:Black、isort、pyupgrade
  2. 检查工具:flake8、mypy
  3. 通用检查:trailing-whitespace、check-yaml、detect-private-key
  4. 推送前测试:pytest(在 pre-push 阶段)

工作流

1
2
3
4
5
编写代码 → git add → git commit 

pre-commit 自动运行

格式化 → 检查 → 通过则提交

pre-commit 是保证代码质量的第一道防线,与 CI/CD 配合使用,形成完整的代码质量工作流!


相关文章