维护的项目多了以后,我养成了一个职业病:开 PR 之前先翻一遍 commit message。不是我有洁癖,而是被坑怕了。早几年带一个中后台团队,十几个前端、后端、测试混在同一个仓库里,提交信息那叫一个乱——"fix""update""111""asdf",还有干脆空着的。真到发版本、写 CHANGELOG、回溯线上 bug 的时候,Git 历史就跟一锅粥似的,git log --oneline 根本没法看。后来我把"提交信息规范化"这件事折腾了个遍,最终动手用 Rust 写了一个零依赖的 Git 提交信息校验工具 gitru,才算把这个问题真正摁住。
先说清楚 gitru 是什么:它是一个跑在 Git commit-msg 钩子里的校验工具,负责在你提交代码的那一刻检查提交信息是否符合团队规范,不符合就直接拦下。它不依赖 Node、不依赖任何运行时、不依赖任何第三方库,编译完就是一个独立的可执行文件,几十 KB 的二进制扔到任何机器上都能跑,校验一条提交信息的耗时在毫秒级。
这篇文章我不打算写成一份干巴巴的 README 翻译,而是把"为什么要做这个工具""核心设计是怎么想的""怎么接入现有项目",再到"实际用下来踩了哪些坑"完整复盘一遍。如果你也在为团队 commit message 头疼,或者单纯想找一个比 commitlint 更轻的替代品,这篇应该对你有用。
1. 被烂提交信息支配的恐惧:gitru 到底在解决什么问题
1.1 提交信息不是写给 Git 看的,是写给六个月后的你
很多人觉得 commit message 就是个备注,随便写写就行。但完整的 Git 工作流里,提交信息真正服务的对象是这些场景:
- code review 时,reviewer 靠 commit message 判断改动意图;
- 发版时,conventional-changelog 这类工具要解析 feat/fix 才能自动生成 CHANGELOG;
- 语义化版本 bump 也要依赖提交类型做 major/minor/patch 判断;
- git bisect 排查回归时,一条清晰的提交信息能帮你快速锁定嫌疑范围;
- revert 一个提交时,如果原始信息没有上下文,回滚理由都写不明白。
这不是理论上的"最佳实践",而是实打实的成本。我见过最典型的一次事故:某次线上故障定位花了两个多小时,最后发现是一个同事在提交信息里写 "fix something stupid" 的改动引入了回归。如果提交信息能展示清楚改动动机和安全影响,这个回溯过程至少能缩短一半。
所以提交信息校验本质上不是"管得宽",而是把 Git 历史当成团队资产来保护。校验的价值不在当下,而在三个月、半年后,当所有人都不记得这次改动为什么存在的时候,历史记录还能替你说清楚。
1.2 规范落地为什么总是失败:不是缺文档,是缺强制
提到规范,大部分团队的第一反应是"写进开发文档"。但文档的宿命是被遗忘。真正有效的做法只有一个:在提交的那一刻强制执行,让不符合规范的提交根本落不下来。
这也是大家最终都会走向 git hook 的原因。社区里最常用的方案是 husky 搭配 commitlint,成熟度没得说,可它有四个硬伤:
第一,环境依赖重。commitlint 是 Node 生态,项目里必须能跑到 npm。Java、Go、Python 这类仓库为了校验提交信息硬塞一个 node_modules,怎么看都别扭。第二,安装体量大、速度慢。一个 commitlint CLI 拉上各种 preset 和依赖,动辄几十 MB,在 CI 环境里就是实打实的构建成本。第三,钩子很难做到开箱即用。JavaScript 项目有 husky 自动装钩子,但别的语言项目经常靠每人手动拷贝脚本,钩子版本不一致、某台机器没装 Node 直接跳过校验,这种情况我见了太多。第四,规则配置分散。每个仓库一份规则,今天这个改了明天那个忘改,团队规范成了拼图游戏。
我想要的工具是:拿到仓库就能校验,不装任何运行时,配置文件跟仓库走,人人都用同一套规则。这正是 gitru 的出发点。它解决的不是"没有规范"的问题,而是"规范执行不下去"的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. gitru 的核心设计:Rust、零依赖、单文件
2.1 零依赖不是噱头,是不想再被供应链折腾
设计 gitru 时我给自己定了一条死规矩:运行时不依赖任何第三方库,编译产物是单个静态可执行文件。这条规矩来自我被 Node 系工具伤过的经历,也来自对供应链安全的一点洁癖。
依赖越少,攻击面越小。commit 信息校验要处理的是开发者本地的任意输入,如果工具本身拉了一大堆传递依赖,任何一个上游包出问题都会波及所有使用方。零依赖意味着没有中间层,代码路径清晰,安全审计就是读自己这几千行代码的事。Rust 在这里的胜出几乎是必然的:它没有 GC、没有运行时,标准库自带的 I/O、字符串处理、进程操作能力足够,编译出来就是一份独立的静态二进制。相比之下,Go 虽然也能编出单文件,但 Rust 在字符串处理、错误处理和模式匹配上的表达力更适合"拿文本做规则校验"这种活。而且 Rust 的编译产物小、启动快,放在 git hook 这种高频路径上体验非常关键。
有人问我为什么不用 Shell 脚本或者 Python 写。Shell 写简单判断可以,但一旦规则复杂起来,跨平台、转义、退出码管理全是坑;Python 要依赖解释器,团队里不是人人都有。一个静态编译的 Rust 二进制,是"轻量"和"功能完整"之间最稳的交集。
2.2 gitru 校验一次提交,内部发生了什么
以当前版本的 CLI 为例,gitru 校验一次提交信息的工作流程说穿了很简单,但每一步都有讲究:
- 定位配置。gitru 默认在当前目录找 gitru.toml,也可以通过
--config参数显式指定。找不到配置就用内置的默认规则(Conventional Commits 基础版)。 - 接收提交信息。hook 场景下通过命令行参数传入提交信息文件路径,CI 场景下可以用
--message "xxx"直接传字符串,也可以用--stdin从标准输入读。 - 解析三层结构。按 Conventional Commits 语义把提交信息拆成 header(
type(scope): subject)、body、footer 三部分。解析不出来,直接判定格式非法。 - 执行规则引擎。逐条跑配置里的规则,比如 type 是否在允许列表、header 是否超长、scope 是否必填、body 是否必须、footer 是否要以特定 token 结尾。
- 汇总输出。所有错误一次性报完,而不是报一条停一条,这样用户在编辑器里能一次改到位。
- 返回退出码。全部通过返回 0,有任何一条不满足返回 1。Git hook 就是靠退出码拦截提交的。
每一步都没有魔法,解析和规则匹配本质上就是字符串处理和条件判断。但正是这种简单,让工具能做到"跑一万次都不出幺蛾子"。在处理上有一个小细节:输入文本会先做统一的换行符归一化,把 CRLF 转成 LF,避免不同操作系统带来的匹配差异——这个细节后面讲 Windows 踩坑时还会提到。
2.3 速度体验:毫秒级是底线,不是卖点
git hook 在每次 git commit 时同步执行,如果校验工具太慢,开发体验会直接从"顺手"变成"烦躁"。我实测过,在一台普通笔记本上,gitru 校验一条提交信息的耗时在 1ms 到 3ms 之间,加上进程启动也就 10ms 以内。而 commitlint 系工具光启动 Node 运行时就要几百毫秒,如果 node_modules 在机械盘或者 CI 缓存没命中,1 秒起步很常见。
这个差距在个人开发时体感不强,但在 CI 上,几十个并发 job 同时跑校验,省下来的时间就是实打实的成本。而且对于高频提交的开发者,每次回车提交后屏幕能即时反馈"通过",这个流畅度本身就在降低对工具的抵触情绪。
3. 从安装到接入 Git 钩子:完整实操记录
3.1 先装起来:cargo install 和直接下二进制
gitru 的安装方式按场景二选一。如果你本来就有 Rust 工具链,一条命令搞定:
bash复制cargo install gitru
装完验证一下:
bash复制gitru --version
如果机器上没有 Rust 环境,不用为这个工具专门装一套工具链,到 release 页面下载对应平台的二进制文件扔进 PATH 即可。我在团队里给同事的推荐方式是直接下载二进制,毕竟为了一个校验工具去装 Rust 工具链有点杀鸡用牛刀,而且总有人不乐意动自己的开发环境。
提示:从 release 页面下载二进制后,建议顺手算一下 SHA256 校验和,并且把版本号写进团队的 setup 脚本里固定住。二进制工具最容易出的问题不是不好用,而是"昨天还好的,今天更新了怎么规则全变了"。
3.2 最基础的接入姿势:手写一个 commit-msg 钩子
Git 钩子里和提交信息校验相关的是 commit-msg,它在用户编辑完提交信息之后、提交创建之前执行。我们要做的就是在 .git/hooks/commit-msg 里调用 gitru,把提交信息文件路径传进去。
新建 .git/hooks/commit-msg,写入:
bash复制#!/bin/sh
gitru check "$1"
然后给它加可执行权限:
bash复制chmod +x .git/hooks/commit-msg
就这么简单。接下来试一次不规范的提交:
bash复制git commit -m "fix bug"
gitru 会给出类似下面的输出并拦截提交:
code复制error: invalid commit message
- type "fix" is allowed, but subject is too vague
- header must include a scope in the allowed list
这里我故意把规则配置成"必须带 scope",让输出更有说服力。实际配置怎么定,完全取决于团队约定,后面第四节会详细说。
需要注意一个小坑:.git 目录是本地仓库私有的,不会跟着 Git 提交走。所以你手动写的这个钩子只对当前仓库有效,换台机器克隆仓库,钩子就没了。这就引出了团队共享钩子的问题。
3.3 让整个团队共用同一份钩子:core.hooksPath 才是正解
前面说的痛点,靠 Git 核心配置就能解决:Git 支持把钩子目录指定到项目内任意位置。
bash复制git config core.hooksPath .githooks
然后在项目根目录建一个 .githooks/commit-msg:
bash复制#!/bin/sh
gitru check "$1"
加上可执行权限后提交到仓库。以后任何人 clone 这个仓库,只要执行一次上面那条 git config 命令,就能用上同一份钩子。再配合一个 setup.sh 脚本或者 Makefile 目标,钩子版本就跟着代码仓库走了,不会出现"你的钩子比我的新"这种情况。
bash复制# setup.sh
#!/bin/sh
git config core.hooksPath .githooks
gitru check --version
这个方案比手动写 .git/hooks 科学得多,也比依赖 husky 通用得多——husky 本质上也是在帮你写钩子,但绑定 npm。Rust 项目、Go 项目、纯 Python 项目、甚至文档仓库,都能用同一套路子。
提示:core.hooksPath 是跟着仓库本地配置走的,不会自动应用到其他开发者机器上。团队里最好有一个统一的引导脚本,否则新人第一次提交还是会漏。我在实际推广时,是把这条命令写进了项目 README 的第一个章节,并且让 CI 在拉代码后自动检测钩子是否生效。
4. 规则配置详解:从 Conventional Commits 到团队自定义
4.1 配置文件长什么样
gitru 把规则收敛在一个 gitru.toml 里,和仓库一起版本管理。一个典型配置如下:
toml复制[message]
header_max_length = 72
require_scope = true
subject_min_length = 4
[types]
allowed = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"]
[scopes]
allowed = ["api", "ui", "docs", "cli", "core"]
[body]
required = true
min_length = 8
[regex_rules]
issue_ref = { pattern = "^(feat|fix):.*\\(#\\d+\\)$", error = "feat/fix 提交必须关联 issue 编号" }
选 TOML 而不是 JSON 或 YAML,是因为 TOML 对注释的支持友好,规则文件写出来像一份可读的"团队公约",后面的人看到注释就能明白每条规则的意图,而不是面对一坨 JSON 不敢动。
4.2 常用规则到底在卡什么
我不打算把每个字段念一遍文档,重点讲几组常见配置背后的逻辑:
header_max_length:为什么通常是 72?因为 Git 终端里 log 默认单行展示,超过 72 字符会被截断,信息丢失。这是从邮件补丁时代传下来的习惯,今天依然适用。按中文字符的话,我建议按实际 CJK 宽度计算,别让一个 40 个汉字的主语就把额度占满。require_scope:要不要强制 scope,取决于仓库结构。单仓多模块(monorepo)强烈建议开,否则 reviewer 很难从提交信息判断改动影响面;单模块小仓库可以不开,强制反而多此一举。types.allowed:建议遵循 Conventional Commits 的标准类型,不要自造词。自造类型意味着 changelog 工具、semver 工具都需要跟着适配,维护成本会悄悄转移。body.required:这条经常被吐槽"强制写 body 会降低效率"。我的看法是,对大型 feature 强制,对文档、样式类改动放行。规则要帮团队建立"轻重缓急"的意识,而不是一刀切。
4.3 自定义正则:规则引擎的天花板由你决定
内置规则覆盖 80% 的团队需求,剩下 20% 的奇奇怪怪约定,用正则兜底。比如很多团队要求提交关联需求单号,格式是 JIRA-123,或者破坏性变更必须在 footer 里带 BREAKING CHANGE。前者可以配一条正则规则,后者可以作为内置的 breaking change 检测由 gitru 直接识别。
用正则的时候我有个经验:宁可写得宽松,也不要写太紧。正则一旦写死,某天流程变了,第一个来找你的就是那些被误伤的同事。所以我会在规则里加一个 error 字段,让校验失败时输出人能读懂的提示,而不是抛一段正则表达式让人猜。
我自己的习惯是"内置规则打底 + 少量正则收口"。内置规则负责最常见的格式问题,正则只用来约束那些确有业务场景的提交标记,剩下的该放行就放行。
5. 和 commitlint 的对比:什么时候值得换掉 Node 系工具
5.1 两者差异
写这种对比容易引战,所以先声明:commitlint 是社区里很成熟的方案,解决过很多团队的痛点。gitru 不是要"秒杀"谁,而是提供一条更轻的路径。选型终究是看团队现状。两者的核心差异我整理成了一个表:
| 维度 | commitlint | gitru |
|---|---|---|
| 运行时 | Node.js,必须能跑 npm | 无,单二进制可执行文件 |
| 安装体量 | CLI 加依赖数十 MB 起步 | 几十 KB 到几百 KB |
| 启动耗时 | 数百毫秒起,CI 上可能上秒 | 毫秒级 |
| 配置方式 | 支持多格式,preset 生态丰富 | TOML 单文件,规则直观 |
| 可扩展性 | 通过 JS 插件机制任意扩展 | 内置规则 + 正则规则,够用但不做插件市场 |
| 钩子集成 | husky 等方案成熟 | 任意语言项目通用,靠 core.hooksPath |
| 适用项目 | Node/前端项目集成最顺滑 | 所有 Git 仓库,尤其是非 Node 项目 |
5.2 什么情况该换,什么情况该留
我给你的判断标准就三条:
- 团队项目是不是都是 JavaScript/TypeScript?如果是,commitlint 顺路用没问题,不用为了换而换。
- 仓库里有没有非 Node 项目?后端 Go、Python、Rust,或者纯文档仓库,再为它们维护一套 Node 依赖就太累了,这类项目换成 gitru 明显更舒服。
- CI 是不是经常被 npm install 拖累?如果是,切掉 commitlint 能省一大截构建时间。
说白了,前端大型 monorepo 用 commitlint 完全合理,它的 plugin 生态和 preset 丰富度是 gitru 短期内比不了的。但如果你维护的是多语言仓库,或者对构建速度敏感,gitru 这种"一个二进制走天下"的方案会让整个团队的接入成本低很多。
5.3 迁移时的一个提醒
commitlint 配置通常是 .commitlintrc.js 或者 package.json 里的 commitlint 字段,迁到 gitru.toml 时,规则语义能对应上的占多数,但并不是 1:1。我的建议是:迁移期间先跑两周"只报告不拦截"的模式(gitru 有 dry-run 选项),把团队里平时那些不规范的提交都暴露出来,再正式开启拦截。直接一刀切强制,大概率会在第一周就收获一堆吐槽,这种事急不得。
6. 实际接入时踩过的坑:几条完整排查链路
工具写出来和工具好用是两回事。gitru 在我自己仓库里跑了大半年,又在两个团队里推广过,中间踩过不少坑,挑几个有代表性的说说。
6.1 钩子写好了,但提交依然不被拦截:先查执行权限
最经典的问题:commit-msg 文件内容没问题,路径也对,但提交还是"咣"一下就过了。我排查过好多次,根因几乎都是同一个——文件没有可执行权限。
Git 钩子的本质是 shell 脚本,靠权限位决定能不能执行。我见过同事把钩子文件从 Windows 拷到 Linux,权限位变成 644,结果钩子完全被跳过。还有一次是 core.hooksPath 指向的目录下,commit-msg 写好了,但 setup 脚本里忘记 chmod,整个团队的钩子静默失效了一周,直到有人手动跑 gitru check 才发现。
排查这类问题有个固定套路:
bash复制git config core.hooksPath
ls -l .githooks/commit-msg
第一步看钩子路径对不对,第二步看权限有没有 x 位。都不对就顺手 chmod +x。如果路径显示为空,说明走的是默认的 .git/hooks,再去那边查。这个"先看路径、再看权限"的顺序很重要,很多人上来就改脚本内容,方向从一开始就错了。
6.2 Windows 环境下 CRLF 把正则规则全部干废
第二个坑更隐蔽。团队里有人用 Windows 开发,gitru 读提交信息文件时读到了 CRLF 结尾,而配置里的正则默认按 \n 设计,导致一行匹配全失败。明明在 mac 上好好的规则,Windows 同事一提交就报错。
问题不在 gitru 本身,而在输入数据。Git 在检出时可能按照 core.autocrlf 配置转换行尾,commit-msg 钩子里拿到的文件就带着 CRLF。我的处理方式有两个层面的兜底:一是在工具层,规则匹配前把 \r 干净地去掉;二是在团队规范层,仓库加一个 .gitattributes,把文本文件统一成 LF,把 core.autocrlf 设为 input。单靠工具扛不住所有环境差异,规则和环境配置得双管齐下。
如果你在排查时发现"同一份配置,有人报错有人不报",优先怀疑行尾差异。用 file 命令看一眼提交信息临时文件就能确认,不需要猜。
6.3 commit-msg 不是唯一关口:amend、merge、revert 都要考虑
严格说这不算工具本身的坑,而是接入策略的坑。commit-msg 在新建提交时触发,但团队里天天有人在用 git commit --amend、git merge、git revert。
amend 会重新走一遍 commit-msg 钩子,所以问题不大。容易被忽略的是 merge commit 和 revert commit。merge 提交的信息是 "Merge branch 'xxx'",revert 提交的信息是 "Revert "..."",这两类都不符合 Conventional Commits 的常规格式。如果配置太严格,会出现"常规提交被拦、工具生成的提交也被拦"的搞笑场面。
我的建议是:默认放行 merge 和 revert 开头的提交信息,专门加一条正则豁免,别让工具和 Git 自身的行为打架。Git 的默认行为本身也是合理的,校验工具应该理解 Git 的工作流,而不是死板地要求每一条提交都长成同一个模样。
7. 进阶玩法:把 gitru 接进 CI,让规范不再靠自觉
7.1 在流水线里多跑一步校验
本地钩子是第一道防线,但总有漏网之鱼:有人绕过钩子(git commit --no-verify),有人改了本地钩子配置,有人本地方便行事。所以 CI 里最好也跑一遍校验。gitru 本来就是单二进制,在 CI 里集成非常轻,以 GitHub Actions 为例,大概长这样:
yaml复制- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: |
curl -sSL https://example.com/gitru/releases/latest/download/gitru-linux-x86_64 -o /usr/local/bin/gitru
chmod +x /usr/local/bin/gitru
git log -1 --pretty=%B | gitru check --stdin
核心思路就三步:下载二进制、拿最新一条提交信息、传给 gitru 校验。如果你的流水线用的是本地缓存机制,也可以把 gitru 二进制缓存起来,连下载都省了。需要注意:actions/checkout 默认浅克隆深度为 1,要用 git log -1 拿当前提交信息的话,fetch-depth: 0 不是必须的,但如果你要校验 PR 的所有提交,就要显式拉全历史。
7.2 让工具输出的提示成为团队规范的一部分
很多团队规范落不了地,是因为规范藏在 Wiki 里没人看。让 gitru 在报错时给出可读的修正提示,等于把规范"写进了工具"。配合配置里的 error 字段,每个不满足的规则都会告诉开发者"你到底该怎么做",而不是甩一句冷冰冰的 "invalid"。
我还会把常用提交模板做成 .gitmessage 文件放在仓库里:
code复制feat(scope): 一句话描述改动
更详细的说明,为什么做、怎么做、影响面是什么。
Closes #123
配合 git config commit.template .gitmessage,开发者每次敲 git commit 就自动带出模板,从源头降低格式错误的概率。校验工具负责卡底线,模板负责提升体验,两者结合起来,团队提交信息的整洁度能肉眼可见地上一个台阶。
最后说点我自己的体会。做 gitru 这个工具,技术上的复杂度其实不高,真正难的是想清楚"什么该严、什么该松"。工具太严,团队抵触;太松,等于没装。我现在的原则是:格式类问题(type、scope、长度)一律卡死,因为这部分没有争议;内容类问题(body 必填、正则关联需求单号)按团队实际情况宽松处理,只约束真正有价值的。工具本身用什么语言、多少依赖,都不如这个尺度拿捏重要。gitru 选择 Rust 和零依赖,说到底只是让"强制执行规范"这件事变得足够轻,轻到没有人有借口拒绝它。如果你也想给自己的仓库上个保险,找个周末装上跑一周,你会发现 git log 变好看这件事,带来的快乐比想象中大得多。
