Tech, Bio & Photography

【翻译】如何编写 Git 提交消息

渣翻了三天,累死了(哭)。

这篇不知道应该放在什么子分类合适,姑且先放在“编程”那一栏。

本文用了一些 Hexo 的特性(比如说脚注),如果不能完全显示可以移步至:https://littleye233.github.io/2021/08/20/techdev/how-to-write-a-git-commit-message-chs/暂时不可用,稍后会已经部署到 GitHub 上)。


《【翻译】如何编写 Git 提交消息》[^1]的简体中文翻译版本对应原文为 How to Write a Git Commit Message ,原作者为 Chris Beams 。请注意:

  • 正文格式尽可能与原网页保持一致;
  • 译者注将以脚注 (footnote) 的形式呈现,且其内容应以”译者注:“起始;
  • 原文中若有文内跳转超链接,如无必要,将直接去除,且不在译文中作进一步说明;
  • 原文中若有翻译后难以传达作者写作意图的词句 (通常是命令,或由源语言的特殊性质等原因导致) ,此时将不再翻译,且不在译文中作进一步说明。

翻译文本将从随后的分隔线开始展示。


如何编写 Git 提交信息

原作者: Chris Beams
原文创作日期: 2014 年 8 月 31 日

提交消息很重要。这里将展示如何写好它们。

图1 信息量递减的提交消息

引子:为什么好的提交消息很重要

如果你曾随意地浏览过一些 Git 仓库,你很可能会发现它们的提交消息或多或少有些杂乱。例如,你可以看看我早期提交给 Spring 的一些 Gem[^2] 的日志:

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

e5f4b49 Re-adding ConfigurationPostProcessorTests after its brief removal in r814. @Ignore-ing the testCglibClassesAreLoadedJustInTimeForEnhancement() method as it turns out this was one of the culprits in the recent build breakage. The classloader hacking causes subtle downstream effects, breaking unrelated tests. The test method is still useful, but should only be run on a manual basis to ensure CGLIB is not prematurely classloaded, and should not be run as part of the automated build.
2db0f12 fixed two build-breaking issues: + reverted ClassMetadataReadingVisitor to revision 794 + eliminated ConfigurationPostProcessorTests until further investigation determines why it causes downstream tests to fail (such as the seemingly unrelated ClassPathXmlApplicationContextTests)
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 自身的源码正是良好的范例。或是看看 Spring Boot 或由 Tim Pope 管理的仓库。

这些仓库的贡献者们知道,一个经过精心打磨的 Git 提交消息是将一个更改与其他开发者 (以及未来的自己) 交流其来龙去脉的最好方式[^3]。一个 diff 的输出结果将告诉你什么改变了,而只有提交消息能恰当地告诉你为什么改变了。 Peter Hutterer 将这个观点表达得很好:

重新确定一段代码的上下文是浪费的。我们不能完全避免它,因此我们应当努力去[竭尽]所能减少这种情况的发生。提交消息能准确地做到这一点,所以一条提交消息能够展示出一位开发者是不是好的协作者。[^4]

如果你还没有很多思路来编写良好的 Git 提交消息,这或许说明你没有花费很多时间使用 git log 命令和相关的工具。这里有一个残酷的循环:因为提交历史是缺少结构和一致性的,某个人不会花费很多时间使用或关心它。并且因为它不被使用或关心,它将始终缺少结构和一致性。

然而,被维护得很好的日志是一种优美和实用的东西,这会让 git blamerevertrebaselogshortlog 和其他子命令充满活力,会给回顾其他人的提交和 pull requests 带来一些价值——并且它们突然能被独立地完成。理解几个月或几年前一些事情为什么发生将不但变得可能,还将变得高效。

一个项目能否取得长远的成功 (相较于其他因素) 不但取决于其可维护性如何,还在于一个维护者是否没有多少比日志更加有力的工具了。我们值得花费一些时间来学习如何适切地维护项目的日志。起初维护日志时可能的困境不久就会转变为习惯,并最终成为所有参与者自豪感和生产力的源泉。

在这篇文章中,我将会告诉你保持一份健康的提交历史的最基础的要素:如何编写一条独立的提交消息。还有其他我在这里不会提及的重要习惯,比如说统整提交[^5],或许我会在后续的投稿中谈到它们。

大多数编程语言都有着已经成形的惯例,它们构建了符合语言习惯的风格,就像命名、格式等等[^6]。当然,这些惯例有诸多变种,但大部分开发者都认同专注于其中一种的情形远优于每个人各自选用一种造成的混乱局面。

一个团队对待提交日志的方式应当没有丝毫不同。为了创建一份实用的修订历史,团队应该首先在至少符合以下三点的提交消息惯例上达成共识:

