跳到主要内容

如何编写 Git 提交信息

信息

译自 How to Write a Git Commit Message,作者 cbeams,转载已获许可。译文可能存在疏漏,请参照原文。

提交信息很重要。这篇文章告诉你怎样写好它。


引言

随便打开一个 Git 仓库的 log,你多半会看到一堆乱七八糟的提交说明。看看我早年往 Spring 提交的这些"杰作"

$ git log --oneline -5 --author cbeams --before "Fri Mar 26 2009"

e5f4b49 Re-adding ConfigurationPostProcessorTests...
2db0f12 fixed two build-breaking issues...
147709f Tweaks to package-info.java files
22b25e0 Consolidated Util and MutableAnnotationUtils classes into existing AsmUtils
7f96f57 polishing

惨不忍睹。再来看看同一个仓库里稍新一些的提交:

$ git log --oneline -5 --author pwebb --before "Sat Aug 30 2014"

5ba3db6 Fix failing CompositePropertySourceTests
84564a0 Rework @PropertySource early parsing logic
e142fd1 Add tests for ImportSelector meta-data
887815f Update docbook dependency and generate epub
ac8326d Polish mockito usage

你喜欢哪个?

前者长短不一、格式各异。后者简洁、一致。前者不费什么力气就能搞出来,后者则不可能靠运气撞上。

当然也有例外。Linux 内核Git 本身的提交 log 就很典范。Spring BootTim Pope 管的项目也是。

这些仓库的贡献者清楚一件事:要告诉别的开发者(以及几个月后的自己)这次修改的上下文,没有比一条精心编写的提交信息更好的办法了。diff 能说明改了什么,但只有提交信息能说清为什么改。Peter Hutterer 说得很好

重新找回一段代码的上下文是件费劲的事。虽然完全避免不可能,但我们的精力应该花在尽量减少它上。提交信息正好能做到这一点。也因此,一条提交信息就能看出一个人是不是好协作者

如果你从来没琢磨过"好提交信息"长什么样,那很可能是因为你很少用 git log 以及它那套工具。这地方有个恶性循环:历史又乱又杂,所以你懒得看、懒得管;因为没人看、没人管,它就继续又乱又杂。

但一条精心打理过的 log,既好看,也好用。git blamerevertrebaselogshortlog 这些子命令真能派上用场了。review 别人的提交和 pull request 变成一件值得做的事——甚至能独立完成。想搞清楚几个月甚至几年前某段代码为什么那样写,不再是一件不可能的事,而且效率还挺高。

一个项目能活多久,很大程度上看它好不好维护。而对维护者来说,没有什么工具比项目的 log 更有分量。花点时间学学怎么把它管好,值。一开始可能嫌麻烦,慢慢就习惯了,再后来,所有人都会引以为豪。

这篇文章只讲最基础的一件事:每条提交信息该怎么写。提交压缩之类的进阶话题不在这讨论——也许后面再写。


团队要定好规矩

大多数编程语言对什么事算"地道"都有公认的约定——怎么命名、怎么排版,诸如此类。细节上当然有分歧,但多数人都同意:挑一套规矩,大家一起照着来,比各写各的乱成一团强得多。

团队怎么看待提交 log,道理一样。想维护一份有用的修订历史,首先得定好提交信息规范,至少说清楚三件事:

  • 格式:用什么标记语法、在哪里换行、语法和大小写规则、标点怎么用。把规矩定清楚,不留猜测空间,让一切尽可能简单。最后你会得到一份出奇一致的 log,不仅读着舒服,而且真的会有人经常去读。
  • 内容:提交信息的正文(如果有的话)应该写什么?不应该写什么?
  • 元数据:issue 编号、pull request 编号这些东西怎么引用?

好消息是,怎么写才算"地道"的 Git 提交信息,社区早就有共识了。很多约定甚至已经内置在 Git 各条命令的行为里。你不需要从头造轮子,照着下面七条做就行。


七条准则

1. 用空行把标题和正文隔开

