pre-commit 演进史:从 0.1.0 到 4.6.2 的多语言 Git 钩子框架完整技术脉络
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 源码,都是理解每个配置项与命令"为什么长这样"的最佳资料。