pre-commit 演进史:从 0.1.0 到 4.6.2 的多语言 Git 钩子框架完整技术脉络

原创2026-10-02 11:30:531,874 阅读
文章标签:开发工具代码质量

pre-commit 演进史:从 0.1.0 到 4.6.2 的多语言 Git 钩子框架完整技术脉络

本篇文章以仓库根目录的 CHANGELOG.md 为主体档案,结合 pre_commit 目录下的源码实现,系统梳理 pre-commit —— 一个用于管理与维护多语言 pre-commit 钩子的框架 —— 从 2014 年 0.1.0 首次发布到 2026 年 4.6.2 共 12 年的技术演进脉络。读完你将掌握:配置格式的两次重大迁移(list → map、sha → rev)、21 种钩子语言的引入与替换顺序、各 Git 钩子阶段(stages)的扩展历程、CLI 命令体系与退出码约定,以及并行执行、克隆优化等工程机制的底层实现依据,从而理解今天 .pre-commit-config.yaml 中每个配置项的设计由来。

项目概览与版本主线

pre-commit 是一个用 Python 实现的框架,其核心定位是"管理并维护多语言 pre-commit 钩子"。它读取仓库根目录的 .pre-commit-config.yaml(源码中由 pre_commit/constants.py 定义),将钩子仓库克隆到本地缓存,按钩子声明的 language 安装隔离环境,并在 git commit、git push 等事件触发时按阶段运行相应钩子。

从版本时间线可以看出清晰的演进主线(详见 CHANGELOG.md):

  • 2014–2015(0.x 早期):确立 MVP,支持 pre-commit / pre-push 两个阶段,引入 system、script、python、ruby、node、pcre 等早期语言,并用 sqlite 管理缓存仓库(0.4.0)。
  • 2017(1.0.0 前后):引入 semver 版本规范;缓存目录从 ~/.pre-commit 迁移到 $XDG_CACHE_HOME/pre-commit(通常为 ~/.cache/pre-commit);发布 v2 配置格式(顶层 map + repos 键);新增 migrate-config、try-repo、gc 等命令。
  • 2018–2020(1.x → 2.x):钩子并行执行(1.13.0)、types/exclude_types(0.15.0)、pygrep 取代 pcre、新增 rust/golang/dotnet/coursier/r 等语言、引入 distinct 退出码(2.8.0)。
  • 2021–2023(2.x → 3.x):新增 dart/lua/haskell、stages 命名规范化(pre-commit/pre-push/pre-merge-commit)、autoupdate 的 blobless clone 与并行化、移除 python_venv、对旧配置格式(顶层 list、sha)发出弃用警告。
  • 2024–2026(3.x → 4.x):新增 julia、unsupported/unsupported_script 别名、--fail-fast 全局选项、hazmat 命令、hook-impl 对 git 2.54+ 新钩子机制的适配。

仓库源码中 pre_commit/all_languages.py 的 languages 字典列出了当前版本全部 21 种受支持语言,是这条演进主线的最新快照。

配置格式演进:两次关键迁移与 migrate-config

v2 map 格式(1.0.0)

1.0.0 引入了新的配置格式:顶层从 list 变为 map,钩子仓库被放入 repos 键。CHANGELOG 给出了升级 diff 示例:

+repos:
 -   repo: https://github.com/pre-commit/pre-commit-hooks
     sha: v0.9.2
     hooks:

旧 list 格式在 1.0.0 后仍被支持,到 3.0.0 才正式移除(见 CHANGELOG 3.0.0 "Drop support for top-level list")。实现上,migrate-config 会在 list 配置的首个非注释/非空行前插入 repos:,并处理 yaml 流式风格与缩进两种情形,详见 pre_commit/commands/migrate_config.py。

sha → rev(1.7.0 → 3.0.0)

1.7.0 将 rev 设为 sha 的推荐替代;3.0.0 正式移除 sha 支持。当前 migrate_config.py 中仍保留着 sha → rev 的重写匹配器:

sha_to_rev_replace = functools.partial(_preserve_style, s='rev')
sha_to_rev_matcher = (
    MappingValue('repos'),
    SequenceItem(),
    MappingKey('sha'),
)