摘一段 git commit 手册里的原话:

虽然不是强制要求,但最好用一行简短(50 字符以内)的文字概括改动,空一行,再展开细说。提交信息里第一个空行之前的部分算作标题,Git 到处都在用它。比如 git-format-patch(1) 会把提交转成邮件,标题放 Subject 行,剩下的放正文。

先说清楚:不是每条提交都要标题加正文。改动简单、不需要多解释的时候,一行就够了:

Fix typo in introduction to user guide

没什么可补充的了。如果真有人好奇那个 typo 是什么,直接看改动就行—— git showgit diff 或者 git log -p

在命令行提交这种东西,用 -m 最省事:

$ git commit -m"Fix typo in introduction to user guide"

但碰到需要解释背景和动机的提交,正文是跑不掉的。比如:

Derezz the master control program

MCP turned out to be evil and had become intent on world domination.
This commit throws Tron's disc into MCP (causing its deresolution)
and turns it back into a chess game.

带正文的提交用 -m 就不好写了。老老实实找个文本编辑器。如果还没给 Git 配过命令行编辑器,翻翻 Pro Git 的相关章节

不管怎样,浏览 log 的时候,标题和正文分开写的好处就显出来了。这是一条完整的 log:

$ git log
commit 42e769bdf4894310333942ffc5a15151222a87be
Author: Kevin Flynn <kevin@flynnsarcade.com>
Date: Fri Jan 01 00:00:00 1982 -0200

Derezz the master control program

MCP turned out to be evil and had become intent on world domination.
This commit throws Tron's disc into MCP (causing its deresolution)
and turns it back into a chess game.

git log --oneline 只打标题:

$ git log --oneline
42e769 Derezz the master control program

git shortlog 按作者分组,同样只显示标题,保持简洁:

$ git shortlog
Kevin Flynn (1):
Derezz the master control program

Alan Bradley (1):
Introduce security program "Tron"

Ed Dillinger (3):
Rename chess program to "MCP"
Modify chess program
Upgrade chess program

Walter Gibbs (1):
Introduce protoype chess program

Git 里还有很多功能依赖标题和正文的区分——但前提是中间必须有那个空行,缺了全都不好使。

2. 标题控制在 50 字符以内

50 不是硬杠杠,差不多就行。控制在这个长度,标题可读性好,也能逼着自己想一想:到底怎样才能用最短的话把事说清楚。

如果实在总结不了,说明这次提交塞了太多改动。尽量做原子提交

GitHub 的界面本身就认这套规矩。超过 50 字符会提醒你:

GitHub 警告

超过 72 字符的标题会截断加省略号:

GitHub 截断

所以冲着 50 去,但别超过 72。

3. 标题首字母大写

这个没什么好说的。所有标题,首字母大写。

比如:

Accelerate to 88 miles per hour

别写成:

accelerate to 88 miles per hour

4. 标题末尾不加句号

标题不需要句尾标点。本来就在省字数,句号没必要占个位子。

比如:

Open the pod bay doors

别写成:

Open the pod bay doors.

5. 标题用祈使语气

祈使语气,说白了就是"发号施令"的口气。比如:

  • 把房间打扫干净
  • 关门
  • 把垃圾扔了

你正在读的这七条,每一条都是祈使语气写的("正文换行到 72 字符",等等)。

祈使语气听起来可能有点冲,所以我们平时说话很少用它。但它用在 Git 提交标题上,刚刚好。一个原因是:Git 自己帮你创建提交的时候,用的就是祈使语气

比如 git merge 的默认信息:

Merge branch 'myfeature'

git revert 的默认信息:

Revert "Add the thing with the stuff"

This reverts commit cc87791524aedd593cff5a74532befe7ab69ce9d.

在 GitHub 上点 merge 按钮:

Merge pull request #123 from someuser/somebranch

所以用祈使语气写提交信息,不过是在顺着 Git 自己的习惯走:

  • Refactor subsystem X for readability
  • Update getting started documentation
  • Remove deprecated methods
  • Release version 1.0.0