风格。标记语言的句法[^7],折行的间隔,语法[^8],大小写,标点符号。将这些都明确给出,去除猜测,并且让一切都尽可能简单。最终的结果将会是一份格外一致的日志——不仅阅读起来很愉悦,而且实际上的确能被定期阅读[^9]。

内容。提交消息的主体 (如果有的话) 应该包含什么信息?什么是不能包含的?

元数据。诸如 issue 的追踪编号[^10]、 pull request 的编号应当如何被提及?

万幸的是,已经有一些成形的惯例来创建一条符合语言习惯的 Git 提交消息。的确,这些都假定在特定的 Git 命令运作的方式下。这里没有你需要重新发明的地方,只要遵循下方的七条规则,你就推开了像专家一样提交的大门。

编写好的 Git 提交消息的七条规则

请谨记:这些已被提出

  1. 用一个空行分隔标题和主体;
  2. 标题控制在 50 个字符以内;
  3. 标题的首字母大写;
  4. 标题的末尾不要写句号;
  5. 标题使用祈使语气;
  6. 主体每 72 个字符折行;
  7. 用主体解释做了什么为什么,而不是如何做到

例如:

Summarize changes in around 50 characters or less

More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of the commit and the rest of the text as the body. The
blank line separating the summary from the body is critical (unless
you omit the body entirely); various tools like `log`, `shortlog`
and `rebase` can get confused if you run the two together.

Explain the problem that this commit is solving. Focus on why you
are making this change as opposed to how (the code explains that).
Are there side effects or other unintuitive consequences of this
change? Here's the place to explain them.

Further paragraphs come after blank lines.

 - Bullet points are okay, too

 - Typically a hyphen or asterisk is used for the bullet, preceded
   by a single space, with blank lines in between, but conventions
   vary here

If you use an issue tracker, put references to them at the bottom,
like this:

Resolves: #123
See also: #456, #789

1. 用一个空行分隔标题和主体

根据 git commit 命令的帮助页面

尽管不是必须的,一个好主意是,提交消息以一行简短的 (少于 50 个字符) 概括这个更改的文字为开始,紧接着是一个空行,随后是一段更详细的描述。整段文本的首行将被认作为提交的标题,那个标题将贯穿整个 Git[^11] 。例如, Git-format-patch(1) 将提交转变为邮件,它将提交的标题作为邮件的主题,将剩余的提交内容作为邮件的主体。

首先,不是每一次提交都需要标题和主体。有时一行也很好,尤其是当更改很简单以至于进一步的阐释都不必要的时候。例如:

Fix typo in introduction to user guide

没有什么是需要解释的了。如果读者想知道这个拼写错误是什么,直接看这个更改本身即可,换句话说使用 git showgit diffgit log -p

如果你想用命令提交,给 git commit 命令添加 -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 的这一节

在任何情形下,标题和主体之间的间隔都能在浏览日志时得到回报。这里是完整的日志:

$ git log
commit 42e769bdf4894310333942ffc5a15151222a87be
Author: Kevin Flynn <[email protected]>
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 中的其他上下文,它们的标题行和主体的区别被打破了——但它们都无法在二者之间不空行时做得合适[^12]。

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

50 个字符不是硬性限制,只是一个经验法则。将标题行控制在这个长度能保证它们是可读的,并且能强迫作者花一些时间思考如何最简洁地解释将要发生什么。

提示:如果你难以概括你提交的内容,或许是因为你一次提交了太多的更改。努力做到原子级提交[^13] (另一篇投稿的话题) 吧。

GitHub 的用户界面全面地意识到了这些惯例。例如,它会在你的标题行超过 50 个字符时警告你:

图2 GitHub 的字数警告

并且会截断超过 72 个字符的标题行,后续用省略号代替:

图3 GitHub 的字数超限截断

因此,争取限制在 50 个字符以内,并把 72 个字符当作是硬性限制。

3. 标题的首字母大写

这就跟听起来一样简单。所有的标题行的首字母都需要大写。

例如,用:

  • Accelerate to 88 miles per hour

来替代:

  • accelerate to 88 miles per hour

4. 标题的末尾不要写句号

句末标点在标题中时无关紧要的。另外,当你想要控制 50 个字符时,空格是很珍贵的。

例如,用:

  • 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 的Pull Request 界面点击 “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 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 release version 1.0.0
  • If applied, this commit will merge pull request #123 from user/branch

注意到对于非祈使语气,这是不能成功的:

  • If applied, this commit will fixed bug with Y
  • If applied, this commit will changing behavior of X
  • If applied, this commit will more fixes for broken stuff
  • If applied, this commit will sweet new API methods

