什么使得在分布式版本控制系统中进行合并变得容易?

48

我在Joel on Software上读到:

使用分布式版本控制,分布式部分实际上并不是最有趣的部分。

有趣的部分是这些系统从变化的角度思考,而不是从版本的角度思考。

以及在HgInit上:

当我们需要合并时,Subversion 试图查看两个版本 - 我修改的代码和你修改的代码 - 并尝试猜测如何将它们合并成一个大的混乱。它通常会失败,并产生许多“合并冲突”,这些冲突实际上并不是冲突,只是 Subversion 无法弄清楚我们做了什么的地方。

相比之下,在 Mercurial 中我们分别工作时,Mercurial 忙于保持一系列的变更集。因此,当我们想要合并我们的代码时,Mercurial 实际上拥有更多的信息:它知道我们每个人都做了什么更改,并且可以重新应用这些更改,而不仅仅是查看最终产品并尝试猜测如何将其组合在一起。

通过查看SVN的存储库文件夹,我有印象Subversion将每个版本都维护为“更改集”。从我的了解来看,Hg同时使用“更改集”和“快照”,而Git纯粹使用“快照”来存储数据。
如果我的假设正确,那么必须存在其他使DVCS合并变得容易的方法。那些是什么?
*更新:*
  • 我更关心技术层面,但非技术层面的回答也可以接受。
  • 更正:
    1. Git的概念模型完全基于快照。这些快照可以存储为其他快照的差异,只是这些差异纯粹是为了存储优化。——Rafał Dowgirdcomment
  • 从非技术层面来看:
    1. 这仅仅是文化问题:如果合并很困难,分布式版本控制系统将根本无法工作,因此分布式版本控制系统的开发人员会花费大量时间和精力使合并变得容易。相反,集中式版本控制系统的用户习惯于糟糕的合并,因此开发人员没有动力让它工作。(当你的用户为烂东西付费时,为什么要做好东西呢?)
      ...
      总之,分布式版本控制系统的整个目的就是拥有许多分散的存储库,并不断地在它们之间合并更改。没有良好的合并,分布式版本控制系统就毫无用处。然而,集中式版本控制系统可以在糟糕的合并下仍然存活,特别是如果供应商可以让其用户避免分支。——Jörg W Mittaganswer
  • 从技术角度来看:
    1. 记录真正的历史DAG确实有所帮助!我认为主要区别在于CVCS并不总是将合并记录为具有多个父项的更改集,从而丢失了一些信息。——tonfacomment
    2. 由于合并跟踪,以及更基本的事实,即每个修订版本都知道其父项。...当每个修订版本(每个提交),包括合并提交,都知道其父项时(对于合并提交,这意味着拥有/记住多个父项,即合并跟踪),您可以重构修订历史的图表(DAG = 直接无环图)。如果您知道修订图形,则可以找到要合并的提交的公共祖先。当您的分布式版本控制系统知道如何查找公共祖先时,您不需要将其作为参数提供,例如在CVS中。
      .
      请注意,两个(或多个)提交可能有多个公共祖先。Git使用所谓的“递归”合并策略,它合并合并基(公共祖先),直到您留下一个虚拟/有效的公共祖先(在某些简化中),然后可以进行简单的3路合并。——Jakub Narębskianswer

请检查在Git中合并代码相比于SVN有何优势?


2
“Git 纯粹使用快照来存储数据” - 这只是部分正确。Git 的概念模型纯粹基于快照。这些快照可以作为其他快照的差异存储,只是这些差异纯粹用于存储优化。 - Rafał Dowgird
1
请注意,HgInit在该页面上存在一个错误或至少是误导性的观点:它声称如果您同时移动函数并更改函数,分布式版本控制系统将能够合并该更改。实际上,这种移动/复制跟踪只会在整个文件级别上发生。 - Laurens Holst
我的理解是Git可以跟踪这种更改(虽然我还没有进行测试来确认)。我不知道Mercurial是否也有这个功能。 - Marnen Laibow-Koser
有人想将这个与https://dev59.com/EnE95IYBdhLWcg3wDpqg和/或https://dev59.com/F3VD5IYBdhLWcg3wO5ED合并吗?我认为它们都有有趣的答案(以及一些过时的答案),但它们涵盖了完全相同的内容。 - IMSoP
9个回答

