Commit Message 的最佳实践
文档用途
此文档讨论了如何恰当地编写 git commit message.
共识与目标
我们希望 git commit message 具备以下特点:
- 有意义. 能够从 commit message 中一眼看出所做的修改.
- 有规范. 所有 commit message 应该有相同的结构, 句法等.
同时最好能够做到:
- 信息密度大. 除了描述外, 还包括例如 Issue 或 PR 的编号.
- 心智负担低. 避免开发者为了写出好的 commit message 而焦虑.
- 通用且灵活. 能适用于不同的工作流 (远程 和 本地), 不同的项目规模,
以及不同的项目组织形式 (单仓库 monorepo 或多仓库 polyrepo).
术语和约定
在这篇文档中:
GitHub Pull Requests 简称 PR.
GitLab Merge Requests 简称 MR.
GitHub Milestones 和 GitLab Milestones 统称 Milestone.
Conventional
标准简述
对 commit message 的倡议中, 最常听说的是 Conventional Commits 标准.
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]feat!: send an email to the customer when a product is shippedfeat(api): send an email to the customer when a product is shippedchore!: drop support for Node 6
BREAKING CHANGE: use JavaScript features not available in Node 6.fix: prevent racing of requests
Introduce a request id and a reference to latest request. Dismiss
incoming responses other than from latest request.
Remove timeouts which were used to mitigate the racing issue but are
obsolete now.
Reviewed-by: Z
Refs: #123由于 Conventional Commits 官网写的很好, 且只需要 15 分钟就能看完,
因此在这里不错详细介绍, 只额外记录笔者的思考:
- 所有的官方案例都直接使用
动词原形(虽然标准里没有写明).
例如应该使用add而不是adds,added,adding. - 建议对于所有破坏性修改都使用
!标记, 因为更一目了然.
(虽然在标准中, "!" 或 "BREAKING CHANGEfooter" 任选一种方案即可). conventional commits标准可以被扩展, 例如通过配置 config-conventional.
要点在于: 单个工程的标准应该保持统一, 且应该通过 CI/CD 等流程确保标准被遵守.- 考虑好单个项目是否要使用
scope, 如果要使用, 又应该存在哪些scope.
例如 polyrepo 可能用不着, 而 monorepo 可能需要用类似feat(helm)来分类.
也有可能选择让scope与 feature flag 对应. 关键是要在项目中统一.
TIP
conventional commit 不仅是优秀的标准, 它还让 commit message 有了 被进一步处理 的能力.
例如 feat fix 等分类, 就可以被处理成 CHANGELOG.md 中不同类型的改动 (Added 和 Fixed).
例如标准中对 BREAKING CHANGE 的标注, 就可以用于 "按照 Semantic Versioning 发布版本".
远程检测
通过 CI/CD 来检查 PR/MR 的标题是否符合 conventional commits 标准.
这里记录 GitLab 中的配置, 其他 CI/CD 的案例请参考 CI setup - commitlint docs.
# 使用 GitLab CI/CD Job 检测 MR 标题是否满足 `conventional commits` 标准.
commitlint:
image: node:lts-alpine
before_script:
- apk add --no-cache git
- npm install --save-dev @commitlint/config-conventional @commitlint/cli
script:
- echo "${CI_MERGE_REQUEST_TITLE}" | npx commitlint理想情况下, 我们创建 PR/MR 之后, 可以相对随意地编写 commit message.
因为远程仓库的管理员可以做设置, 让 PR/MR 合并时被 squash,
这样一来我们的 commit message 就不会出现在主分支,
而 PR/MR 的 标题 将会作为合并后的 commit message.
本地检测
通过 git hook 配合 commitlint 在每次执行 git commit 命令时做检查.
如果不满足 conventional commits 标准, 则在 commit 时会报错并说明原因.
使用 git hook 与 commitlint 来检查 commit message
请确保安装了 node.js, 也就是说 npm 命令可以用. 在工程的根目录执行以下命令:
# 创建 package.json 文件, 并写入一行配置.
echo "{ "type": "module" }" > package.json
# 在工程内安装 commitlint 命令行工具以及配置模块.
npm install -D @commitlint/{cli,config-conventional}
# 创建配置文件, 并在其中写入基础配置.
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
# 通过不符合标准的 commit message 做检查, 预期效果: 报错.
echo "you shall not pass!" | npx commitlint
# 通过符合标准的 commit message 做检查, 预期效果: 没有输出.
echo "chore: setup commitlint" | npx commitlint通过以上步骤, 我们已经能够在工程内手动使用 commitlint 命令做检查.
接下来, 我们要使用 git hook, 使得每次 commit 时能自动做检查:
# 创建名为 commit-msg 的 git hook,
# 并在其中写入脚本, 使其执行 commitlint 命令.
tee .git/hooks/commit-msg << EOF
#!/bin/sh
npx commitlint --edit \$1
EOF
# 通过不符合标准的 commit message 做检查, 预期效果: 报错并终止 commit.
git commit -a -m "you shall not pass!"
# 通过符合标准的 commit message 做检查, 预期效果: 成功 commit.
git commit -a -m "chore: setup commitlint"注意: ./git 路径下的文件 不会 被 git 追踪, 因此不会被推送至远程仓库.
所有的协作者在首次 git clone 后, 都需要手动设置 git hook.
如果希望解决这个问题, 可以参考 这个方案.
也可以通过 commitizen 之类的工具, 在交互式命令行中编写 commit message.
只要通过这个流程编写出来的 commit message, 必然满足 conventional commits 标准.
WARNING
"在交互式命令行中编写符合标准的 commit message" 的体验不是很好, 感觉比较繁琐.
只要熟悉了 conventional commits, 其实很容易手动编写出符合标准的 commit message.
再配合基于 CI/CD 的 远程检测 或基于 git hook 的 本地检测, 更能从客观上确保符合标准.
工作流
远程驱动
"远程驱动的工作流" 的思路
把 Pull Request 或 Merge Request 作为唯一的源头 (即 "single source of truth").
将 PR/MR 的标题与描述包含在 commit message 内, 通过 CI/CD 保证符合 conventional commits.
对于需要多人协作的项目, 或是开发与维护周期足够长的项目, 建议使用此工作流.
工作流的具体方案举例:
PR/MR 的标题作为 commit message 的主体 (通过 CI/CD 确保其符合 conventional commits).
PR/MR 的描述直接作为 commit message description, 或在其末尾添加补充信息 (作为 footer).
发布版本时, 依靠 milestone 来安排版本发布计划 (连接相关 PR/MR 和 Issues).
若每个 commit message 中都包含了 PR 的编号, 就可以用这种方式来记录版本的更新内容:
在 GitHub Release 的描述中, 创建一个链接, 可点击跳转到对应的 Comparing changes 页面.


