1. Commit Message 的重要性
- 可以使自己或者其他开发人员能够清晰地知道每个 commit 的变更内容,方便快速浏览变更历史,比如可以直接略过文档类型或者格式化类型的代码变更。
- 可以基于这些 Commit Message 进行过滤查找,比如只查找某个版本新增的功能:git log --oneline --grep "feat|fix|^perf"。
- 可以基于规范化的 Commit Message 生成 Change Log。
- 可以依据某些类型的 Commit Message 触发构建或者发布流程,比如当 type 类型为 feat、fix 时我们才触发 CI 流程。
- 确定语义化版本的版本号。比如 fix 类型可以映射为 PATCH 版本,feat 类型可以映射为 MINOR 版本。带有 BREAKING CHANGE 的 commit,可以映射为 MAJOR 版本。
总结来说,一个好的 Commit Message 规范可以使 Commit Message 的可读性更好,并且可以实现自动化。
2. 规范
2.1. 应该有的样子
社区已有多种 Commit Message 的规范了,例如 JQuey、Angular 等,这些规范的关系如下所示
而不管什么样的规范,个人认为一个好的 Commit Message 从结构上应该由以下三个部分组成:
-
主题
主题应该只有一行,是对你 commit 的总结。这个句子应该用祈使句的语气,以大写字母开头,不以句号为结束符,并且字数应该少于等于 50 。
这个主题句子可以很好地填充 “This commit will ......” 这个句子,比如 “add new neural network model to back-end”。
-
正文
正文应该包含了 commit 的要点,通过正文可以了解更改的详细信息。在正文中你应该详细地阐述 commit 所做的改变,以及阐述你这么做的上下文,比如解释为什么做出这样的改变,为什么选择使用这种方式去实现你的改变,以及其他可以理解这个 commit 的背后思考过程。同时没有必要重复阐述那些通过 diff 命令就可以看出来的改变,也没有必要一行一行的进行解释。一定要专注于更高级的细节,这些细节是通过读代码不好发现的。总的来说,正文内容的目标是提供这么改变的上下文,这个上下文主要包括修改的东西和目标。
另外对于一些很小的 commits 来说,比如 “fixing a typo” 这种的,那么就可以不需要正文了,因为主题句中已经提供了足够多的信息了。
-
结束语
结束语应该是 commit message 的最后一行。这一行中你可以放一些关于 commit 的有用的 meta-data。比如 Github issue numbers,co-author names 和额外的链接。这些可以帮助你找到跟这个 commit 有关的重要信息。
2.2. Angular 规范
Angular 规范在功能上能够满足开发者 commit 需求,在格式上清晰易读,目前也是用得最多的。Angular 规范是一种语义化的提交规范(Semantic Commit Messages),语义化的提交规范包含两方面:
- 语义化:Commit Message 都会被归为一个有意义的类型,用来说明本次的 commit 类型。
- 规范化:Commit Message 遵循预先定义好的规范,比如 Commit Message 格式固定、都属于某个类型,这些规范不仅可被开发者识别也可以被工具识别。
下面是 Angular 规范的一个例子:
Angular 规范的 Commit Message 包含了三部分:Header、Body、Footer,格式如下所示。其中 Header 是必需的,Body 和 Footer 都是可以省略的。其中 Header、Body、Footer 之间必须要有空行。
<type>[optional scope]: <description>
// 空行
[optional body]
// 空行
[optional footer(s)]
2.2.1. Header
Header 部分只有一行,包含了三个字段:type(必选)、scope(可选,有的话需要用 () 括起来)和 description(subject,必选)。<type>[optional scope] 后必须紧跟冒号,冒号后面必须紧跟空格。
-
type 字段,这个字段用来说明 commit 的类型。
Angular 规范中常见的 type 如下图所示,其中这些 type 大致可以归为两大类:
- Production:这类修改会影响最终的用户和生产环境的代码。所以对于这种改动,我们一定要慎重,并在提交前做好充分的测试。比如,在做 code review 的时候,如果遇到 Production 类型的代码,一定要认真 Review,因为这种类型,会影响到现网用户的使用和现网应用的功能。
- Development:这类修改一般是项目管理类的变更,不会影响最终用户和生产环境的代码,比如 CI 流程、构建方式等的修改。遇到这类修改,通常也意味着可以免测发布。
如何确定一个 commit 所属的 type 呢?可以通过下面这张图来确定。
-
如果我们变更了应用代码,比如某个 Go 函数代码,那这次修改属于代码类。在代码类中,有 4 种具有明确变更意图的类型:feat、fix、perf 和 style;如果我们的代码变更不属于这 4 类,那就全都归为 refactor 类,也就是优化代码。
有冲突后的 commit 可以使用 refactor。
-
如果我们变更了非应用代码,例如更改了文档,那它属于非代码类。在非代码类中,有 3 种具有明确变更意图的类型:test、ci、docs;如果我们的非代码变更不属于这 3 类,那就全部归入到 chore 类。
Angular 的 Commit Message 规范提供了大部分的 type,在实际开发中,我们可以使用部分 type,或者扩展添加我们自己的 type。但无论选择哪种方式,我们一定要保证一个项目中的 type 类型一致。
-
scope 字段,scope 是用来说明 commit 的影响范围的,它必须是名词。
显然,不同项目会有不同的 scope。在项目初期,我们可以设置一些粒度比较大的 scope,比如可以按组件名或者功能来设置 scope;后续,如果项目有变动或者有新功能,我们可以再用追加的方式添加新的 scope。
这里想强调的是,scope 不适合设置太具体的值。太具体的话,一方面会导致项目有太多的 scope,难以维护。另一方面,开发者也难以确定 commit 属于哪个具体的 scope,导致错放 scope,反而会使 scope 失去了分类的意义。
在指定 scope 时需要遵循我们预先规划的 scope。除此之外,我们要将 scope 文档化,放在类似 devel 这类文档中。比如下面这样
参考:https://github.com/marmotedu/iam/blob/master/docs/devel/zh-CN/scope.md
-
subject 字段,即 description 部分。
subject 是 commit 的简短描述,必须以动词开头、使用现在时(比如,我们可以用 change,却不能用 changed 或 changes)。这个动词的第一个字母必须是小写,通过这个动词,我们可以明确地知道 commit 所执行的操作。此外,subject 的结尾不能加英文句号。
2.2.2. Body
Body 部分是对本次 commit 的更详细描述,是可选的。
Body 部分可以分成多行,而且格式也比较自由。Body 中需要包含修改的动机,以及跟上一版本相比的改动点。
2.2.3. Footer
Footer 部分用来说明本次 commit 导致的后果,是可选的。
在实际应用中,Footer 通常用来说明不兼容的改动和关闭的 Issue 列表,格式如下所示:
BREAKING CHANGE: <breaking change summary>
// 空行
<breaking change description + migration instructions>
// 空行
// 空行
Fixes #<issue number>
-
不兼容的改动:如果当前代码跟上一个版本不兼容,需要在 Footer 部分,以 BREAKING CHANG: 开头。后面跟上不兼容改动的摘要。之后空一行,来说明变动的描述、变动的理由和迁移方法。示例如下所示:
BREAKING CHANGE: isolate scope bindings definition has changed and the inject option for the directive controller injection was removed. To migrate the code follow the example below: Before: scope: { myAttr: 'attribute', } After: scope: { myAttr: '@', } The removed `inject` wasn't generaly useful for directives so there should be no code using it.
-
关闭的 Issue 列表:关闭的 Bug 需要在 Footer 部分新建一行,并以 Closes 开头列出,例如:Closes #123。如果关闭了多个 Issue,可以这样列出:Closes #123, #432, #886。
Change pause version value to a constant for image Closes #1137
2.2.4. Revert Commit
除了 Header、Body 和 Footer 这 3 个部分,Commit Message 还有一种特殊情况:如果当前 commit 还原了先前的 commit,则应以 revert: 开头,后跟还原的 commit 的 Header。在 Body 中必须写成 This reverts commit ,其中 hash 是要还原的 commit 的 SHA 标识。例如,
revert: feat(iam-apiserver): add 'Host' option
This reverts commit 079360c7cfc830ea8a6e13f4c8b8114febc9b48a.
2.2.5. 其他
- 为了使 Commit Message 在 GitHub 或者其他 Git 工具上更加易读,往往会限制每行 message 的长度。根据需要,可以限制为 50/72/100 个字符,这里我将长度限制在 72 个字符以内(也有一些开发者会将长度限制为 100,你可根据需要自行选择)。
- 为了更好地遵循 Angular 规范,建议你在提交代码时养成不用 git commit -m,即不用 -m 选项的习惯,而是直接用 git commit 或者 git commit -a 进入交互界面编辑 Commit Message。这样可以更好地格式化 Commit Message。
3. Commit Message 规范自动化
有一定的规范之后,我们可以使用一些工具,来自动化地做以下这些事情:
- Commit Message 生成和检查功能:生成符合 Angular 规范的 Commit Message、Commit Message 提交前检查、历史 Commit Message 检查。
- 基于 Commit Message 自动生成 CHANGELOG 和 SemVer 的工具。
可以通过以下几个工具自动完成上面的功能:
- commitizen-go(https://github.com/lintingzhen/commitizen-go):使你进入交互模式,并根据提示生成 Commit Message,然后提交。
- commit-msg:githooks,在 commit-msg 中,指定检查的规则,commit-msg 是个脚本,可以根据需要自己写脚本实现。这门课的 commit-msg 调用了 go-gitlint 来进行检查。
- go-gitlint(https://github.com/llorllale/go-gitlint):检查历史提交的 Commit Message 是否符合 Angular 规范,可以将该工具添加在 CI 流程中,确保 Commit Message 都是符合规范的。
- gsemver(https://github.com/arnaud-deprez/gsemver):语义化版本自动生成工具。
- git-chglog(https://github.com/git-chglog/git-chglog):根据 Commit Message 生成 CHANGELOG。
4. 规范推荐网址
https://www.conventionalcommits.org/en/v1.0.0-beta.4/
5. Commit 相关的 3 个重要内容
除了 Commit Message 规范之外,在代码提交时,我们还需要关注 3 个重点内容:提交频率、合并提交和 Commit Message 修改
5.1. 提交频率
在实际项目开发中,如果是个人项目,随意 commit 可能影响不大,但如果是多人开发的项目,随意 commit 不仅会让 Commit Message 变得难以理解,还会让其他研发同事觉得你不专业。因此,我们要规定 commit 的提交频率。
进行 commit 的时候一般分为两种情况:
- 只要我对项目进行了修改,一通过测试就立即 commit。比如修复完一个 bug、开发完一个小功能,或者开发完一个完整的功能,测试通过后就提交。
- 我们规定一个时间,定期提交。这里我建议代码下班前固定提交一次,并且要确保本地未提交的代码,延期不超过 1 天。这样,如果本地代码丢失,可以尽可能减少丢失的代码量。
5.2. 合并提交
上述两种方式提交代码,可能会导致代码 commit 比较多,看起来比较随意。另外,有时候想等一个功能开发完成之后,将所有 commit 放在一个 commit 中。这个时候可以在最后合并代码或者提交 pull request 前,可以使用 git rebase -i <commit ID>
将若干个 commit 合并为一个 commit。
如果有太多的 commit 需要合并的话,可以尝试撤销过去的 commit,然后重新提交一个新的 commit。但是需要说明的一点是,除了 commit 实在太多的时候,一般情况不采用这种方法,比较粗暴,而且之前提交的 commit message 都得重新提交一遍。
$ git reset HEAD~3
$ git add .
$ git commit -am "feat(user): add user resource"
合并提交的建议:在把新的 commit 合并到主干的时候,最好只保留 2-3 个 commit 记录。
5.3. 修改 Commit Message
有时候我们希望修改某次 commit message,那么有以下两种方式:
- git commit --amend:修改最近一次 commit 的 message;
- git rebase -i:修改某次 commit 的 message。
需要注意的是:
-
Commit Message 是 commit 数据结构中的一个属性,如果 Commit Message 有变更,则 commit ID 一定会变,git commit --amend 只会变更最近一次的 commit ID,但是 git rebase -i 会变更父 commit ID 之后所有提交的 commit ID。
-
如果当前分支有未 commit 的代码,需要先执行 git stash 将工作状态进行暂存,当修改完成后再执行 git stash pop 恢复之前的工作状态。