32
DVCS和CVCS并没有什么特别的东西使得合并更容易。这只是一种文化现象:如果合并很困难,DVCS根本无法正常工作,因此DVCS开发人员花费了大量时间和精力使合并变得容易。另一方面,CVCS用户习惯了糟糕的合并,因此开发人员没有动力让它正常工作。(当你的用户为垃圾付款时,为什么要创造优良的东西呢?)
Linus Torvalds在他的Git演讲中说,当他在Transmeta使用CVS时,他们在开发周期中留出了整整一周的时间来进行合并。而且每个人都将其视为正常状态。如今,在合并窗口期间,Linus可以在短短几个小时内完成数百个合并。
如果CVCS用户向供应商表示这种糟糕的合并是不可接受的,那么CVCS就可以具有与DVCS相同的出色合并功能。但他们陷入了"Blub悖论":因为他们从未看过一个有效的合并系统,所以他们根本不知道这是不可接受的。他们不知道还有更好的选择。
当他们尝试了DVCS后,就会神奇地认为所有的好处都归功于"D"部分。
理论上,由于CVCS具有集中式的特性,它应该具有更好的合并功能,因为它拥有整个历史记录的全局视图,而不像DVCS每个仓库只有一个小片段。
总之,DVCS的整个目的就是拥有许多分散的仓库并不断地进行变更的合并。没有良好的合并,DVCS就是无用的。然而,CVCS可以在糟糕的合并下存活,特别是如果供应商可以促使用户避免分支。
因此,就像软件工程中的其他一切一样,这只是一个努力的问题。

7
记录真实的有向无环图(DAG)历史确实有帮助!我认为主要的区别在于,集中式版本控制系统(CVCS)并没有总是将合并记录为一个具有多个父节点的变更集,导致丢失了一些信息。 - tonfa
5
@tonfa:当然,你是正确的。但再次强调,这并不是CVCS本身的限制,而只是开发者懒惰的表现。CVCS理论上可以记录完整的DAG,包括合并操作。Subversion花了10年时间才开始记录合并操作,特别是已经有第三方工具可用至少5年的情况下,这一点非常值得深思。换句话说,他们在10年前就已经有了所有需要的东西,而且没有对数据格式进行任何更改! - Jörg W Mittag
我认为往返服务器的次数也会影响。由于Hg具有完整的本地历史记录,因此信息就在那里,而Subversion则不同。 - myron-semack
@msemack:你可以通过向SVN网络协议添加一个新命令“将A合并到B”来在服务器上执行合并。或者,您可以在客户端缓存整个历史记录。Subversion 已经缓存了一个修订版本,没有理由它不能缓存所有修订版本。(而且当他们这样做时:清理缓存,因为目前Subversion需要更多的磁盘空间来缓存一个修订版本,而Git和Mercurial需要缓存1000个修订版本。Subversion(~20000 revs)的Mercurial checkout只比Subversion(1 rev)的Subversion checkout略大一些)。 - Jörg W Mittag

23
在Git和其他分布式版本控制系统中,合并很容易,不是因为像Joel所说的一些神秘的变更集序列视图(除非您使用具有补丁理论或某些受Darcs启发的DVCS的Darcs;尽管它们是少数),而是因为有了合并跟踪和每个修订版本都知道其父代的更基本事实。为此,您需要(我认为)整个树/完整存储库提交...这不幸地限制了进行部分检出的能力,并使提交仅涉及文件子集的能力。
当每个修订版本(每个提交),包括合并提交,都知道其父代时(对于合并提交,这意味着具有/记住多个父代,即合并跟踪),您可以重建修订历史记录的图表(DAG =直接无环图)。如果您知道修订版的图形,您可以找到要合并的提交的公共祖先。当您的DVCS知道如何查找公共祖先时,您无需提供它作为参数,例如在CVS中。
请注意,两个(或多个)提交可能有不止一个公共祖先。Git利用所谓的“递归”合并策略,合并合并基础(公共祖先),直到您剩下一个虚拟/有效的公共祖先(在某些简化中),然后可以进行简单的三方合并。

