英语

变基和合并

一般来说,维护一个子系统需要熟悉 Git 源代码管理系统。Git 是一个功能强大的工具,有很多功能;正如通常使用此类工具一样,使用这些功能有正确和错误的方法。本文档特别关注变基和合并的使用。维护者在不正确地使用这些工具时经常会遇到麻烦,但实际上避免问题并不那么难。

一般需要注意的一点是,与许多其他项目不同,内核社区并不害怕在其开发历史中看到合并提交。实际上,考虑到项目的规模,避免合并几乎是不可能的。维护者遇到的一些问题源于避免合并的愿望,而另一些问题则来自合并过于频繁。

变基

“变基”是更改存储库中一系列提交历史的过程。有两种不同的操作被称为变基,因为它们都是使用 git rebase 命令完成的,但它们之间存在显着差异。

  • 更改构建一系列补丁的父(起始)提交。例如,变基操作可以将基于先前内核版本构建的补丁集改为基于当前版本。在下面的讨论中,我们将此操作称为“重新设置父级”。

  • 通过修复(或删除)损坏的提交、添加补丁、向提交更改日志添加标签或更改提交的应用顺序来更改一组补丁的历史记录。在下面的文本中,此类操作将被称为“历史修改”。

术语“变基”将用于指代上述两种操作。如果使用得当,变基可以产生更清晰的开发历史;如果使用不当,它会模糊历史并引入错误。

有一些经验法则可以帮助开发人员避免变基的最糟糕的危险。

  • 通常不应更改已暴露在你私有系统之外的世界的历史记录。其他人可能已经拉取了你树的副本并在此基础上进行构建;修改你的树会给他们带来痛苦。如果工作需要变基,这通常表明它尚未准备好提交到公共存储库。

    也就是说,总会有例外。一些树(例如,linux-next 是一个重要的例子)本质上经常被变基,并且开发人员知道不要在此基础上进行工作。开发人员有时会公开一个不稳定的分支,供其他人测试或用于自动化测试服务。如果你以这种方式公开可能不稳定的分支,请确保潜在用户知道不要在此基础上进行工作。

  • 不要变基包含其他人创建的历史记录的分支。如果你从另一个开发人员的存储库中拉取了更改,那么你现在是他们历史记录的保管人。你不应该改变它。除少数例外,例如,应该显式回滚像这样的树中的损坏提交,而不是通过历史修改使其消失。

  • 如果没有充分的理由,请不要重新设置树的父级。仅仅基于较新的基础或避免与上游存储库合并通常不是一个好理由。

  • 如果必须重新设置存储库的父级,请不要选择一些随机的内核提交作为新的基础。内核通常在发布点之间处于相对不稳定的状态;基于这些点之一进行开发会增加遇到意外错误的机会。当补丁系列必须移动到新的基础时,请选择一个稳定的点(例如 -rc 版本之一)进行移动。

  • 认识到重新设置补丁系列的父级(或进行重要的历史修改)会改变其开发的​​环境,并且可能会使所做的许多测试无效。一般来说,重新设置父级的补丁系列应像对待新代码一样对待,并从头开始重新测试。

合并窗口问题的常见原因是,当 Linus 在发送拉取请求前不久收到一个明显被重新设置父级的补丁系列(通常是随机提交)时。这种系列经过充分测试的机会相对较低,并且拉取请求被接受的机会也相对较低。

相反,如果变基仅限于私有树,提交基于众所周知的起始点,并且经过充分测试,则出现问题的可能性很低。

合并

合并是内核开发过程中常见的操作;5.1 开发周期包括 1,126 次合并提交,几乎占总数的 9%。内核工作在 100 多个不同的子系统树中累积,每个子系统树可能包含多个主题分支;每个分支通常独立于其他分支进行开发。因此,自然地,在任何给定分支进入上游存储库之前,至少需要一次合并。

许多项目要求拉取请求中的分支基于当前的主干,以便历史记录中不出现合并提交。内核不是这样的项目;任何为了避免合并而对分支进行的变基都极有可能导致麻烦。

子系统维护者发现自己必须执行两种类型的合并:来自较低级别的子系统树的合并和来自其他树的合并,无论是同级树还是主线。在这两种情况下,遵循的最佳实践有所不同。

从较低级别的树合并

