翻译自Thoughts on improving OpenStack GIT commit practice/history
Git commit 中良好的习惯
本文基于使用git用于代码开发,bug fix及代码review的工程实践经验,比如libvirt、qemu、openstack nova。其他开源工程比如说linux内核、core utils,GNULIB等也遵循一个相对通用的习惯。本文主要用于促进openstack nova工程提升代码质量。虽然“质量”在计算领域是难以衡量的,对某人来说的“漂亮代码”对另一人来说可能意味着“邪恶hack”,但我们能提出一些通用性指导意见来指导我们在工程中发布commit。本文主要从两方面进行阐述:
本文首先强调将代码修改分割为一系列的单个提交,及其良好附加消息的重要性。若这一指导意见能够得到广泛应用,那么Openstack GIT历史质量将会有极大提升。不过这一效果达成既需要有“胡萝卜”也需要有“大棒”,本文主要是作为“胡萝卜”,而那些通过Gerrit进行代码review的人要成为坚守原则的“大棒”。
换句话说,当在Gerrit revie代码时,不仅仅是简单地分析代码正确性,还要Review commit message本身及要求作者提升其质量,发现代码多个修改混合在一起以及要求提交者将它们分割成若干单个commit,及保证空格修改不会和函数修改夹杂在一起,保证代码重构和函数修改是分开的。等等。
可能也有人提到说gerrit对paitch系列化的的处理做的不是很好。这并不是一个避免patch序列化的有效原因。工具要服从开发者需要,而且它们是开源的,更能够提升。但是软件源码是“读多,写少”的,因此最重要的一点在于提升其被社区的众多者维护时提升其可维护性,而不是满足某些永远不再碰那些代码的人的诉求。
以下是关于良好或者不好的实践例子和详细指导。
代码修改的结构化分割
创建一个commit的基本原则在于每个commit有且仅有一个“逻辑修改”:
- 代码改变的越少,那么越快越容易review和发现其中的缺点。
- 如果某个修改后面发现有缺陷,那么需要回退这个有缺陷的commit,相比于夹杂着其他无关代码修改的情况,这更容易处理。
- 当使用GIT bisect工具定位问题时,小的良好定义的修改能够有助于定位哪个代码引入的问题。
- 当使用GIT annotate/blame 浏览历史时,小的良好定义的修改能够有助于定位引入问题的代码。
commit禁忌
- 夹杂着函数修改和空格修改。空格修改会遮掩重要的函数修改,这会让代码审查者很难正确地发现修改是正确的。
方案:创建2个commit,一个专门用于空格修改,一个用于函数修改。一般来说需要首先做空格修改,但并不是说一定要这样。
2. 两个无关的代码修改混合。这也会让代码审查者很难确定缺陷。如果后续又需要对某个修改进行整理时,它会增加引发bug的风险。
3. 用一个巨大的commit实现一个大的新特性。可能某个feature需要所有修改呈现后才会有效。然而,这并不意味着整个feature需要在单独某个commit中提交。新的feature通常需要重构当前代码,这种情况下最好是将当前代码重构部分和新特性实现部分分开。甚至新写的代码也能分成多个独立模块来进行review。比如说,添加新的APIs/classes的修改能够在自包含commit中。而这有助于代码review。如果整个feature还无法马上merge时,这也允许其他的开发者择优选择小部分代码。额外的APIs和接口实现需要同实际的内部实现分开。这能鼓励作者&审查者考虑通用API/RPC设计,而不是选择一个当前容易的设计。
基本规则在于:
如果代码修改能够分为patch/commits序列,那就分开。少不是多,多才是多。
错误示范
例1
1 | commit ae878fc8b9761d099a4145617e4a48cbeb390623 |
这个commit至少可以分为两个独立的修改:
- “hard_reboot”方法改为使用心得”reset”API
- 内部driver不再使用“hard_reboot”
那么问题在哪里呢?首先这些修改没有明确说明为什么这些代码需要同时修改。第一个commit能够包含哪些停止调用”hard_reboot”的代码。第二个提交能够重写”hard_reboot”的实现。
由于重构时在很多代码使用libvirt “reset”方法,审查者没有发现这引入一个新的libvirt API版本。这个commit后面很快就被发现是一个“罪人”,但由于其夹杂着大量无关修改导致无法进行关键性revert。
例2
1 | commit e0540dfed1c1276106105aea8d5765356961ef3d |
表面上看这引入了一个新的特性,使用单独commit也是合理的。但仔细看这个patch的话,它明显混杂了很多代码重构和新的LVM特性代码,这导致很难识别在支持QCow2/Raw镜像时可能的回退。该commit至少需要分为4个单独的commits:
- Replace the ‘use_cow_images’ config FLAG with the new FLAG ‘libvirt_local_images_type’, with back-compat code for support of legacy ‘use_cow_images’ FLAG
- Creation of internal “Image” class and subclasses for Raw & QCow2 image type impls.
- Refactor libvirt driver to replace raw/qcow2 image management code, with calls to the new “Image” class APIs
- Introduce the new “LVM” Image class implementation
正确示范
例子1
1 | commit 3114a97ba188895daff4a3d337b2c73855d4632d |
这两个修改提供配置KVM guest timer的支持,而引入创建libvirt XML 配置的API明显区别于使用新的API的KVM guest创建策略。
例子2
1 | commit 62bea64940cf629829e2945255cc34903f310115 |
这个commit序列重构了整个nova的RPC API层来允许可插拔消息实现。对于这种大的函数代码修改,只有分为一系列的单个commits才能真正有利于做代码审查,及便于跟踪/识别过程代码回退。
commit message中的信息
同代码修改同等重要的是commit消息描述。写commit消息时,这些事情需要牢记:
不要假定审查者理解根本问题是什么。当阅读bug报告时,在很多的反复评论之后,通常会变得混轮,commit消息需要清楚说明原始问题是什么。bug仅仅是识别问题的所感兴趣的历史背景,要尽可能使审查patch的时候就能确定其正确性而无需阅读bug提票。
不要假定审查者能够访问外部网络或者服务。我们不能保证他们能够访问在线bug跟踪系统,分布式SCM核心在于无须“在线”就能获取所有所需信息,commit消息应该整个是自包含的。
不要假定代码是不言自明的。对某人它是不言自明的,但是对于另一个人就可能是混乱不堪的。一定要记录原始问题是什么以及其如何解决(除了明显的打字错误和空白代码)。
描述为何要做这么改。一个普遍性错误在于仅仅记录代码怎么改的,而没有说明开发者选择这么改。要用尽量描述整个代码架构,特别是对于大的修改更要详细记录背后的目的/动机。
阅读commit消息来看它是否暗示提升了代码结构。当描述一个大的提交消息时,通常很明显需要分为多个commit。不要害怕回退及rebase后再修改后分成多个commit。
确保包含需要review什么的充分信息。当gerrit发送新patch提交的邮件提示时会包含小量信息,原则上是commit消息及所修改的文件列表。假如patch内容很多,那么期望所有reviewer详细检查patch是不合理的。commit消息需要有充分的信息来提示代码审查者需要review这个patch的哪些内容。
commit消息的第一行是最重要的。在GIT commits中第一行有特殊含义。它用于邮件主题,git annotate消息,gitk viewer annotations,…第一行。同总结修改本身一样,也需要详细考虑这部分代码的影响。比如说,如果它影响到libvirt driver,那么在第一行提到”libvirt”
描述当前代码的限制。如果代码未来还有提升空间,或者任何已知的限制都需要在commit消息中提到。这向审查者展示一个更为广阔的蓝图以及长短期之间的权衡。
基本规则如下:
commit消息必须包含整个理解&审查patch正确性的信息。更少的不是更多的,更多的才是更多的
包含外部引用
commit消息主要用于人类解释,但是通常会有给机器用的元数据。虽然GIT记录patch的作者&committer,通常情况下最好还是如同很多开源工程一样包含”Signed-off-by”tag。此外,patch上还能有人的认证信息比如说:
- ‘Reviewed-by: …some name.. <…email…>’ 尽管Gerrit会跟踪正式的review流程,一些patch会在提交到社区之前被其他人review
- ‘Suggested-by: …some name.. <…email…>’ patch作者之外的人建议代码增强或者设计影响
- ‘Reported-by: …some name.. <…email…>’ 某人报告bug被修改但写一个bug report
错误示范
例1
1 | commit 468e64d019f51d364afb30b0eed2ad09483e0b98 |
问题:这没有提到什么被import以及为何需要import这些。当然虽然信息在bug tracker中有记录,但也应当拷贝到commit消息中,使得其能提供一个自包含描述,比如:
1 | Add missing import of 'exception' in compute/utils.py |
例2
1 | author: [removed] |
问题:没有提到当前的格式及新的格式是什么。这个信息也在bug tracker中,且也需要包含到commit message中。另外,这个bug还修改了一个更早修改的回退,但它并没有提到更早的修改是什么. commit应该改成:
1 | Present correct ec2id format for volumes and snaps |
例3
1 | commit f28731c1941e57b776b519783b0337e52e1484ab |
这个commit只提到它做了什么,而没有提到为什么这么做。它应当提到更早的修改引入一个新的最小libvirt version,及当check失败时会发生什么。
1 | Add libvirt version check, min 0.9.7 |
正确示范
例1
1 | commit 3114a97ba188895daff4a3d337b2c73855d4632d |
例子中的commit message 提到的:
- 描述了问题场景是什么
- 描述了fix目的
- 描述修正的大致架构
- 说明了修正的局限性