Git使用重命名检测旨在处理涉及文件重命名的合并。(这支持Jörg W Mittag 的观点:分布式版本控制系统具有更好的合并支持,因为它们必须拥有该功能,由于合并在CVCS中比在“更新”命令中被隐藏,在更新-提交工作流程中要常见得多。 参见Eric S. Raymond的 Understanding Version Control(WIP))。


那么,实际上,酷炫的“合并容易”的分布式版本控制系统的唯一区别是允许你在合并时省略祖先的特性吗?在您看来,获取祖先是否真的很痛苦且耗时? - systempuntoout
@systempuntoout:像Git和Mercurial这样的DVCS存储查找共同祖先所需的信息;Subversion则不会(即使使用Subversion 1.6及其以svn:mergeinfo属性形式合并的信息也不容易:在我看来,svn:mergeinfo设计有误且解决了错误的问题,但它是Subversion“分支”概念的结果)。 - Jakub Narębski
我认为寻找共同祖先的能力是我见过的第一个说明分布式版本控制系统(DVCS)的架构如何使复杂合并计算基本更容易(例如在多个方向上相互作用的多个分支之间),而不仅仅是聪明地使用相同的信息(每个分支记录的一系列更改)(例如更好地处理重命名)。 - IMSoP
@JakubNarębski 我现在很好奇,既然我们已经走过了5年的路程,Git/Hg和SVN之间的这些差异是否仍然存在? - SiegeX
@SiegeX:Subversion在跟踪使用svn:mergeinfo属性(和svn:copyfrom)合并的内容方面有了很大的改进,更加自动化。但这是一个复杂的内部实现。特别是它仍然存在移动(重命名)方面的问题。 - Jakub Narębski
显示剩余3条评论

10

部分原因当然是技术上的论点,即DVCS存储的信息比SVN多(DAG、拷贝),并且具有更简单的内部模型,这就是为什么它能够执行更准确的合并,正如其他回答中所提到的。

然而,更重要的差异可能在于,因为你拥有本地存储库,所以可以频繁地进行小提交,也可以经常拉取和合并传入的更改。这主要是由“人的因素”造成的,即人类使用集中式VCS与DVCS的方式不同。

对于SVN,如果您更新时存在冲突,SVN将合并可用的内容,并在无法合并的代码中插入标记。这个问题很大,因为您的代码现在将不再处于可工作状态,直到您解决所有冲突为止。

这会分散你从事任务的注意力,因此通常SVN用户在处理任务时不会进行合并。再加上SVN用户还倾向于让更改在一个大的提交中累积,以防止破坏其他人的工作副本,因此在分支和合并之间将会有很长一段时间。

使用Mercurial,你可以在较小的增量提交之间更频繁地与传入的更改进行合并。这将从定义上导致较少的合并冲突,因为您将在一个更及时的代码库上工作。

而且如果出现冲突,你可以决定推迟合并,并在你自己的空闲时间内完成它。这特别使得合并变得不那么烦人。


请注意,上面我主要谈论的是匿名分支(SVN工作副本,因此由svn update执行的合并),但这也适用于命名分支(SVN分支)。 - Laurens Holst

7

哇,五段式论文的攻击!

简而言之,没有什么能让它变得容易。这很难,我的经验表明错误确实会发生。但是:

  • DVCS 强制您处理合并,这意味着花费几分钟熟悉现有的工具来帮助您。这本身就有所帮助。

  • DVCS 鼓励您频繁合并,这也有所帮助。

你引用的 hginit 片段声称 Subversion 无法进行三方合并,并且 Mercurial 通过查看两个分支中的所有 changeset 进行合并,这两点都是错误的。


1
简而言之,我认为这既有技术因素——更好的合并算法,也有工作流程因素——分布式版本控制系统更好地支持频繁合并。 - Laurens Holst
1
分布式版本控制系统对工作副本有一个更简单的视图。在Subversion中,您可以拥有混合修订工作副本和部分工作副本,这显然不是使合并更容易的组成部分。 - Bert Huijben
@Laurens,你指的更好的合并算法是什么?除非我大错特错,Mercurial在合并所有可用变更方面并没有比svn up更高级的功能。而且我认为这两个程序都通过将问题委托给某些外部合并程序(或者如果失败,则在文件中放置冲突标记)来处理冲突。 - Jason Orendorff
也许我错了,但即使 SVN 能够跟踪副本,我从未见过对副本进行的合并操作成功过。 - Laurens Holst