记住:祈使语气的使用仅对标题很重要。在写主体时,你可以放松这条规定。

6. 主体每 72 个字符折行

Git 从来不会自动折行。当你编写一个提交的主体时,你需要注意到它的右边界,并手动折行。

折行的推荐值是 72 个字符,因此 Git 在大体上将对 80 个字符以内的内容保持原样,有足够的空间用于缩进排版。

一个好的文本编辑器能帮到你。 Vim 的配置很容易,例如在编写 Git 提交消息时在 72 个字符处折行。然而,在传统意义上, IDE 对于智能地支持提交消息的折行很糟糕 (尽管 IntelliJ IDEA 在最近的版本中终于做得比以前好了) 。

7. 用主体解释做了什么和为什么,而不是如何做到

这里的一条来自 Bitcoin Core 的提交 是解释更改了什么和为什么的最佳范例:

commit eb0b56b19017ab5c16c745e6da39c53126924ed6
Author: Pieter Wuille <[email protected]>
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.

看一看它的完整的 diff 信息,思考这位作者在此时此地花费时间提供代码的上下文节约了其他和未来的提交者多少时间。如果他没有做到,这次提交将很可能会永远被遗忘。

在大多数情况下,你可以省去这个更改如何被做出的细节。在这点上,代码通常能自我解释 (如果这段代码如此复杂以至于它需要用枯燥的语言来解释,那就是源代码注释应该做的事情了) 。只是专注于首先明确更改的原因——你在更改之前所做的事 (以及出现了什么错误) ,它们现在如何工作的,以及为什么你决定用你的方式解决这个问题[^14]。

未来感谢你的维护者可能就是你自己!

提示

学着热爱命令行。把 IDE 抛在脑后。

处于诸多原因,比如说 Git 子命令的存在,拥抱命令行是一种明智之举。 Git 有着令人疯狂的力量, IDE 亦然,但这两者体现在不同的方面上。我每天都使用一款 IDE (IntelliJ IDEA) ,也广泛地使用其他的 (Eclipse) ,但我还从没有见过哪款 IDE 的 Git 集成能比得上简单有力的命令行 (一旦你意识到了这点) 。

某些 Git 相关的 IDE 功能是无价的[^15],就像当你删除文件时执行 git rm ,在你重命名文件的时候用 git 执行正确的命令。而当你开始试着用 IDE 提交、合并、 rebase 或分析高深莫测的历史记录时,一切都会崩溃。

当支配 Git 的全部法力之日到来之时,一切都只剩下命令行。

记住不论你是使用 Bash 、 zsh 还是 PowerShell ,都有 Tab 自动补全脚本来减轻不少记忆子命令和开关的痛苦。

阅读 Pro Git

Pro Git 在网上可以免费阅读,并且它很美妙。好好利用它吧!

题图提供者: xkcd


【完】

脚注

[^1]: 译者注:”提交消息“原文为 ”commit message“ ,下同。
[^2]: 译者注: Spring 库的插件名为 “Gem” 。
[^3]: 译者注:原文为 ”The contributors to these repositories know that a well-crafted Git commit message is the best way to communicate context about a change to fellow developers (and indeed to their future selves)“ 。
[^4]: 译者注:原文为 ”Re-establishing the context of a piece of code is wasteful. We can’t avoid it completely, so our efforts should go to reducing it [as much] as possible. Commit messages can do exactly that and as a result, a commit message shows whether a developer is a good collaborator“ 。
[^5]: 译者注:原文为 “commit squashing” 。
[^5]: 译者注:原文为 “Most programming languages have well-established conventions as to what constitutes idiomatic style, i.e. naming, formatting and so on” 。
[^6]: 译者注:原文为 “syntax” 。
[^7]: 译者注:原文为 “grammar” 。
[^8]: 译者注:原文为 “The end result will be a remarkably consistent log that’s not only a pleasure to read but that actually does get read on a regular basis” 。
[^9]: 译者注:原文为 “issue tracking IDs” 。
[^10]: 译者注:原文为 “ The text up to the first blank line in a commit message is treated as the commit title, and that title is used throughout Git” 。
[^11]: 译者注:原文为 “There are a number of other contexts in Git where the distinction between subject line and body kicks in—but none of them work properly without the blank line in between” 。
[^12]: 译者注:原文为 “atomic commits” 。
[^13]: 译者注:原文为 “Just focus on making clear the reasons why you made the change in the first place—the way things worked before the change (and what was wrong with that), the way they work now, and why you decided to solve it the way you did” 。
[^14]: 译者注:原文为 “Certain Git-related IDE functions are invaluable” 。

1 Like