No Longer My Favorite Git Commit

Michael Lynch

不再是我最喜爱的 Git 提交

原文由 Michael Lynch 发布,订阅该博客

六年前,David Thompson 写了一篇颇受欢迎的博客文章“My favourite Git commit”,盛赞同事写的一条既详尽又妙趣横生的提交信息。我当时很喜欢这篇文章,还把它发给了好几位同事,作为优秀提交信息的范例。

最近,在编写自己的如何写出有用的提交信息指南时,我重读了 Thompson 的文章。当被问及究竟是什么让 Thompson 的例子如此有效时,我惊讶地发现自己竟无法回答。作为旁观者,这篇文章读起来很有趣,但我无法再把它当作良好软件工程实践的典范来为其辩护。

Thompson 最喜爱的提交

就是这条当时让 Thompson 和包括我在内的许多人着迷的提交信息

将模板转换为 US-ASCII 以修复错误

我在功能分支上添加了一些测试,用于匹配 /etc/nginx/router_routes.conf 的内容。当使用 bundle exec rake specbundle exec rspec modules/router/spec 运行时,它们都能正常通过。但当以 bundle exec rake 运行时,每个 should 块都会报错:

ArgumentError:
 invalid byte sequence in US-ASCII

我最终发现,去掉 .with_content(//) 匹配器后错误就消失了。spec 文件里并没有什么奇怪的字符。而且,只要在同一个解释器中引入 Puppet 就能复现这个问题:

rake -E 'require "puppet"' spec

那个模板似乎是我们代码库中唯一被识别为 utf-8 编码的文件,其他文件都是 us-ascii

dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf
modules/router/templates/routes.conf.erb:  text/plain; charset=utf-8

尝试将该文件转回 US-ASCII 后,发现罪魁祸首是一个看起来像空白字符的字符:

dcarley-MBA:puppet dcarley$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 | tail -n5
  proxy_intercept_errors off;

  # Set proxy timeout to 50 seconds as a quick fix for problems
  #
iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert

(手动)替换掉它之后,该文件又被识别为 us-ascii 了:

dcarley-MBA:puppet dcarley$ file --mime modules/router/templates/routes.conf.erb
modules/router/templates/routes.conf.erb: text/plain; charset=us-ascii

现在测试终于通过了!白白浪费了我一小时的生命……

这段长长铺垫之后的“笑点”在于,Thompson 展示了实际的 diff:

没错,这条提交信息用了整整六段文字和五个代码片段,仅仅为了解释一个空白字符的改动。

喜爱的不等于最好

很容易看出这条提交为何讨人喜欢。

大多数开发者只会把这次改动简单记为“修复空白字符”,因此有人如此详尽地解释自己排查和修复 bug 的过程,着实让人眼前一亮。

就 Thompson 列举的那些理由而言,这确实是一条不错的提交信息:它留下了一份可供搜索的记录,也分享了关于开发者工具和流程的有用见解。

这并不是对 Thompson、甚至对原提交作者的攻击。Thompson 从未声称这是“最好”的提交信息,只是说这是他最喜欢的。

不过,如今我看到了其中的一些缺陷,这让我无法再把它当作提交信息的范例来推荐。

它把最重要的信息埋在了最后

Thompson 最初发表这篇博客时,最常见的批评是说这条提交信息过于冗长。我当时觉得这种批评有些偏颇。

提交信息中的详尽细节只要与主题相关,就是有价值的,而 Thompson 例子中的细节正是如此。它们能帮助经验较少的同事学习作者的调试思路和工具使用,也能让经验更丰富的同事有机会判断开发者是否有所疏漏,或是否不知道某个相关工具。

人们之所以觉得 Thompson 的例子过于冗长,原因在于它把最重要的信息深深埋在了提交信息的末尾。

再读一遍第一段:

我在功能分支上添加了一些测试,用于匹配 /etc/nginx/router_routes.conf 的内容。当使用 bundle exec rake specbundle exec rspec modules/router/spec 运行时,它们都能正常通过。但当以 bundle exec rake 运行时,每个 should 块都会报错:

ArgumentError:
 invalid byte sequence in US-ASCII

整整三句话加一个代码片段之后,读者依然对这次改动究竟做了什么一无所知。

提交信息应该把最重要的信息放在最前面,再逐步展开到更细节的内容。记者们称这种写法为倒金字塔结构。

倒金字塔

记者以倒金字塔结构来组织新闻报道,将与最多读者相关的信息放在最顶部。

当我浏览提交历史时,我希望能快速判断每条提交是否与自己相关。提交信息理应在开头就提供对改动的高度概括。

它始终没有真正讲清楚问题所在

读完 Thompson 示例中的那条提交信息,你真的明白这次改动是怎么回事吗?

这是其中最接近问题解释的一段:

那个模板似乎是我们代码库中唯一被识别为 utf-8 编码的文件,其他文件都是 us-ascii:

dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf
modules/router/templates/routes.conf.erb:  text/plain; charset=utf-8

信息中说 routes.conf.erb 是 UTF-8 编码,却始终没有解释原因。好在该项目是开源的,我可以自己一探究竟。

问题出在 第 463 行routes.conf.erb

$ cat modules/router/templates/routes.conf.erb | head -n 463 | tail -n 1
  # where civica QueryPayments calls are taking too long.

用普通文本编辑器或浏览器是看不出问题的,但如果用 xxd 这样的工具导出文件的原始字节,就能发现端倪:

$ cat modules/router/templates/routes.conf.erb \
  | head -n 463 | tail -n 1 \
  | xxd | head -n 1
00000000: 2020 23c2 a077 6865 7265 2063 6976 6963    #..where civic
                 ^^ ^^

如果你没有背下 US-ASCII 和 UTF-8 编码表,这里是该行开头几个字符的对照:

字节表示文本表示
0x20' '(空格)
0x20' '(空格)
0x23'#'
0xC2 0xA0' 'UTF-8 不换行空格

因此,该文件包含了字节序列 0xC2 0xA0,这意味着它不可能是 US-ASCII 文件,因为 0xC20xA0 都超出了 US-ASCII 的字节范围

字节序列 0xC2 0xA0 意味着任何读取 routes.conf.erb 的应用都必须以 UTF-8 编码来解析它——UTF-8 是一种更新、更具国际化友好性的文本编码方案。

Thompson 的代码库使用的是 Ruby 1.9.3,它虽然支持 UTF-8 编码,但如果文件未显式声明,则默认会按 US-ASCII 处理。

翻阅源码历史后,我发现是提交 5a8607最早引入了这个 UTF-8 字符。该提交信息并未提及引入该字符的任何理由,因此很可能只是个意外。

一位 Hacker News 评论者提出了一个颇为可信的推测,解释了 routes.conf.erb 中这个多余的 UTF-8 字符为何会出现:

这个无效字符很可能的来源是,有人使用了爱尔兰/英国的苹果键盘布局,其中 # 是通过 Option-3(AltGr-3)输入的,而不换行空格则是通过 Option-Space(AltGr-Space)输入的。

-messe 在 Hacker News 上

它引用了代码却没有给出链接

Thompson 的示例提交以一段对外部代码的引用开头:

我在功能分支上添加了一些测试,用于匹配 /etc/nginx/router_routes.conf 的内容。当使用 bundle exec rake specbundle exec rspec modules/router/spec 运行时,它们都能正常通过。

但提交信息既没有给出分支名,也没有提供提交哈希,读者根本无法复现开发者的发现。

后文提交信息又写道:

我最终发现,去掉 .with_content(//) 匹配器后错误就消失了。我在 spec 文件中并未看到任何奇怪的字符。

没有提交哈希或链接,读者无从得知开发者所指的究竟是哪些匹配器、哪个 spec 文件。

如果提交信息中引用了外部代码,就应该明确给出链接,这样代码审查者和未来的维护者才能看到改动的确切上下文。

我的改写版本

以下是我对 Thompson 最喜爱的这条 Git 提交的改写建议:

将 routes.conf.erb 模板转换为 US-ASCII

routes.conf.erb 中有一个多余的 UTF-8 字符,似乎是在 5a8607 中意外引入的。

rake 要求文件为 US-ASCII 格式,因此 routes.conf.erb 中这单个 UTF-8 字符会导致 rake 中的测试失败。

本次改动将该 UTF-8 字符替换为等效的 US-ASCII 字符,以避免 rake 中的测试失败。

多余的 UTF-8 字符

问题位于 modules/router/templates/routes.conf.erb 的第 463 行:

$ cat modules/router/templates/routes.conf.erb \
  | head -n 463 | tail -n 1 \
  | xxd | head -n 1
00000000: 2020 23c2 a077 6865 7265 2063 6976 6963    #..where civic
                 ^^ ^^

0xC2 0xA0 不是合法的 US-ASCII 字节序列,而是 UTF-8 不换行空格字符。任何以 US-ASCII 编码读取该文件的工具都会失败。

我是如何发现这个问题的

我在功能分支上添加了一些测试,用于匹配 /etc/nginx/router_routes.conf 的内容(见 abcd123)。当使用 bundle exec rake specbundle exec rspec modules/router/spec 运行时,它们都能正常通过,但当以 bundle exec rake 运行时,每个 should 块都报错:

ArgumentError:
 invalid byte sequence in US-ASCII

我最终发现,去掉 .with_content(//) 匹配器后错误就消失了。我在 spec 文件中并未看到任何奇怪的字符。只要在同一解释器中引入 Puppet,就能复现该错误:

rake -E 'require "puppet"' spec

那个模板似乎是我们代码库中唯一被 file 识别为 utf-8 的文件,其他文件都是 us-ascii

$ find modules -type f -exec file --mime {} \+ | grep utf
modules/router/templates/routes.conf.erb:  text/plain; charset=utf-8

尝试将该文件转回 US-ASCII 后,发现罪魁祸首是一个看起来像空白字符的字符:

$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 \
   | tail -n5

  proxy_intercept_errors off;

  # Set proxy timeout to 50 seconds as a quick fix for problems
  #
iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert

(手动)替换掉该 UTF-8 字符后,file 又将 routes.conf.erb 识别为 us-ascii

$ file --mime modules/router/templates/routes.conf.erb
modules/router/templates/routes.conf.erb: text/plain; charset=us-ascii

现在测试终于通过了!白白浪费了我一小时的生命……

我的改动如下:

  • 我在信息开头添加了高度概括的总结。
  • 我对 UTF-8 字符及其来源给出了更明确的解释。
  • 我将作者原文中的大部分内容移到了“我是如何发现这个问题的”一节,以表明这部分属于额外阅读内容。
  • 我做了一些轻微的语法修正。
  • 我去掉了被动语态,以减少歧义。
  • 我将终端提示符从 dcarley-MBA:puppet dcarley $ 简化为 $,因为前者大多是冗余信息。

值得注意的是,我并没有删减细节,因为问题不在于冗长,而在于开发者组织和呈现信息的方式。

明确个人原则的价值

重读 Thompson 的文章让我意识到,为自己明确软件工程原则是多么有价值。

我曾因为认同 Thompson 所说的优点而将其视为优秀范例。直到我坐下来,明确自己认为提交信息最重要的特质时,才看出了 Thompson 例子中的不足。

这些年来,我曾就多种软件工程实践阐述过自己的观点,而每一次这样做都让我成为更好的开发者。它迫使我批判性地审视那些被视为理所当然的观念,并帮助我记住自己心中的理想形态,即使我未必总能达到它。

延伸阅读


摘自 govuk-puppet 项目的内容版权归 Crown Government Digital Service 所有,依据 MIT 许可证使用。

本文章由 muse-spark-1.2-contributor 进行翻译

评论