python_venv → python(2.4.0 起别名,4.0.0 移除)

2.4.0 起 language: python_venv 成为 language: python 的别名(并预告未来移除);3.1.0 正式弃用;4.0.0 移除。migrate-config 同样内置了该重写规则(migrate_config.py)。

旧 stages 名称的迁移(4.0.0 / 4.0.1)

4.0.0 对旧阶段名 commit → pre-commit、push → pre-push、merge-commit → pre-merge-commit 发出警告,并在 migrate-config 中自动重写(migrate_config.py,重写为 pre-{name} 形式);4.0.1 修复了纯 pyyaml 下对未加引号旧阶段名的迁移问题。

实践要点:任何 3.x 及更早的存量配置,升级到 4.x 前应先运行 pre-commit migrate-config,一次性完成 list→map、sha→rev、python_venv→python、旧 stages→新 stages 四类重写。

多语言生态:从 system 到 21 种语言的扩展历程

语言系统是 CHANGELOG 中出现频率最高的主题。每个 language 在源码中实现为一个满足 pre_commit/lang_base.py 中 Language 协议的对象,核心方法包括 ENVIRONMENT_DIR、get_default_version()、health_check()、install_environment()、in_env() 与 run_hook()。当前完整清单见 pre_commit/all_languages.py。

以下按 CHANGELOG 中的引入顺序整理:

语言 引入版本 关键要点
python / ruby / node / system / script / pcre 0.x 早期 框架最初的支柱语言
docker / docker_image 0.10.0 / 0.18.0 0.10.0 支持 docker 钩子;0.18.0 新增 docker_image(类似 system/script 的轻量用法,可作为 repo: local 钩子);2.8.0 起用 tty 传递颜色(3.7.0);4.3.0 检测 rootless docker
swift 0.11.0 SwiftPM 支持;2.x 中多次随新版本修复(2.3.2 支持 swift>=5.8)
golang 0.12.0 3.0.0 起 go.mod 指定依赖、缺失时自动引导 go;4.1.0 禁用自动 toolchain 切换
pygrep(取代 pcre) 1.2.0 比 pcre 更跨平台;1.8.0 增加 --multiline;2.8.0 增加 --negate;pcre 在 2.0.0 移除
python_venv(后并入 python) 1.9.0 基于标准库 venv 而非 virtualenv;4.0.0 移除
rust 1.10.0 2.21.0 成为 first-class 语言:支持 language_version 与缺失时安装;3.7.1 修复 rust-toolchain.toml 场景
perl 2.1.0 纯 perl 钩子支持
dotnet 2.8.0 基于 dotnet 的包管理;2.21.0 支持点号前缀包名、跳过默认 nuget 源
coursier 2.8.0 JVM 系包管理器;3.0.0 起支持 additional_dependencies 与 repo: local;2.18.0 兼容 cs/coursier 两种可执行名
r 2.11.0 2.13.0 开放 repo: local;3.8.0 增加系统 R 版本变化的健康检查;修复过 RStudio 发起安装(4.1.0)、$R_HOME(2.18.0)等平台问题
dart 2.15.0 4.3.0 适配要求更高 sdk 下限的新版 dart
lua 2.17.0 支持 lua 钩子
conda 1.21.0 2.17.0 起可通过 PRE_COMMIT_USE_MAMBA=1 / PRE_COMMIT_USE_MICROMAMBA=1 改用 mamba / micromamba
haskell 3.4.0 基于 cabal 的 haskell 钩子
julia 4.1.0 4.6.1 起设置 JULIA_DEPOT_PATH;4.3.0 避免加载 startup.jl
unsupported / unsupported_script 4.4.0 system / script 的别名,两者未来将弃用

以 node 语言为例,源码展示了"环境安装 + 健康检查"的完整闭环(pre_commit/languages/node.py):get_default_version() 在非 Windows 且系统已安装 node/npm 时返回 system 以省去 nodeenv 搭建时间;install_environment() 通过 python -m nodeenv --prebuilt --clean-src 创建隔离 node 环境;health_check() 用 node --version 验证环境健康。CHANGELOG 中关于 node 的多次修复(npm 11.x/12.x 兼容、4.6.1/4.6.2)正是围绕这套 install/health_check 机制展开的。此外 lang_base.py 的 exe_exists() 会排除 shims 目录内的可执行文件与家目录下的安装(2.8.0 修复的"误把 shim 当系统版本"问题即源于此)。

