Subversion merge reintegrate

Filed Under (Submerged) by racoonwise on 17-07-2008

这篇文章包含了一些与Subversion 1.5合并相关的主题。目的是解释反射合并的问题,新的reintegrate选项怎么用和一些这个选项现存的问题。

反射/循环(Reflective/Cyclic)合并

最简单的解释方法是举例子。假设你现在在一个从主干拷贝过来的分支上工作。在开发的过程中,你定期的把主干的修改“全”都合并到工作分支,这样才能使之与主干“同步”。当你最后把分支合并回主干的时候,这就叫做反射(循环)合并。这种合并对Subversion来说是个问题,让我们看看为什么。

版本号和合并跟踪

Subversion是基于版本号的。每一次检入都建立一个版本号,一个合并最终也只是一个检入。Subversion 1.5的合并跟踪功能是记录哪个版本是从哪个路径合并过来的。在上面对例子中,那种方法太烂了,不能总是得到想要的结果。回想例子中我们定期从主干合并到分支。每一次合并都在分支上产生一个版本号。当最后要从分支合并回主干的时候,合并跟踪能做的是帮助你决定哪些是包括或者排除哪些“同步了的”版本。问题是两种答案都不总是对的。

如果排除那些从主干合并的版本,那么同样会排除我们为了解决冲突所做的任何工作。甚至更糟,我们可能粗心的提交了一些不相关的修改,那么这些修改同样会被排除。

如果包括那些同步的版本,那么合并回主干的时候那些修改已经存在了,这会产生不必要和混乱的冲突。

唯一的解决办法是在Subversion中发明一种新的、不依赖于版本号的合并算法。在讨论这个问题的时候,有人提出这样可能会要求设计新的版本库格式。作为1.5版本的开发周期,在现有模式下开发新算法的一些初步的工作已经做好了。最终我们决定新算法不包含在这个版本中。我们期待在1.6或者未来的版本中继续这个工作来看看它是否能作为一个解决方案。

再集成(Reintegrate to the Rescue)

把新算法的事先放一边,Subversion已经证明有能力解决反射合并的问题。在1.5版本以前叫做2-URL合并。回到前面的例子,假设最后一次从主干合并到分支,我们合并所有的修改到版本号100.为了适当的从分支合并回主干,我们在一个主干的工作拷贝上运行下列命令:

svn merge url://trunk@100 url://feature-branch .

这个命令告诉Subversion计算主干版本号100与分支最新版本的区别,然后合并这些区别回主干工作拷贝。因为这本质上只是产生一个diff,它包括了解决冲突的工作,但是不包括在两个地方都存在的修改,也就是说,我们得到了想要的结果。

新的reintegrate选项是2-URL合并的简略版。有了它你可以这样运行命令:

svn merge --reintegrate url://feature-branch .

在内部,当你用这个选项的时候,它计算url://trunk@100,然后执行和2-URL合并同样的工作,记住这点很重要。当你遇到与这个新选项的一些问题的时候,你可以用旧的2-URL语法去解决问题。换句话说,reintegrate只是一个新的语法加了些安全检查。如果这些安全检查失败并且不能够轻易的被修正,那么你可以用旧的语法。

用2-URL的一个大问题是你告诉它什么它都会做。你能很容易的做出命令捕获不到的错误。假设我们去掉例子中的@100,并且自从我们上次同步之后又检入了r101和r102版本,当2-URL合并运行的时候,它计算的是主干@HEAD和分支@HEAD的差别。对于合并的过程,看起来像是分支移除了r101和r102所做的修改并且合并会把他们从你的工作拷贝移除。依据在那些版本中的修改,你可能不会注意到并且最后的提交会那些修改作为合并的一部分移除。在这个问题上还有许多其他的变更(比如得到错误的URL),但是大多数的问题很明显,你可以恢复合并。

正因为2-URL在合并过程中的问题,当reintegrate选项加入的时候,我们决定让它在运行之前来进行一些安全检查。其中的一些相当顺利,像确定工作拷贝在单一的版本,没有转换的孩子,不是稀疏检出(如工作拷贝的深度是无限的)。如果reintegrate因为这些原因指出错误,你可以只用svn update或者svn switch来“修复”你的工作拷贝。因为reintegrate需要计算合并的基础URL和版本号,它还要一个祖先检查(ancestry check)来保证合并源与目的地相关。

最有争议的reintegrate检查是合并源没有任何的subtree mergeinfo。Mergeinfo(技术上的版本属性 svn:mergeinfo)储存合并的跟踪信息。通常mergeinfo只设在合并目的地。Subtree mergeinfo发生在当一个合并目标有一些子树,这些子树以前本身也是合并目标(比如我们从主干的一个文件合并到分支的一个文件,在那个文件上创建了mergeinfo。这种合并准确的叫法应该是子树合并)。当整合分支回主干的时候,reintegrate安全检查失败,因为分支有subtree mergeinfo。检查的原因是2-URL合并在这种情况下不总是给出正确的结果。

Reintegrate的问题和重命名的文件

大多数reintegrate问题都是起源于subtree mergeinfo的检查。当检查失败时,错误信息是这样的:

svn: Cannot reintegrate from 'url://feature-branch' yet:
Some revisions have been merged under it that have not been merged
into the reintegration target; merge them first, then retry.