较大的子系统往往有多个级别的维护者,较低级别的维护者向较高级别发送拉取请求。处理此类拉取请求几乎肯定会生成合并提交;这是应该的。实际上,子系统维护者可能希望使用 --no-ff 标志来强制添加通常不会创建的合并提交,以便可以记录合并的原因。对于任何类型的合并,合并的更改日志都应说明为什么要进行合并。对于较低级别的树,“为什么”通常是对该拉取请求将带来的更改的摘要。

所有级别的维护者都应该在其拉取请求上使用签名标签,并且上游维护者在拉取分支时应验证标签。如果这样做失败,将会威胁到整个开发过程的安全性。

按照上述规则,一旦你将其他人的历史合并到你的树中,你就不能变基该分支,即使你原本可以这样做。

从同级或上游树合并

虽然来自下游的合并很常见且不引人注目,但在将分支推送到上游时,来自其他树的合并往往是危险信号。此类合并需要仔细考虑并充分证明其合理性,否则随后的拉取请求很有可能会被拒绝。

很自然地希望将 master 分支合并到存储库中;这种类型的合并通常称为“反向合并”。反向合并有助于确保与并行开发没有冲突,并且通常会给人一种感觉良好的更新感觉。但是,几乎所有时候都应避免这种诱惑。

为什么会这样?反向合并会混淆你自己的分支的开发历史。它们将大大增加你在社区其他地方遇到错误的机会,并且难以确保你正在管理的工作是稳定且已准备好用于上游的。频繁的合并还会掩盖你树中开发过程的问题;它们可以隐藏与不应该(通常)在管理良好的分支中发生的其他树的交互。

也就是说,偶尔需要反向合并;当这种情况发生时,请务必在提交消息中记录为什么需要它。与往常一样,合并到众所周知的稳定点,而不是某些随机的提交。即使这样,你也不应将树反向合并到直接上游树之上;如果确实需要更高级别的反向合并,则应首先由上游树执行。

与合并相关的麻烦的最常见原因之一是维护者为了在发送拉取请求之前解决合并冲突而与上游合并时。同样,这种诱惑很容易理解,但绝对应该避免。对于最终的拉取请求尤其如此:Linus 坚持认为他宁愿看到合并冲突也不愿看到不必要的反向合并。看到冲突让他知道潜在的问题区域在哪里。他进行了大量合并(在 5.1 开发周期中为 382 次),并且在解决冲突方面做得很好,通常比相关开发人员更好。

那么,当子系统分支和主线之间存在冲突时,维护者应该怎么做?最重要的一步是在拉取请求中警告 Linus 将会发生冲突;至少,这表明你了解你的分支如何适应整体。对于特别困难的冲突,请创建并推送一个单独的分支,以显示你将如何解决问题。在你的拉取请求中提及该分支,但是拉取请求本身应该针对未合并的分支。

即使在没有已知冲突的情况下,在发送拉取请求之前进行测试合并也是一个好主意。它可能会提醒你注意你不知何故没有从 linux-next 中看到的错误,并有助于理解你正在要求上游执行的操作。

执行上游或其他子系统树的合并的另一个原因是解决依赖关系。这些依赖关系问题有时确实会发生,有时与另一棵树进行交叉合并是解决这些问题的最佳方法;与往常一样,在这种情况下,合并提交应解释为什么进行合并。花一点时间正确地执行此操作;人们会阅读这些更改日志。

但是,通常,依赖关系问题表明需要更改方法。合并另一个子系统树以解决依赖关系可能会带来其他错误,因此几乎不应该这样做。如果该子系统树未能被拉入上游,那么它遇到的任何问题也会阻止你的树的合并。更好的替代方法包括与维护者达成协议,以便在一个树中携带两组更改,或者创建一个专门用于可合并到两个树中的先决条件的提交的主题分支。如果依赖关系与重大的基础设施更改相关,则正确的解决方案可能是为一个开发周期保留相关的提交,以便这些更改有时间在主线中稳定下来。

最后

在开发周期的早期,为了获取代码树中其他地方的更改和修复,与主线合并是相对常见的做法。与往常一样,这样的合并应该选择一个众所周知的发布点,而不是一些随机的位置。如果在合并窗口期间,你的上游分支已经完全并入主线,你可以使用类似以下的命令将其向前拉:

git merge --ff-only v5.2-rc1

上面列出的指导方针仅仅是指导方针。总会有需要不同解决方案的情况,这些指导方针不应阻止开发人员在需要时做正确的事情。但人们应该始终思考是否真的有必要这样做,并准备好解释为什么需要做一些不寻常的事情。