CLI 命令体系演进

CHANGELOG 记录了命令从无到有的完整过程,当前命令实现位于 pre_commit/commands:

  • install:0.2.x 引入;支持 --hook-type 多次指定(1.19.0,如 pre-commit install --hook-type pre-commit --hook-type pre-push)、--install-hooks(0.2.4)、--allow-missing-config(0.13.3)、-f/--overwrite(0.2.0);2.18.0 增加顶层 default_install_hook_types 作为未指定 --hook-types 时的默认值;4.6.0 起 hook-impl 允许 --hook-dir 缺失以适配 git 2.54+ 的钩子机制,同时 --hook-type 变为必填。
  • run:0.3.0 增加 --files;0.13.4 增加 --show-diff-on-failure;1.15.0 增加 CI 友好的提示信息;2.21.0 允许对未合并路径运行 --files;4.4.0 增加全局 --fail-fast。当前实现中文件过滤逻辑在 pre_commit/commands/run.py(filter_by_include_exclude 与 Classifier.by_types),并支持 Windows 上统一正斜杠。
  • autoupdate:早期引入后持续增强 —— --repo 多次指定(1.7.0)、--tags-only/--bleeding-edge(0.14.0)、--freeze(1.21.0,产出"冻结"提交号)、-j/--jobs 并行(3.3.0)、blobless clone 加速(3.3.0);2.4.0 起自动调用 migrate-config 并保持 rev 的引号风格。现代实现见 pre_commit/commands/autoupdate.py,其中 4.6.1 新增了对错误拼写的 --repo 报错,防止静默跳过。
  • migrate-config:1.0.0 引入(见上文配置迁移)。
  • sample-config:0.14.0 引入,用于生成示例配置。
  • try-repo:1.3.0 引入,可在不修改配置的情况下试用钩子仓库,也用于开发钩子仓库时的本地验证。
  • init-templatedir:1.18.0 引入,配合 git template 目录让克隆出的仓库自动启用 pre-commit;2.7.0 增加 --no-allow-missing-config。
  • gc:1.14.0 引入,清理不再被引用的缓存仓库。
  • validate-config / validate-manifest:2.19.0 引入,取代旧的 pre-commit-validate-config / pre-commit-validate-manifest 入口(后者 3.0.0 移除)。
  • clean:缓存清理;1.15.0 起不再要求处于 git 仓库内。
  • hazmat:4.5.0 引入(hazmat = "危险材料",供高级用户使用的底层命令)。
  • install-hooks:0.10.0 引入,与 install --install-hooks 等价但不产生 install 副作用。

退出码约定(2.8.0)

2.8.0 引入 distinct 退出码:1(用户错误)、3(意外错误)、130(Ctrl+C)。此前版本仅靠 0/1 区分,这一改动让 CI 与脚本能够区分"钩子失败"与"框架故障"。

Git 钩子阶段(stages)扩展史

stages 用于限定钩子在哪些 Git 事件中运行,其演进如下:

版本 新增/变更
0.6.0 按 commit / push 过滤钩子
0.15.4 支持 commit-msg 钩子
1.8.0 增加 manual 阶段(仅 CLI 手动触发)
1.16.0 支持 prepare-commit-msg(并通过 --commit-msg-filename 传递文件名,2.2.0 修复)
1.21.0 支持 pre-merge-commit(需 git 2.24+)
2.2.0 支持 post-checkout
2.4.0 支持 post-commit
2.11.0 支持 post-merge
2.15.0 支持 post-rewrite
3.2.0 正式支持 pre-commit、pre-push、pre-merge-commit 作为阶段名,并新增 pre-rebase
4.0.0 旧名 commit/push/merge-commit 弃用

2.20.0 还为 prepare-commit-msg 暴露了 PRE_COMMIT_COMIT_MSG_SOURCE 与 PRE_COMMIT_COMMIT_OBJECT_NAME 两个环境变量,便于钩子读取 git 传入的位置参数。