刚开始写可能会不习惯。我们平时说话更常用陈述语气——描述事实。于是提交信息就经常写成了这样:

  • Fixed bug with Y
  • Changing behavior of X

有时候干脆写成了内容清单:

  • More fixes for broken stuff
  • Sweet new API methods

想再也不搞混,记住这条规则就行:

一条格式正确的 Git 提交标题,应该能接在下面这句话后面:

If applied, this commit will _____

比如:

  • If applied, this commit will refactor subsystem X for readability
  • If applied, this commit will update getting started documentation
  • If applied, this commit will remove deprecated methods
  • If applied, this commit will merge pull request #123 from user/branch

换成人话,就是"这个提交会干嘛干嘛"——能顺下来的就对,顺不下来的就是写错了:

  • 这个提交会修复了 Y 的 bug
  • 这个提交会更改 X 的行为
  • 这个提交会更多修复
  • 这个提交会超赞的新 API

记住:祈使语气只管标题。正文可以宽松些。

6. 正文在 72 字符处换行

Git 不会帮你自动换行。正文写到右边距的时候,得自己敲回车。

建议 72 字符就换,给 Git 留够缩进的空间,整体不超过 80 字符。

好的编辑器能帮你。比如把 Vim 配置成写 Git 提交时自动在 72 字符换行,很简单。至于 IDE,老实说,对提交信息换行的支持一直很烂(IntelliJ IDEA 最近几个版本倒是终于赶上来了)。

7. 正文解释"做了什么"和"为什么",别解释"怎么做的"

Bitcoin Core 的这条提交是个很好的例子:

commit eb0b56b19017ab5c16c745e6da39c53126924ed6
Author: Pieter Wuille <pieter.wuille@gmail.com>
Date: Fri Aug 1 22:57:55 2014 +0200

Simplify serialize.h's exception handling

Remove the 'state' and 'exceptmask' from serialize.h's stream
implementations, as well as related methods.

As exceptmask always included 'failbit', and setstate was always
called with bits = failbit, all it did was immediately raise an
exception. Get rid of those variables, and replace the setstate
with direct exception throwing (which also removes some dead
code).

As a result, good() is never reached after a failure (there are
only 2 calls, one of which is in tests), and can just be replaced
by !eof().

fail(), clear(n) and exceptions() are just never called. Delete
them.

翻一下要点:去掉 serialize.h 里几个没用的变量和方法。exceptmask 和 setstate 这俩东西看起来在干活,其实每次调用都在原地抛异常,毫无意义。删掉之后顺便清掉了一段死代码,good() 也就能直接用 !eof() 替代了。

去瞄一眼完整的 diff。想想看,要是作者没花这几分钟写这些说明,后来的人得浪费多少时间去猜。

大多数情况下,改动的实现细节可以不用写——代码本身说得很清楚(如果代码复杂到需要用文字解释,那是该在源码里写注释)。正文要回答的问题是:改动之前是怎么工作的(哪里不对)、改完之后怎么样、为什么选了这种方案。

将来的维护者会感谢你的。那个人可能就是你自己。


两个建议

拥抱命令行,离开 IDE

不管从哪个角度看,命令行都值得花时间学。Git 非常强大,IDE 也很强大,但两者强的方向不一样。我天天用 IDE(IntelliJ IDEA),也长期用过别的(Eclipse),但在 Git 这件事上,我没见过哪个 IDE 集成能和命令行比——前提是你得会。

IDE 也不是没用。删文件时自动调 git rm、重命名时帮你做正确的 git 操作,这些是挺好的。问题出在你试图在 IDE 里做提交、合并、变基、或者啃复杂的历史分析的时候。

想发挥 Git 的全部实力,命令行就是唯一的答案。

不管你用的是 Bash、Zsh 还是 PowerShell,都有自动补全脚本,记不住子命令和参数也没关系。

读读 Pro Git

《Pro Git》 线上免费,写得非常好,别浪费。


题图:xkcd