告别 Conventional Commits:它让我们关注了错误的东西
- #Git
- #提交规范
- #Conventional Commits
- #scope-prefixed commi
- #Hacker News
停止使用 Conventional Commits
你几乎肯定见过 Conventional Commits。它可能出现在你使用的开源项目的变更日志中,也可能是你参与贡献的开源项目强制要求的提交格式。很多人对它信誓旦旦,而我则对它嗤之以鼻。
尽管大量流行开源项目在使用它,Conventional Commits 仍然是一个主动有缺陷的标准,它鼓励关注错误的事情,并且未能兑现其承诺。
关注的失败
Conventional Commits 承诺为提交消息添加语义,帮助开发者和最终用户理解提交中的变更。然而,它以一种惊人的方式失败了。为了证明这一点,让我们看看 conventional commit 的构成。根据 Conventional Commit 网站,提交消息应格式化为:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
提交的标题行有一个 <type>(如 fix、feat、chore、docs 或 refactor¹)描述变更类型。其后是可选的 scope,然后是描述。
这种格式有一个重大缺陷:type 被优先于 scope。这完全是颠倒的。
范围 > 类型
变更的范围(变更的主题)是提交中最重要的部分。为了证明这一点,让我们考虑为什么以下每个利益相关者都比变更类型更关心变更的范围:
-
贡献者:作为项目的贡献者,你常常需要阅读提交日志,以识别代码库中与代码的某个区域相关的变更。原因包括:想了解自上次贡献以来发生了什么;试图理解项目的整体走向;在拉取或变基时查找可能与正在进行的工作冲突的提交。在阅读提交日志时,你关注的是哪些区域被触及。你真的不关心变更的类型,你关心的是变更的范围。
-
调试者:在调查 bug 时,你常常想翻阅提交日志,看看哪些变更可能触及了与 bug 显现的组件相关的区域。同样,范围是最重要的信息。变更类型完全无用,因为任何类型的变更都有可能引入 bug。(我相信我们都经历过编写 bugfix 却导致另一个 bug 的情况。)
-
事故响应者:当生产环境宕机时,扫描提交日志以查看宕机前后做出的变更是一种有效的方式,可以识别可能导致问题的区域。此时,范围是你拥有的最重要的信息。例如,如果你看到在入站 API 错误飙升的顶端有一个与 auth 范围相关的提交,它很可能是问题的罪魁祸首。同样,类型无关紧要,因为任何变更都可能引入 bug。
那么 Conventional Commits 做了什么?它大大降低了范围的优先级,以至于它是可选的!为什么范围是可选的?没有范围的提交就像没有主语的句子!更糟的是,Conventional Commits 将类型提升到了提交消息的前面。Conventional Commits 完全颠倒了范围和类型的优先级。
类型是冗余和限制性的
你可能会想:“虽然顺序颠倒,但提交类型至少还是重要的吧?”我的回答是“不”。一个提交的描述几乎总能告诉你变更的类型!考虑以下提交消息示例:
fix(compiler): prevent namespaced SVG <style> elements from being stripped
即使你只有描述,也很明显这是一个 bugfix!提交标题行的空间已经宝贵,浪费字符在类型上并无帮助!但它常常比无用更糟糕;它往往是限制性的。看这个提交消息:
refactor(core): Update webmcp support to use document.modelContext
这个提交更新了 core 组件中的 webmcp 功能以支持 document.modelContext 和 navigator.modelContext,那么它到底是 bugfix、重构还是新功能?我认为它兼备!但同样,唯一重要的是它是针对 core/webmcp 组件的变更。
Conventional Commits 从根本上关注了错误的事情(提交类型),并且贬低了范围(这才是人们真正关心的)。
破碎的承诺
我们已经确定 Conventional Commits 的格式很糟糕,但它总该提供一些好处吧。让我们阅读“为什么要使用 Conventional Commits”部分,看看是否有任何理由成立。
自动生成 CHANGELOG
这是 Conventional Commits 最大的承诺:你可以运行类似 git-cliff 或 conventional-changelog 的工具,根据自上次发布以来的提交生成变更日志。这甚至是个好主意吗?不!变更日志的受众与提交日志的受众完全不同!
- 变更日志面向用户,用户关心的是理解版本之间的功能差异。他们关心的是从业务/功能角度发生了什么变化。
- 提交日志面向开发者,开发者关心的是阅读代码库随时间变化的故事。他们关心的是从范围角度发生了什么变化。
如你所见,这是两种完全不同的粒度,任何将它们结合的努力都会导致次优结果。原因如下:
- 在任何中等复杂度的项目中,一个显著特性通常需要多个提交才能完成。特性落地的过程(如提交日志所记录的)对开发者和贡献者有价值,但对最终用户无用。最终用户只关心新功能,不关心它是如何构建的!
- 正如 Rich 指出的,回退对 Conventional Commits 来说是有问题的。从提交日志的故事角度看,回退提交对开发者很重要,但对最终用户来说,被回退的变更等同于没有做过的变更。
自动确定语义化版本提升(基于落地的提交类型)
这听起来不错,但软件工程的现实常常严重干扰准确完成此任务的可行性。考虑以下情况:
- 回退:想象你引入了一个破坏性变更,但实际上破坏性太大以至于不得不回退?你的工具会检测到破坏性变更并递增主版本号,尽管破坏实际上已经回退,实际上没有破坏性变更。
- 意外破坏:也许破坏很微妙,你在做出变更时没有意识到它是破坏性变更。只有事后才意识到它是破坏性的。你会错误地递增次要/补丁版本,而实际上需要主版本号提升。
- 事后修复:假设你后来添加了一个提交,该提交与之前破坏性提交的组合结果是非破坏性的。类似于回退的情况,工具会错误地识别为破坏性变更。
在这种情况下,你可以通过变基重写历史,但这常常会破坏或被工作流阻止。它还会向试图贡献的贡献者展示修正主义历史,降低提交日志所讲述故事的可靠性。
向团队成员、公众和其他利益相关者传达变更的性质
正如我们到目前为止所确定的,团队成员和公众对变更日志和提交日志有完全不同的需求。Conventional Commits 两者都未能解决。
触发构建和发布流程
这只是一个坏主意。假设你只对触及代码的提交运行自动化安全检查,然后有人创建了一个名为 docs: fix typos 的特洛伊木马提交,实际上在身份验证子系统中引入了漏洞?显然,这种恶意行为有希望被代码审查发现,但自动化工具被绕过了,将识别问题的责任推给了人类。
计算资源很廉价,只需使用 git diff 识别更改的文件(再次强调,范围),并基于此运行构建/发布流程。
让人们更容易为你的项目做贡献,通过允许他们探索更结构化的提交历史
确实更结构化。但更容易贡献吗?完全不是(我们已经详细证明了这一点)。
Conventional Commits 的“卖点”没有一个站得住脚。
Conventional Commits 也很难应用于项目
你本应定义自己的一套“类型”,但实际上几乎每个人都直接使用 commitlint 的默认值,这些默认值往往不适合个别项目的具体情况。这个问题在企业环境中尤为严重,变更管理和审计要求常常强制在每个提交消息中包含工单号。<scope> 字段显然是放置它的位置,但这最终会用完全无用的工单号替换掉 Conventional Commit 中唯一有用的元数据。
更好的方式
那么你应该怎么做?跟随真正成功的软件项目如 Linux、FreeBSD、Git、Go 和 NixOS 的领导!这些项目有什么共同点?它们都使用 scope-prefixed 的提交消息(其中“scope”被定义为与实际项目相关)。通常,在给定项目上使用的 scope 是不言自明的。对于 Linux 内核,子系统是自然的 scope。对于 Go 项目,包路径是自然的 scope。对于使用微服务架构的项目,微服务名称是自然的 scope。
以下是项目及其提交格式指南的一些示例:
| 项目 | 格式 | 示例 |
|---|---|---|
| Linux | subsystem: description | i2c: virtio: mark device ready before registering the adapter |
| FreeBSD | prefix: Description | linuxulator: Return EINVAL for invalid inotify flags |
| Git | area: description | gitlab-ci: update macOS image |
| Go | package: description | net/http/cookiejar: add godoc links |
| nixpkgs | pkg-name: description | xwayland: 24.1.11 -> 24.1.12 |
| Node.js | subsystem: description | stream: fast-path stateless transform flush results |
不幸的是,尽管被一些有史以来最成功的开源项目使用,这种提交风格似乎输掉了品牌战。我打算改变这一点。介绍 scopedcommits.com。该网站致力于倡导回归提交消息的理智,并将变更日志生成与提交日志管理分离。
结论
Conventional Commits 声称的优势实际上是虚幻的,行业没有看到使用它作为标准带来的任何实质好处。然而,不幸的是,Conventional Commits 似乎在开源项目中变得相当流行,由于这个原因,AI 似乎习惯默认使用它来生成提交消息。这导致了反模式提交消息在项目中的传播。
本文的目标是反对 Conventional Commits 的主导地位,并演示有更好的方式来组织提交消息。但如果这篇文章没有说服你停止使用 Conventional Commits,我期待在评论区看到激烈的争论。
¹ 技术上,Conventional Commits 规范只定义了 fix 和 feat,将其他类型留给各个项目指定,然而大多数项目最终使用了 commitlint 定义的类型,因此我在列表中包含了其中一些。 ↩
评论