最大的问题是,不想其他的检查失败,这个问题本身和出错方式都不明显。如果你真的做了一些子树合并,那么这些检查使你不犯错误。不幸的是,有一种通常的情况是,运行命令之后只“出现”像一个子树合并被执行一样:当你重命名/移动文件,mergeinfo在新的路径上被建立并且阻止reintegrate工作。为何复制/移动时创建mergeinfo超过了本文的内容,但是足以说明我们认为在大多数情况下mergeinfo不必创建。因此我们会去解决让在复制/移动时创建mergeinfo更聪明些(希望在1.5.1版本中实现)。如果这些子命令尽量减少创建mergeinfo,那么在使用reintegrate时将会极大的减少发生这种特殊问题(至少为了您将来的复制/移动操作)。

如果你发现这个问题,你可以这么做。

  1. 你可能只想手动档移除子树mergeinfo,可以运行如下命令: svn propdel svn:mergeinfo FOO你可以在移动后,检入前运行,那么问题就不会存在了。或者你可以在发现问题后再运行。通常,只有time mergeinfo需要在复制/移动一个路径像它最近的父母的mergeinfo在路径的源和目标不同的时候创建。因为一个路径没有明确的mergeinfo(当一个路径有svn:mergeinfo属性设立的时候就说它有明确的mergeinfo)会简单的继承它最近父母的明确mergeinfo。也就说说,svn:mergeinfo属性是可继承的并且如果一个复制的路径从相同的源和目标继承mergeinfo,那么通过在目标设置明确的mergeinfo会什么也得不到并且你可以安全的删除那个mergeinfo。

    在许多情况下,所有的mergeinfo在分支的根上存在。如果移动分支上的路径,那么它最近的带有mergeinfo的父母不会改变,这个路径也不需要新的mergeinfo。然而如果你先前做了一个子树合并,然后从子树外移动一个路径进来子树,那么路径会继承不同的mergeinfo。这种情况下目标路径应该保留它自己的明确mergeinfo。

  2. 这种情况下你还可以用老的2-URL语法。

另外的reintegrate问题

与前面问题密切相关的是当子树mergeinfo合并到分支或者当分支建立的时候打入到分支。如果你在主干上(当前创建mergeinfo)重命名一个路径,每个你从主干创建的分支从那时以前的也有这个子树mergeinfo。另外,如果你从主干拷贝出一个分支,然后在主干上拷贝一个路径,再同步你的分支,那么这个分支得到了子树mergeinfo。这两种情况都说明你不能够用reintegrate,至少除非你能删除子树上的svn:mergeinfo属性。

所以除了创建mergeinfo更聪明外,我们还需要检查是否reintegrate更安全的忽略一些情况下子树mergeinfo,允许reintegrate继续合并。例如,如果我们创建从主干创建一个分支,一些子树mergeinfo当分支创建的时候就出现了。只要子树mergeinfo在分支上存在,就需要在主干上也存在,这样运行reintegrate合并也许是安全的。当然这种检查开始变的相当复杂,所以我们不确定有简单的解决办法,但是希望有一些可以在1.6中实现。一个解决办法是值得考虑的,就是是否我们应该在第一个地方做检查?

分支管理和reintegrate

一个很重要的事需要指出(我不确定文档中是否有):一旦分支被reintegrate,它就应该被删除。如果更多的工作要做,需要创建一个新的拷贝。这样做有两个原因:

  1. 还记得反射合并的例子么?一旦你从分支合并回主干,如果你想要同步所有的修改到分支,你就会出现和分支反射合并同样的代码!你不能再用简单的“svn merge url://trunk“语法,你应该用2-URL合并来把你主干的修改合并到分支。
  2. 如果你没有把主干的修改合并到分支因为前面提到的问题,那么当你再一次用reintegrate时,它还会看r100,作为主干的最后版本合并到分支。所以当它diff trunk@100和你当前的分支,它不会看到你在r103合并分支回主干,并且会试图再合并一次。

底线是,如果你重建分支,你不会运行这一点。svn delete跟在svn copy,并且不占用任何版本库磁盘共建。你甚至不需要在你的工作拷贝做任何事情。照做吧!

将来的解决方案

本文已经花大量的时间聚焦在一些我们在发布1.5的时候意识到的问题上。我想表达的意思是让大家理解问题的根源。我不想让大家觉得“不幸和忧郁”。事实是整个新的1.5合并过程工作很顺利,在本质上有很大提高。尽管当前的reintegrate反射合并解决方案没有用新的合并算法,如果你理解流程,你可以开发一个工作流程来避免前面提到的局限性。还有一些其他的合并bug我们没有提到,但是会在接下来的1.5.1版本中(计划在这个月底)解决。

值得注意的是合并跟踪还只是在初级的阶段,开发社区有全世界的数据和用例来利用,使评估新算法和解决bug更简单。这应该帮助我们在1.5.x产品线解决bug更快速。在开发1.5.0的时候,缺乏庞大的,真正的世界性的用合并跟踪的版本库是个问题,但现在不存在了。

About author

Mark Phippard is a software engineer (with a great interest in open source) who specializes in development of software development tools, particularly those in the Software Configuration Management (SCM) space. He is the project lead for Subclipse, an open source Subversion plug-in for Eclipse as well as a full committer for Subversion.

原文链接:

http://blogs.open.collab.net/svn/2008/07/subversion-merg.html

Make a comment