一次优秀的代码提交应该包含什么?
英文原文: What's in a Good Commit? 译者: @neevek
首先我们来听一个令人恶心的例子。
你看到 问题 F00-123
被解决了。这是关于一个你自己很熟悉的子系统的 Bug,所以直觉告诉你造成这个 Bug 最可能的原因。为了证实你的怀疑,你决定看看这个 bug 是怎么被解决的。你花了很长时间搜索了整个版本历史,直到把这个 bugfix 的范围缩小到了 4 个连续的提交,它们分别的提交信息是: dao 小调整(dao tweaks)
、 moar
、 Fixes
, 还有 删除一些调试信息(remove debug stuff)
。每个提交的修改集看起来都很大,多达十几个文件的几百处修改。“我艹尼@#$%%^&”,你准备骂娘但还是停住了,没有骂出你脑中那句最难听的粗口。“这个 bugfix 不应该超过三行代码!”。
上面的例子听起来是不是很熟悉咧?很多开发者仅仅只把版本控制系统当成是一堆文件的备份。这些历史备份除了可以让你取回某个时间点上的文件内容之外,没有任何其他用处。下面这些建议可以帮你把你的版本控制系统从一个备份系统,变成用于沟通和文档编写的一个宝贵的工具。
1. 每次提交只做一个修改
如果你在一次提交中修复了 F00-123
和 F00-233
、重构了一个类、界面上添加了一两个按钮、还有把整个项目文件中的 tab 改成空格,那么基本上没有人能 review F00-123
的 bugfix,只有你自己才知道哪些修改属于那个 bugfix 的一部分,但是一周以后很可能你自己都忘了。
要是一周以后你发现原来的那个 bugfix 导致了另外一个更严重的问题那你就完蛋了,你不能使用 hg backout
或者 git revert
来撤销你的修改,因为那样会把除了那个 bugfix 之外的所有修改都撤销掉,也意味着你一周的努力白费了。
上面这种问题的解决办法就是“每次提交只做一个修改”,对于怎么样才算是“一个修改”没有任何硬性或者既定的规则,但如果你能够用一个句子并且不用用到“和”这个字来描述你所做的所有事情,那么你算是做到了。
分布式版本控制系统其中一个很好的功能就是,如果你的工作目录中有很多相互之间没有关联的修改,你可以用它来帮你 收拾好你的烂摊子(clean up the mess you’ve made),但是最好别在一开始就搞出一个烂摊子。在你开始修改代码之前,先确定你想干什么和你想怎么做,然后再认真把关注点放在你想修改的那个点上。
在没有想出怎么改进某块代码之前,貌似不太可能去修改那块代码。你发现了 bug、看到了一些糟糕的代码、还有一些你很好奇想进一步了解的东西。无论你多么想去做,千万别被转移了注意力。这些发现非常有价值,用一个笔记本或者一个 TODO 文件把他们 写下来(jot them down),在完成当前任务之前不要想去解决那些问题。
这个不仅仅是关于更好地提交。当你沉浸在解决某个编程问题里面的时候,你的头脑充满着关于这个问题的一堆细节,如果这个时候你跳去想一些其他问题,那么你会忘了原来那个问题的细节,再回到原来那个问题需要花一些时间。你需要 减少任务切换(minimize task switches)来提高你的工作效率。
当然有些时候你会发现在没有解决某些其他问题之前,你很难完成当前的任务。这个时候最简单的方式就是使用 hg shelve
或者 git stash
把你当前还没有完成的修改暂存起来,把两个问题的修改分开,提交那个被依赖的修改,再回到你原来的任务上面去。
2. 每次提交要包含完整修改
如果一个修改分布到了几个提交里面,那么这个修改也是很难 review 的。通常这是因为在同一时间解决太多问题导致的,如果你吃不了那么多,却啃了那么多,那么在你想保存其中的某些修改的时候,你会发现大部分的修改都是未完成的。同一时间关注太多问题会导致最后需要很长时间才能够提交完整的修改。(原文是:Focusing on one task at a time takes you a long way towards committing complete changes. 但我觉得作者的原意应该是:Focusing on too many tasks at a time takes you a long way towards committing complete changes.)
有些修改需要花很长时间,所以如果中间在某个点上你搞错了,重头再来你肯定折腾不起,所以你需要在过程中保存一些中间版本。幸好分布式版本控制系统都允许你保存很多中间版本但是最后只提交一次修改到中心版本库(central repository),你可以提交任意多次的中间版本,在最后使用 hg histedit
或者 git rebase
把多次的中间版本合并成一个修改集。
另外一种,也是我个人比较喜欢的方法,因为它把中间版本和永久性的修改版本分开了,这种方法使用 Git 的 index,或者 Mercurial Queues 中的一个 patch 来保存最近一次正确的(没有 bug 的)中间版本,每次你做了新的修改你都更新这个 index/patch,如果你犯了个错误,你就可以使用他们恢复你的工作目录了。I like to think of it as a one-slot quicksave for version control.(译注:这句只可意会,我不知道怎么翻。)
3. 写好注释,说明你修改了什么
像“修复(Fixes)”、“提交(Commit)”这样的提交信息没包含任何有用的信息。如果别人想看看版本历史,像这样的提交信息只会逼他们去看完所有的代码修改,看代码是很费时费力的。写这样一个简短但表达不清晰的提交信息,可能是省了你一分钟时间,但是却浪费了其他人几个小时。
一个好的提交信息可以让看的人清楚代码的哪一部分被修改了,是怎么被修改的,他们也不需要去看你的代码:
SomeClass: use bleh instead of xyzzy in someMethod (fixes FOO-123)
4. 注释说明为什么做这个修改
假设每次修改代码都有一个很好的理由/原因,但如果这个理由/原因被没有记录下来,那么整个代码库(codebase)将面临以下风险:
- 其他开发者不明白为什么原代码是那样写的。当他们修改代码的时候,他们可能会引入一些原作者已经发现或者避免的问题。
- 其他开发者认为原代码那样写肯定是有(好的)原因的,所以最好别动它。他们把这些代码看成是一个黑盒,然后加各种复杂的 workaround,避免修改原代码。最后导致这个代码库变得臃肿,代码变得难以看懂。
如果你有足够的理由有必要破坏一个项目的规范或者约定,那一定要把这个理由作为注释写在你的代码里面:
- xyzzy (bars); + // Our bars are already sorted, so bleh is much faster than xyzzy + bleh (bars);
如果你的代码遵守了规范,并且你的代码没有什么微妙(subtleties)的点需要注意,那就没有必要把你的文档注释写在代码里面。但仍然有必要让人知道为什么新代码优于旧代码(尤其是当新代码引入了一个新问题),所以还是要把原因写在提交信息里面的:
SomeClass: Don't flush caches in someMethod The caches are flushed automatically at the end of each request.
如果一个修改解决了一个已知问题,确保在提交信息里面带上 ticket 号(bug 追踪系统中的 ticket),以便其他开发者在看版本历史的时候能够清楚地知道是在什么情况下做出的这个修改。
5. 不要提交被注释掉的代码
我没有办法理解提交被注释掉的代码背后的理论依据,我假设这是为了保存旧代码,以防新代码不能正常工作,但这种做法很莫名其妙,最开始我们使用版本控制系统不就是为了保存旧版本吗?!
为什么要注释掉这些代码?这些代码能运行吗?会正常运行吗?曾正常运行过吗?注释代码是我们应该支持还是摒弃的呢?被注释掉的代码毫无用处,因为每当开发者读到这些被注释的代码,总会冒出一些没有答案的问题,它只会混淆开发者视听,让开发者分心而无法更好专注于有用的代码。
对于提交被注释掉的代码这件事情,只有一个原则,那就是: