我不再最喜欢的 Git 提交
六年前,David Thompson(大卫·汤普森)写了一篇广为流传的博客文章《My favourite Git commit》,称赞他的同事写了一条妙趣横生、细节详尽的提交信息。我当时很喜欢那篇文章,还把它发给几位队友,作为优秀提交信息的范例。
最近,我在撰写自己的实用提交信息写作指南时重新读了汤普森的文章。当被要求解释汤普森的文章为何是一个如此出色的范例时,我惊讶地发现自己竟说不出来。作为一个旁观者,读起来确实很有趣,但我无法把它论证为优秀软件工程的典范。
汤普森最喜欢的提交
下面是当时令汤普森以及包括我在内的其他人如此着迷的提交信息:
Convert template to US-ASCII to fix error
I introduced some tests in a feature branch to match the contents of
/etc/nginx/router_routes.conf. They worked fine when run withbundle exec rake specorbundle exec rspec modules/router/spec. But when run asbundle exec rakeeach should block failed with:ArgumentError: invalid byte sequence in US-ASCIII eventually found that removing the
.with_content(//)matchers made the errors go away. That there weren’t any weird characters in the spec file. And that it could be reproduced by requiring Puppet in the same interpreter with:rake -E 'require "puppet"' specThat particular template appears to be the only file in our codebase with an identified encoding of
utf-8. All others areus-ascii:dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8Attempting to convert that file back to US-ASCII identified the offending character as something that looked like a whitespace:
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 convertAfter replacing it (by hand) the file identifies as
us-asciiagain:dcarley-MBA:puppet dcarley$ file --mime modules/router/templates/routes.conf.erb modules/router/templates/routes.conf.erb: text/plain; charset=us-asciiNow the tests work! One hour of my life I won’t get back..
“包袱”在于,经过这段冗长的铺垫之后,汤普森才展示实际的 diff:
是的,这条提交信息包含六个段落和五个代码片段,全都是为了描述一个字符的空白符改动。
喜欢不等于最佳
这条提交的魅力所在不难理解。
大多数开发者只会把这种改动记录为简单的“Fix whitespace character”(修复空白字符),因此有人愿意花这么多笔墨来解释自己排查和修复 bug 的过程,着实令人欣喜。
正如汤普森所说的那样,它确实是一条好的提交信息:它创造了一个可检索的记录,并分享了关于开发者工具和工作流程的有益见解。
这并不是在攻击汤普森,甚至也不是在攻击该提交的原作者。汤普森从未声称它是“最佳”提交信息,只说它是他最喜欢的。
话虽如此,我现在看到了一些缺陷,使我不愿再把它当作模范提交信息。
它把最重要的信息埋在了最后
汤普森最初发表这篇博文时,最常见的批评之一是这条提交信息过于冗长。我认为这种批评有失偏颇。
提交信息中的详尽细节只要切题就有用,而汤普森的细节是切题的。它们能帮助经验较少的队友学习作者的调试过程和工具集,也能让经验更丰富的队友有机会检查开发者是否遗漏了什么,或者不了解某个相关工具。
人们之所以觉得汤普森的范例过于冗长,是因为它把最重要的信息深深埋在了提交信息之中。
重读第一段:
I introduced some tests in a feature branch to match the contents of
/etc/nginx/router_routes.conf. They worked fine when run withbundle exec rake specorbundle exec rspec modules/router/spec. But when run asbundle exec rakeeach should block failed with:ArgumentError: invalid byte sequence in US-ASCII
读完了三句话和一个代码片段,读者仍然不知道这个改动究竟做了什么。
提交信息应该把最重要的信息放在最前面,然后逐步过渡到更细的细节。新闻工作者称之为倒金字塔写作法。
新闻工作者以倒金字塔结构组织新闻报道,与最多人相关的信息放在最顶端。
如果我正在浏览提交历史,我想快速判断每个提交是否与我相关。提交信息应该从一开始就提供改动的高层概述。
它始终没有把问题讲清楚
读到最后,你真的理解这个改动了吗?
下面是它最接近解释问题的地方:
That particular template appears to be the only file in our codebase with an identified encoding of utf-8. All others are 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 编码,但从未解释原因。幸运的是,该项目是开源的,我可以自己去调查。
问题出在 routes.conf.erb 的第 463 行:
$ 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 文件,因为 0xC2 和 0xA0 都落在US-ASCII 字节范围之外。
0xC2 0xA0 序列意味着任何读取 routes.conf.erb 的应用程序都必须以 UTF-8 编码来解释它——这是一种更新、对国际化更友好的文本编码方案。
汤普森的代码库使用的是 Ruby 1.9.3,它支持 UTF-8 编码,但如果文件没有显式声明,则默认使用 US-ASCII。
翻查源码历史后,我发现 commit 5a8607 最早引入了这个 UTF-8 字符。那条提交信息没有提到引入该字符的任何原因,因此它很可能是意外引入的。
一位 Hacker News 评论者对该杂散 UTF-8 字符为何出现在 routes.conf.erb 中提出了一个看似合理的推测:
这个无效字符的来源很可能是有人使用 Apple 爱尔兰/英国键盘布局,其中 # 是 Option-3(AltGr-3),不换行空格是 Option-Space(AltGr-Space)。
-Hacker News 上的messe
它引用了代码却没有链接
汤普森的范例提交开头引用了一些外部代码:
I introduced some tests in a feature branch to match the contents of
/etc/nginx/router_routes.conf. They worked fine when run withbundle exec rake specorbundle exec rspec modules/router/spec.
但提交信息从未指明分支名称,也没有给出 commit hash,读者无法复现开发者的发现。
后来,提交信息又说:
I eventually found that removing the
.with_content(//)matchers made the errors go away. I didn’t see any weird characters in the spec file.
没有 commit hash 或链接,读者不知道开发者指的是哪些 matcher、哪个 spec 文件。
如果提交信息引用了外部代码,就应该明确地链接过去,这样代码审查者和未来的维护者才能看到改动的确切上下文。
我的改写
下面是我对汤普森最喜欢的 Git 提交提出的修订版本:
Convert routes.conf.erb template to US-ASCII
routes.conf.erb含有一个杂散的 UTF-8 字符,它似乎是在 5a8607 中被意外引入的。
rake要求 US-ASCII 格式,因此routes.conf.erb中的这一个 UTF-8 字符会导致rake中的测试失败。本次改动将该 UTF-8 字符替换为等价的 US-ASCII 字符,以防止
rake中的测试失败。The stray UTF-8 character
The issue is on line 463 of
modules/router/templates/routes.conf.erb:$ 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 编码读取该文件的工具都会失败。How I discovered this
I introduced some tests in a feature branch to match the contents of
/etc/nginx/router_routes.conf(see abcd123). They worked fine when I ran them withbundle exec rake specorbundle exec rspec modules/router/spec, but when I ran the tests asbundle exec rake, eachshouldblock failed with:ArgumentError: invalid byte sequence in US-ASCIII eventually found that removing the
.with_content(//)matchers made the errors go away. I didn’t see any weird characters in the spec file. I could reproduce the error by requiring Puppet in the same interpreter with:rake -E 'require "puppet"' specThat particular template appears to be the only file in our codebase that
fileidentifies asutf-8. All others areus-ascii:$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8Attempting to convert that file back to US-ASCII identified the offending character as something that looked like a whitespace:
$ 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 convertAfter I replaced the UTF-8 character (by hand),
fileidentifiesroutes.conf.erbasus-asciiagain:$ 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 $简化为$,因为前者大多是噪音。
值得注意的是,我没有删除细节,因为问题不在于冗长,而在于开发者组织和呈现信息的方式。
定义自己原则的价值
重读汤普森的文章让我想起,为自己定义软件工程原则是多么有价值。
我之所以接受那条提交作为好范例,是因为我在它的优点上与汤普森意见一致。直到我坐下来明确了我认为提交信息最重要的特质之后,我才看到汤普森范例的不足之处。
这些年来,我已经阐述过自己对若干软件工程实践的看法,而每一次这样做都让我成为更好的开发者。它迫使我对那些习以为常的观念进行批判性思考,也帮助我记住自己的理想状态是什么样子,即使我并不总能达到。
延伸阅读
- “How to Write Useful Commit Messages”——我对什么才算好的提交信息的更详细阐述。
govuk-puppet 项目的摘录版权归 Crown Government Digital Service 所有,依 MIT License 使用。
随机一篇博客