2

其中一个问题是svn的合并存在微妙的错误;请参见http://blogs.open.collab.net/svn/2008/07/subversion-merg.html。我怀疑这与svn即使在挑选合并时记录合并信息有关。再加上处理边界情况时存在一些简单的错误,因此作为CVCS的代表,svn看起来很糟糕,而所有已经正确解决问题的DVCS却和它不一样。


1
这个死链接实际上在网上出现了很多次,因此 Wayback Machine 的等效物可能会有用:https://web.archive.org/web/20080827133904/http://blogs.open.collab.net/svn/2008/07/subversion-merg.html - Ron Burk

1

我认为,正如其他人所提到的,变更集的DAG(有向无环图)是一个很大的区别。分布式版本控制系统需要在根本层面上进行拆分历史记录(和合并),而我想传统的集中式版本控制系统(CVCS)(它们更早)从第一天开始就是为了首先跟踪修订版和文件,合并支持是事后添加的。

所以:

  • 当标签/分支与源代码目录树分开跟踪时,合并易于执行和跟踪,因此整个存储库可以一次性合并。
  • 由于分布式版本控制系统具有本地存储库,因此很容易创建这些存储库,因此很容易将不同的模块保留在不同的存储库中,而不是在一个大型存储库中跟踪所有模块。(因此,存储库范围内的合并不会像在svn/cvs中那样引起相同的中断,其中一个存储库通常包含许多不相关的模块,这些模块需要具有单独的合并历史记录。)
  • CVS/SVN允许工作目录中的不同文件来自不同的修订版,而分布式版本控制系统通常对整个工作副本只有一个修订版(即使将文件还原为早期版本,它也会显示为已修改状态,因为它与签出的修订版中的文件不同。SVN/CVS并非总是如此。)
混合这些概念(如Subversion所做的)是一个大错误。例如,分支/标签在源代码树中,因此您必须跟踪哪些文件修订版本已合并到其他文件中。这显然比仅跟踪已合并的修订版本更复杂。
因此,总结一下:
- 分布式版本控制系统需要易于合并,并且其功能集基于此。设计决策是使这些合并易于执行和跟踪(通过DAG),并实现其他功能(分支/标签/子模块)以适应此要求,而不是相反。 - 集中式版本控制系统从一开始就有一些功能(例如模块),这使得某些事情变得容易,但使整个存储库的合并非常棘手。
至少这是我从使用cvs、svn、git和hg的经验中感受到的。(可能还有其他集中式版本控制系统也做对了这件事。)

嗯...Subversion将目录作为版本历史中的一级对象进行跟踪。这是一个很好的决定(因为它使得跟踪副本和删除变得容易,而这在以前非常困难),但并不是使合并和重命名处理更容易的决定。在基于文件的版本控制世界中,混合修订工作副本很常见,但这是另一件使合并更加困难的事情。新的分布式版本控制系统从过去中吸取了教训,做出了其他选择;这解决了一些场景,但同时也引入了其他问题。(但这些都不是分布式或集中式版本控制系统特定的问题;只是实现上的区别) - Bert Huijben

1
一件我觉得在分布式版本控制系统中更容易的事情是,每个开发者可以将自己的改动合并到任何一个仓库中。当你合并自己的代码时,处理合并冲突要简单得多。我曾经在一些地方工作过,有些可怜的人通过找到每个开发者来解决合并冲突。
而且,在分布式版本控制系统中,你可以做一些很酷的事情,比如克隆一个仓库,将两个开发者的工作合并到克隆中,测试这些变更,然后将克隆中的内容合并回主仓库。
真是挺酷的东西。

1
作为历史记录,现在已经过时的PRCS系统也知道共同祖先并且可以高效地合并,尽管它不是分布式的(它是建立在RCS文件之上的!)。这意味着它可以被有效地迁移到git并保留历史记录。

-10
也许分布式版本控制系统(DVCS)的用户只是从未做出像修改项目中大部分文件的重构或重命名/复制,或者从头开始重新设计数百个文件中使用的API等使合并变得困难的操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接