配置项演进:从 fail_fast 到 default_* 系列

CHANGELOG 记录了丰富的配置项演化,这些字段共同构成了现代 .pre-commit-config.yaml 的语法:

  • fail_fast:1.1.0 引入(默认 false,限 v2 格式);2.16.0 支持单钩子级 fail_fast;3.7.0 修正了单钩子 fail_fast 在先前钩子失败时未正确停止的问题,并给出迁移建议:若需旧行为,请在最后一个 fail_fast 钩子前的所有钩子上都设置 fail_fast: true。
  • 顶层 files / exclude:1.1.0 增加全局 exclude;1.21.0 增加顶层 files;0.15.0 起 files 变为可选(默认 ''),并明确 always_run 缺失 files 时默认 files: ''(0.15.0 修正了此前 ^$ 的行为)。2.9.1 / 2.10.0 对 files/exclude 中的 /* 模式增加警告,2.17.0 对 [\/]、[/\\] 类正则给出"敏感正则"提示。
  • types / exclude_types:0.15.0 引入,值来自 identify 库的文件标签;2.9.2 修复 types_or 默认值使 symlink/directory 可被匹配。
  • types_or:2.9.0 引入,允许一个钩子匹配多种不相关类型。
  • always_run:0.7.2 引入,无文件变化也运行。
  • verbose:1.6.0 引入,钩子成功时也输出;1.21.0 重构为与普通运行一致。
  • pass_filenames:0.14.0 引入,关闭自动追加文件名参数。
  • log_file:0.14.0 引入,将钩子输出 tee 到文件供 CI 消费。
  • additional_dependencies:0.6.6 引入;1.7.0 起带该字段的钩子获得隔离环境;2.18.0 增强了对不支持该字段的语言的错误提示。
  • minimum_pre_commit_version:0.6.8 引入早期版本;1.15.0 正式加入顶层;3.6.0 改为解析配置时最先检查。
  • default_language_version / default_stages:1.14.0 引入,集中覆盖多处 language_version / stages 配置。
  • alias:1.14.0 引入,区分重复配置的同一钩子。
  • require_serial:1.13.0 引入,配合并行执行(见下)。
  • 顶层 ci 键:2.10.0 引入,供 https://pre-commit.ci 读取配置。
  • 未知键警告:1.14.0 / 1.17.0 对顶层与仓库级未知配置键发出警告。

运行机制与性能演进

CHANGELOG 中相当比例的条目属于工程性能与正确性修复,这里提炼与当前实现直接对应的几条主线:

  • 并行执行:1.13.0 起钩子并行运行,require_serial: true 可退出并行。实现上文件按 xargs 分批,pre_commit/lang_base.py 使用固定随机种子(FIXED_RANDOM_SEED)做确定性打乱后再分片,既均衡负载又保证可复现(对应 1.13.0 "improve balancing by deterministic shuffling")。
  • 并发度探测:3.4.0 在 cgroups 下改进 CPU 数探测;target_concurrency() 还支持 PRE_COMMIT_NO_CONCURRENCY=1 强制串行、TRAVIS 环境下降级为 2(lang_base.py)。
  • 克隆策略优化:1.15.0 采用 shallow clone;3.3.0 改用 blobless clone 加速 autoupdate。当前 autoupdate 通过 git fetch origin HEAD --filter=blob:none 拉取(pre_commit/commands/autoupdate.py),并按 tags_only 选择 --abbrev=0 或 --exact 标签描述。
  • 缓存存储:0.4.0 用 sqlite 取代 symlink 管理仓库缓存,是后续 gc、clean 命令的基础。
  • 计时与输出:3.5.0 用 time.monotonic() 保证钩子计时准确;2.3.0 起按 east_asian_width 计算字符宽度(run.py),避免 CJK 字符导致对齐错误。
  • 文件选择正确性:2.5.0 起 Windows 上统一使用正斜杠匹配 files 正则(1.19.0 的路径归一化在 run.py 中有完整实现);4.6.1 修复 run --all-files 重复出现冲突文件名的问题。

环境变量与平台支持

钩子运行期间 pre-commit 暴露/透传了一批环境变量(均有 CHANGELOG 出处):

  • PRE_COMMIT=1(2.5.0):钩子运行标志。
  • SKIP(2.13.0):跳过指定钩子,且跳过环境安装。
  • PRE_COMMIT_COLOR={auto,always,never}(1.18.0)与 TERM=dumb 关闭颜色。
  • PRE_COMMIT_ORIGIN / PRE_COMMIT_SOURCE(0.14.3)、PRE_COMMIT_REMOTE_NAME / PRE_COMMIT_REMOTE_URL(2.0.0)、PRE_COMMIT_REMOTE_BRANCH(2.10.0)、PRE_COMMIT_LOCAL_BRANCH(2.14.0):pre-push 期间的 git 上下文。
  • PRE_COMMIT_COMIT_MSG_SOURCE / PRE_COMMIT_COMMIT_OBJECT_NAME(2.20.0):prepare-commit-msg 上下文。
  • PRE_COMMIT_USE_MAMBA=1 / PRE_COMMIT_USE_MICROMAMBA=1(2.17.0):conda 环境管理器切换。
  • git 相关环境透传:GIT_SSH、GIT_SSL_CAINFO、GIT_SSH_COMMAND、GIT_EXEC_PREFIX、GIT_ASKPASS、GIT_ALLOW_PROTOCOL、GIT_HTTP_PROXY_AUTHMETHOD、GIT_CONFIG_KEY_* / GIT_CONFIG_VALUE_* / GIT_CONFIG_COUNT(2.16.0)等陆续放开。

平台支持方面:0.4.0 提供 MVP Windows 支持;1.5.0 支持 Windows 上的 node 钩子;1.18.0 支持 Windows 上的 docker 钩子;2.6.0 提供 Windows 上 ruby 的部分支持(依赖系统 ruby);cygwin 场景在 1.18.0 后多次修复(lang_base.py 中 SHIMS_RE 专门用于识别 shim 路径)。2.18.0 修复了 git worktree 场景,4.4.0 增加 cgroups v2 下 docker-in-docker 检测。

破坏性变更与升级路径一览

以下为 CHANGELOG 明确标注的 Updating 项,是版本升级时最需要关注的兼容性边界:

版本 变更
1.0.0 配置顶层 list → map;缓存目录迁移至 ~/.cache/pre-commit
2.0.0 移除 pcre 语言(用 pygrep);移除 autoupdate --tags-only(已无作用);移除 python2 / python3.5 支持(仍可运行 python2 编写的钩子)
3.0.0 移除顶层 list 与 sha(用 migrate-config 迁移);移除 pre-commit-validate-config / pre-commit-validate-manifest 入口;golang 钩子必须用 go.mod 声明依赖;移除 python<3.8
3.6.0 移除 python<3.9
4.0.0 移除 language: python_venv(改用 python);旧 stages 名弃用

升级建议:3.x 存量仓库先执行 pre-commit migrate-config 再升级 4.x;CI 中缓存目录地址需按 1.0.0 的新位置($XDG_CACHE_HOME/pre-commit)调整;fail_fast 语义在 3.7.0 有行为变更,若需旧行为需显式在各钩子上配置。

小结:从 CHANGELOG 看框架设计哲学

纵览 0.1.0 至 4.6.2,CHANGELOG 呈现出的技术主线可概括为三点:其一,语言生态的持续扩张——从 4 种语言到 21 种(见 pre_commit/all_languages.py),且新语言必须实现 install_environment/health_check/in_env 等协议方法(pre_commit/lang_base.py),从而保证钩子环境的可重建、可验证;其二,配置与命令的平滑迁移——通过 migrate-config、validate-config、弃用警告等机制,让破坏性变更可自动化完成;其三,工程细节的持续打磨——并行调度、确定性打乱、blobless clone、退出码语义化、跨平台路径归一化等大量修复,使框架在大型仓库与 CI 场景下保持可靠。对于任何深度使用 pre-commit 的开发者,这份 CHANGELOG 连同 pre_commit 源码,都是理解每个配置项与命令"为什么长这样"的最佳资料。

登录后查看全文
pre-commit