类似地, 在 GitLab 中可以通过 Milestone 作为 Release 与 Issue 及 MR 之间的桥梁.
例如下图就是一个 GitLab Release, 我们可以点击跳转到对应的 Milestone 页面.
而 Milstone 页面中将会列出所有与之相关的 Issue 和 MR.

对 CHANGELOG.md 必要性的讨论
虽然 keep a changelog 倡议中建议我们使用 CHANGELOG.md,
但是笔者认为在采用 "远程驱动的工作流" 时, 多数情况下 不必 这么做.
除非需要在远程仓库不可访问的情况下, 希望仅通过本地工程查看更新日志.
因为只要能确保 PR/MR 都有意义不零碎, 并且 commit message 与之对应,
那么 commit message 就是最好的更新日志 (考虑到 git tag 提供了版本号信息).
且我们完全可以通过 GitHub/Gitlab Release 来跳转到适合查看更新日志的页面.
在这种情况下, 如果出于某种原因必须要提供 CHANGELOG.md,
那么应该通过命令或脚本来 自动生成 而非手动编写.
本地驱动
"本地驱动的工作流" 的思路
把 commit message 作为唯一的源头 (即 "single source of truth").CHANGELOG.md 中, 按照 commit message 提供的信息, 将它们分版本且分类列出.commitizen 之类的命令行工具也会根据 "是否存在破坏性修改" 来恰当地更新版本号.
开发者需要做的只是: 恰当编写 commit message, 并在需要发布新版本时执行一行命令.
出于某些原因, 我们可能不希望为某个项目启用 "远程驱动的工作流":
例如纯个人项目, 希望在想到什么时直接动手 (而不是 "先规划再实现").
例如不希望深入使用 Git 托管平台的拓展功能 (Pull Request, Release 等).
此时我们可以使用 "本地驱动的工作流".
因为缺少了 GitHub/GitLab Release, 我们就需要使用 CHANGELOG.md 来记录改动.
由于缺少了 PR/MR, 我们就需要使用 squash 等方法, 使得每个 commit 有意义 (不零碎).
工作流的具体方案举例:
确保项目的 commit message 始终遵循 conventional commits (或基于它的变体).
使用 commitizen 之类的命令行工具, 通过类似于 cz bump 这样的命令来发布新版本.
这将会自动创建 git tag 并根据 "自上个版本起的 commit message" 更新 CHANGELOG.md.
与 commitizen 类似的工具还有: python 版 commitizen 以及 cz-git.
AI 助力
思路简述
GitButler 的各项功能中, 最让人眼前一亮的是 "通过 AI 生成 commit message".
cz-git 也支持通过命令来自动生成 commit message (需要提供 OpenAI API Token).
笔者认为比较合适的使用场景是:
PR/MR 的标题和描述依然由人来手动编写 (毕竟这代表着开发者的意图),
但那些 "打算推送至 PR/MR 所在分支的 commit" 则适合用 AI 来编写.
因为 AI 可以从 git diff 了解到 "这个 commit 发生了什么具体的改动".
这样既能够降低我们编写 commit message 的心智负担和时间成本,
也能够提高 PR/MR 中 commit message 的质量 (它肯定好过 "做了一些修改").
且由于 PR/MR 在合并至主分支时会被 squash, 因此细枝末节最终会被剪除,
只在主分支上留下干净整洁且有意义的 commit history.
具体方案
可以借助 aicommits 命令行工具配合 OpenAI API Token 来实现.
GitHub 主页的 README.md 将用法写得很清楚, 这里就不再重复了.
比较麻烦的问题是: 如果想要调用 OpenAI API Token, 就需要绑定信用卡.
而国内的信用卡不容易绑定上. 因此这里再介绍另外一个方案:
通过 Cloudflare Workers AI 来调用大语言模型的 API.
然后自己写一个命令行工具来实现类似于 aicommits 的功能.
todo: 尝试使用 Workers AI 方案.