LWN: 如何像kernel专家一样rebase & merge?

640点击上方蓝色字关注我们~



Rebasing and merging in kernel repositories

By Jonathan Corbet
June 18, 2019


本文是Jonathan在过去一个月撰写的一份kernel document,旨在merge window(合并窗口)期间减少子系统维护者所碰到的问题。如果一切顺利,会放置在Linux 5.3版本的Documentation/maintainer/rebasing-and-merging.txt。还有一个维护者手册在这里(https://www.kernel.org/doc/html/latest/maintainer/index.html  ),因为有一些可能会感兴趣的读者可能没有关注这个网站,所以Jonathan将此文也发布到LWN上了。


维护一个内核子系统,通常来说首先需要熟悉Git代这个源码管理系统。Git是一个拥有众多功能的强大工具,对于这种(功能众多并且强大的)工具的各个功能来说,会有正确的用法,也会有一些常见的错误用法。本文主要专注于rebase和merge这两个方面。维护者如果使用工具不当的话,一定会碰到麻烦,不过其实避免这些问题并不困难。


有一个通用规则首先要知道,也是与其他许多项目所不同的一点,kernel community通常不怕在开发历史里面出现merge commit。其实,因为Linux kernel的规模太大,基本不可能避免merge操作。有些代码维护者想避免merge操作,因此碰到错误。也有一些问题是因为merge得过于频繁。


Rebasing

"Rebasing"是个会改变仓库中一系列commit的历史的操作。有两类操作都会被称为“rebasing",因为它们都用到了git rebase命令,不过两者之间其实区别很大:

  • 改变一系列commit所基于的parent (starting) commit。例如,可以用rebase操作来把基于此前kernel发布版本的patch set来拿到当前最新kernel release版本之上。这个操作下面称之为“reparenting"。

  • 对一系列commit进行修改、删除操作,或者增加patch,在commit的changelog上增加tag,修改这几个patch的合入顺序等等。在下文中,这类操作被称为“history modification"。


我们所说的"rebasing"指的就是上述两种操作。正确使用的话,rebasing可以让开发历史信息更加干净清晰;用得不对的话,就会让代码历史信息混乱,或者引入bug。

有一些基本规则可以帮助开发者避免rebasing引入的最坏情况:

  • 已经从你个人本地的仓库发布出去的历史信息,通常不应该再改变了。其他人可能已经从你的git tree来pull了一份代码,并且基于这个版本进行一些开发了。此时如果修改你的代码树会让他们很麻烦。如果你觉得手头的工作可能需要做一些rebasing操作,那就意味着这些改动还不具备发布到公共代码库(public repository)的条件。

  • 不过还是有例外情况的。有些代码分支(linux-next就是一个最明显的例子)上频繁出现rebase操作,开发者们知道不应该基于这个分支来做开发工作。有时候开发者还会把自己的一个unstable branch(不稳定分支)提供给其他人进行测试,或者发布给自动测试系统。假如你确有需要发布这样的一个不稳定分支,请确保潜在用户们都知道不要基于这个分支做开发工作。

  • 不要对包含其他人工作的branch进行rebase操作。例如你是从其他开发者的代码仓库pull的改动,那么你就是他们代码历史的保管人,不能去改变他们的东西。也有少数例外情况,例如,这个代码分支里面有个错误的commit,那么就需要专门用git revert操作来回退,而不是简单的通过history modification的操作来让它消失。

  • 不要在没有正当理由的情况下对代码仓库进行reparent操作。“想使用最新的代码”或者“避免使用merge”操作,这都不是一个正当理由。

  • 如果必须要对代码库进行reparent操作,那么不能随意选择一个kernel commit来作为新的base版本。在两次正式release发布点之间,kernel通常稳定性不是在最好的情况,因此随意选择一个base版本的话会有更大概率碰到意外的bug。因此当你需要把一系列patch都更改到新的base版本上去的时候,需要选择一个稳定的节点(例如某个-rc release)作为新的base版本。

  • 要意识到reparenting一系列patch(或者对代码历史进行大量改动),事实上都改变了这些patch的工作环境(与之前开发时候的版本不同了),也意味着之前做的大量测试可能都无效了,需要再次测试。也就是说reparent过的patch set通常来说就要当做是一组新的patch set,来从头开始进行测试。


在merge-window里出现问题的话,有一个常见的原因,就是刚把一组patch给reparent到随便一个commit上,马上就发pull request给Linus Torvalds了。这类patch通常都没有经过完善的测试,同样,这种pull request通常也不会被接受。


相反,假如只对私有的代码仓库做rebasing操作,所有的commit都是基于一个仔细选择过的稳定版本,并且经过完善的测试,那么导致问题的可能性就很低了。


Merging

Merging是kernel开发流程里面的一个常见操作。在5.1版本的开发周期里,有1126个merge commit,约占所有commit数量的9%。Kernel的开发进展都是分别在100多个不同子系统的代码库里进行积累开发的,每个子系统的代码库都可能有多个topic branch(话题分支,指专用于开发某个功能的分支);每个branch的开发工作通常跟其他branch都是独立的。所以,每个branch要想合入upstream repository(上游仓库),自然都会至少有一次merge操作。


很多项目的开发规范都是要求每个pull request要求合入的branch都是要基于当前的主干分支上最新commit的,这样合入时代码历史中就不会有merge commit出现。而Linux kernel不是这样管理的,如果为了避免merge操作,而对branch代码采取rebase操作,多数情况下都会导致问题。


子系统的维护者意识到他们通常得做两种类型的merge操作:从更下一级子系统的代码仓库里merge代码,或者从其他(或者是平级别仓库,或者是mainline主线)来merge代码。这两种情况下的最佳操作方式也有不同。


Merging from lower-level trees

比较大的子系统可能会有多级维护者,其中下一级的维护者会发pull request给上一级别的维护者。对这种pull request的合入操作通常都会生成一个merge commit,这个行为本身设计如此。实际上,子系统的维护者应该用 --no-ff 这个Git参数来强制生成merge commit,这样确保在某些少见的不会自动生成merge commit的情况下也能生成merge commit,并且记录下这次merge操作的缘由。这笔merge操作——其实任何merge操作都一样——都应该在changelog里面记录下为什么要做这个merge动作。对于低一级别的代码仓库,这里要记录的原因通常是这笔pull操作会引入的改动的总结列表。


各级维护者对他们发出的pull request都应该加个signed tag,这样上游仓库的维护者需要在pull这个branch的时候验证这个tag。如果不做这个操作的话,会引入kernel开发流程的安全性问题。


基于上面介绍的规则,每次你合入别人的代码改动到自己的代码仓库后,就不应该再进行rebase操作了,哪怕你有权限做这个操作也要避免。


Merging from sibling or upstream trees

其实从downstream(下游分支)merge代码是比较常见的操作,没有太多好说的。不过从其他代码仓库merge代码就需要在推送到upstream上游分支的时候提高警惕了。这样的合入通常都需要仔细想清楚,并且提供充足理由。否则的话对upstream发出的pull request很可能会被拒绝。


很多人都想把master branch(主干分支)的代码merge到本地仓库里。这种类型的merge通常被称为“back merge”。Back merge有助于确认在并行开发过程中是否有冲突,保持跟master branch的最新版本同步通常也会给开发者一个很安心、舒服的感觉。不过这种倾向绝大多数情况下都需要严格避免。


为什么呢?因为back merge会弄乱你自己branch的开发历史。这通常会显著增加你碰到community里其他人改动引入bug的概率,也会很难确定你自己负责的这部分改动是否稳定、能否达到推送给upstream的状态。


频繁进行merge操作也会让你的代码仓库中的开发流程变得晦涩难懂,这会让跟其他代码仓库的交互merge操作(这种操作在一个管理规范的仓库中通常不应该发生)信息变得不够显眼。


其实,有些极少情况下,back merge还是有用的。每次进行这种操作的时候,都请确保在commit message中记录好为什么需要进行back merge。并且每次一定要避免随便选择一个commit,而是要基于一个已知稳定的版本进行merge。哪怕是确有需要,你也绝不应该直接把这些改动给back merge到你的upstream代码版本上,而是应该提醒upstream仓库的维护者来先进行这个back merge操作。


有一种最常见的merge操作相关的问题,就是在维护者想要发送pull request之前,为了确保解决merge conflict(合并时产生的冲突),先和upstream代码进行merge操作。确实,这种想法是很可以理解的,不过绝对是要避免的。特别是对最后给Torvalds发送的pull request来说,格外要注意这点,因为Torvalds明确的表示他宁可看到merge conflict,也不愿意大家进行不必要的back merge。每次看到有conflict代码冲突,就能知道哪里可能有潜在问题。他会进行非常多的merge操作(5.1版本开发周期中他有382次merge),比起普通开发者,也更擅长处理代码conflict的情况。


那么如果maintainer确实看到他们的子系统分支和mainline分支有conflict,那该做什么呢?最重要的一个步骤就是要在pull request里面提醒Torvalds这里可能会有conflict,至少这样会表明你是意识到你的branch跟upstream整体配合的情况的。


对一些特别困难的conflict,可以创建一个单独演示分支,在此解决掉这些conflict,然后在pull request里面附带提一下这个演示分支,不过pull request本身还是得用那个没有进行merge的代码版本的。


就算没有已知的merge conflict,在发出pull request之前做一次test merge也是一个好主意。可能可以提醒你注意到一些linux-next branch上没有看到的问题,帮助你理清你需要让upstream这边做什么操作。


从upstream或者其他子系统的代码库进行merge的另一个常见原因,是解决dependency依赖关系。这种依赖关系问题时不时会发生,确实有时候从另外一个代码库进行cross-merge(相互merge)是最好的解决办法。同样,在这种情况下,这个merge commit的描述信息里需要解释为什么要做这个merge操作。请花些时间把它写好写完整,其他开发者会仔细读这个changelog的。


不过,通常依赖关系问题也意味着需要改变一些实现方式。从另一个子系统的代码库做merge操作来解决依赖问题,会有更大风险引入其他bug,还是应该尽量避免这些操作。假设那个子系统的代码库改动没能被pull到upstream上去,不管是出于什么原因,都会导致你的代码库也无法合入upstream。比较好的解决方法,可能是跟相关的子系统维护者商量好,把有关联的改动都放到一个代码仓库(而不是分散在两边),或者专门创建一个topic branch来进行公共部分改动的开发,这样后面这个topic branch可以被merge到两个代码仓库里去。假设这个依赖关系是源自一个核心代码框架的改动,那么正确解决方法可能是等这些依赖的commit完成一个development cycle(开发周期)的验证,能确保这些改动在mainline上有较长的时间来稳定下来。


Finally

还有个常见操作是在每个development cycle的开始的时间点来把mainline的改动merge到自己的代码库,从而能拿到kernel里其他子系统的改动以及fix。同样,这种merge操作也需要挑选一个已知的发布节点,不能随便挑一个节点。如果你的跟upstream绑定的branch已经在上次merge window里面把所有本地改动都合入mainline了,那么你可以使用类似下面命令来pull mainline的代码:


这里的^0 会让git进行fast-forward merge(这种情况下应该会成功),这样避免引入一个不该出现的虚假merge commit。


上述列出的这些原则,仅仅是一些原则。肯定会有一些情况需要用到其他解决方案,这些guideline也并不是禁止开发者做什么操作,在有需要的时候肯定应该选择最合适的方案。不过开发者需要三思,是否真的有需要打破上述原则,是的话就需要解释清楚为什么要进行这些非常规操作。


全文完

LWN文章遵循CC BY-SA 4.0许可协议。

极度欢迎将文章分享到朋友圈 
热烈欢迎转载以及基于现有协议上的修改再创作~


长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~


640?wx_fmt=jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值