猿问
下载APP

如何和/或为什么在Git中合并比在SVN中更好?

我在一些地方听说过,分布式版本控制系统之所以如此出色的主要原因之一,是与诸如SVN之类的传统工具相比,合并起来要好得多。这实际上是由于两个系统在工作方式上的固有差异,还是Git / Mercurial之类的特定 DVCS实现仅具有比SVN更聪明的合并算法?



RISEBY
浏览 60回答 3
3回答

123456qqq

为什么在DVCS中合并比在Subversion中更好的说法主要是基于前一段时间Subversion中分支和合并的工作方式。1.5.0之前的Subversion 没有存储有关合并分支的时间的任何信息,因此,当您要合并时,您必须指定必须合并的修订范围。那么,为什么Subversion合并很烂?思考这个例子:&nbsp; &nbsp; &nbsp; 1&nbsp; &nbsp;2&nbsp; &nbsp;4&nbsp; &nbsp; &nbsp;6&nbsp; &nbsp; &nbsp;8trunk o-->o-->o---->o---->o&nbsp; &nbsp; &nbsp; &nbsp;\&nbsp; &nbsp; &nbsp; &nbsp; \&nbsp; &nbsp;3&nbsp; &nbsp; &nbsp;5&nbsp; &nbsp; &nbsp;7b1&nbsp; &nbsp; &nbsp; &nbsp;+->o---->o---->o当我们想要将 b1的更改合并到中继中时,我们将站在站在已检出中继的文件夹中发出以下命令:svn merge -r 2:7 {link to branch b1}…它将尝试将更改合并b1到您的本地工作目录中。然后,在解决所有冲突并测试了结果之后,提交更改。当您提交修订树时,将如下所示:&nbsp; &nbsp; &nbsp; 1&nbsp; &nbsp;2&nbsp; &nbsp;4&nbsp; &nbsp; &nbsp;6&nbsp; &nbsp; &nbsp;8&nbsp; &nbsp;9trunk o-->o-->o---->o---->o-->o&nbsp; &nbsp; &nbsp; "the merge commit is at r9"&nbsp; &nbsp; &nbsp; &nbsp;\&nbsp; &nbsp; &nbsp; &nbsp; \&nbsp; &nbsp;3&nbsp; &nbsp; &nbsp;5&nbsp; &nbsp; &nbsp;7b1&nbsp; &nbsp; &nbsp; &nbsp;+->o---->o---->o但是,当版本树变大时,这种指定修订范围的方法很快就失控了,因为Subversion没有关于何时以及哪些修订合并在一起的元数据。思考以后会发生什么:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;12&nbsp; &nbsp; &nbsp; &nbsp; 14trunk&nbsp; …-->o-------->o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"Okay, so when did we merge last time?"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 13&nbsp; &nbsp; &nbsp; &nbsp; 15b1&nbsp; &nbsp; &nbsp;…----->o-------->oSubversion拥有的存储库设计在很大程度上是一个问题,为了创建分支,您需要在存储库中创建一个新的虚拟目录,该目录将存储主干副本,但不存储有关何时何地的任何信息。事情又重新融合了。有时会导致讨厌的合并冲突。更糟糕的是,Subversion默认情况下使用双向合并,当两个分支头未与其共同祖先进行比较时,它在自动合并方面存在一些严重的限制。为了缓解这种情况,Subversion现在存储用于分支和合并的元数据。那会解决所有问题吧?哦,顺便说一下,Subversion仍然很烂……在集中式系统(如Subversion)上,虚拟目录很烂。为什么?因为每个人都可以查看它们……甚至是垃圾实验的人。如果您想尝试,但不想看到每个人及其姨妈的实验,则分支是很好的。这是严重的认知噪音。您添加的分支越多,您看到的内容就越多。您在存储库中拥有的公共分支越多,跟踪所有不同分支的难度就越大。因此,您将要问的问题是分支是否仍在开发中,或者它是否真的已经死了,这在任何集中式版本控制系统中都很难分辨。从我所看到的大部分时间来看,组织将默认使用一个大分支。令人遗憾的是,这反过来将难以跟踪测试和发行版本,而分支带来的其他好处。那么,为什么GCS,Mercurial和Bazaar等DVCS在分支和合并方面比Subversion更好?原因很简单:分支是一流的概念。在设计上没有虚拟目录,而分支是DVCS中的硬对象,为了与存储库同步(即push和pull)简单地工作,就必须如此。使用DVCS时,要做的第一件事是克隆存储库(git clone,hg clone和bzr branch)。从概念上讲,克隆与在版本控制中创建分支相同。有人将其称为分叉或分支(尽管后者通常也用于指代共处分支),但这是同一回事。每个用户都运行自己的存储库,这意味着每个用户都在进行分支。版本结构不是树,而是图。更具体地说,是有向无环图(DAG,表示没有任何循环的图)。除了每个提交都有一个或多个父引用(该提交所基于的父引用)以外,您实际上不需要深入研究DAG的细节。因此,下图因此将反向显示修订之间的箭头。这是一个非常简单的合并示例。想象一个名为的中央存储库origin,一个用户Alice将存储库克隆到她的计算机上。&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…origin&nbsp; &nbsp;o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;| clone&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;v&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…alice&nbsp; &nbsp; o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^origin/master克隆期间发生的事情是,每个修订版本都被完全复制到Alice(已通过唯一可识别的hash-id验证),并标记了原始分支的位置。然后,Alice在自己的存储库中工作,在自己的存储库中提交并决定推送她的更改:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…origin&nbsp; &nbsp;o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^ master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "what'll happen after a push?"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;d…&nbsp; &nbsp;e…alice&nbsp; &nbsp; o<---o<---o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^origin/master解决方案非常简单,origin存储库唯一需要做的就是接受所有新修订并将其分支移至最新修订(git称为“快进”):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;d…&nbsp; &nbsp;e…origin&nbsp; &nbsp;o<---o<---o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^ master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;d…&nbsp; &nbsp;e…alice&nbsp; &nbsp; o<---o<---o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^origin/master我在上面说明的用例甚至不需要合并任何东西。因此,合并算法并不是真正的问题,因为所有版本控制系统之间的三向合并算法几乎相同。问题更多的是结构问题。那么,如何向我展示一个具有真实合并的示例呢?诚然,上面的示例是一个非常简单的用例,因此尽管更常见,但让我们做的更多。还记得origin从三个修订版开始吗?好吧,做这些的人叫他Bob,他一直在自己工作,并在自己的存储库中进行了提交:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;f…bob&nbsp; &nbsp; &nbsp; o<---o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ^ master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^ origin/master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"can Bob push his changes?"&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;d…&nbsp; &nbsp;e…origin&nbsp; &nbsp;o<---o<---o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^ master现在,Bob无法将其更改直接推送到origin存储库。系统如何通过检查Bob的修订版本是否直接从origins 下降而检测到的,在这种情况下不是这样。任何试图推入的尝试都会导致系统说出类似“ 呃...我怕不能让你那样做Bob”。所以,鲍勃有吸合,然后合并更改(用Git的pull;或HG的pull和merge;或BZR的merge)。这是一个两步过程。首先,Bob必须获取新修订,它将从origin存储库中复制它们。现在,我们可以看到图形有所不同:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;f…bob&nbsp; &nbsp; &nbsp; o<---o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; d…&nbsp; &nbsp;e…&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+----o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^ origin/master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;d…&nbsp; &nbsp;e…origin&nbsp; &nbsp;o<---o<---o<---o<---o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^ master拉取过程的第二步是合并不同的提示并提交结果:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;v master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;f…&nbsp; &nbsp; &nbsp; &nbsp;1…bob&nbsp; &nbsp; &nbsp; o<---o<---o<---o<-------o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; d…&nbsp; &nbsp;e…&nbsp; |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+----o<---o<--+&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^ origin/master希望合并不会发生冲突(如果您预计会发生冲突,则可以在git中使用fetch和手动进行两个步骤merge)。以后需要做的是将这些更改再次推送到中origin,这将导致快速合并,因为合并提交是origin存储库中最新消息的直接后代:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;v origin/master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;v master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;f…&nbsp; &nbsp; &nbsp; &nbsp;1…bob&nbsp; &nbsp; &nbsp; o<---o<---o<---o<-------o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; d…&nbsp; &nbsp;e…&nbsp; |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+----o<---o<--+&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;v master&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a…&nbsp; &nbsp;b…&nbsp; &nbsp;c…&nbsp; &nbsp;f…&nbsp; &nbsp; &nbsp; &nbsp;1…origin&nbsp; &nbsp;o<---o<---o<---o<-------o&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;^&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; d…&nbsp; &nbsp;e…&nbsp; |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;+----o<---o<--+还有另一个合并到git和hg中的选项,称为rebase,它将在最新更改之后将Bob的更改移动到。由于我不希望这个答案过于冗长,因此我将让您阅读有关git,mercurial或Bazaar的文档。作为读者的练习,请尝试确定如何与其他相关用户一起工作。与上述Bob的示例类似。存储库之间的合并比您想象的要容易,因为所有修订/提交都是唯一可识别的。还有在每个开发人员之间发送补丁的问题,这在Subversion中是一个巨大的问题,可通过唯一可识别的修订版本在git,hg和bzr中缓解。一旦有人合并了他的更改(即进行合并提交)并将其发送给团队中的其他每个人,则可以通过推送到中央存储库或发送补丁来使用,因此他们不必担心合并,因为合并已经发生了。马丁·福勒(Martin Fowler)称这种工作方式为混杂集成。因为该结构与Subversion不同,所以通过采用DAG代替了DAG,它不仅使系统而且为用户提供了更容易的分支和合并方式。

动漫人物

从历史上看,Subversion只能执行直接双向合并,因为它没有存储任何合并信息。这涉及进行一组更改并将其应用于树。即使有了合并信息,这仍然是最常用的合并策略。Git默认情况下使用三向合并算法,该算法涉及为要合并的磁头找到一个共同祖先,并利用合并双方的知识。这使得Git在避免冲突方面更加智能。Git也有一些复杂的重命名查找代码,这也很有帮助。它不存储变更集或存储任何跟踪信息-它仅存储每次提交时的文件状态,并使用启发式方法根据需要定位重命名和代码移动(磁盘存储比这更复杂,但是界面它呈现给逻辑层不暴露任何跟踪)。

12345678_0001

简而言之,合并实现在Git中比在SVN中做得更好。在1.5之前,SVN尚未记录合并操作,因此如果没有用户的帮助,将来无法进行合并,而用户需要提供SVN未记录的信息。有了1.5,它会变得更好,实际上SVN存储模型的功能比Git的DAG略强。但是SVN以相当复杂的形式存储了合并信息,这使得合并比Git花费了更多的时间-我观察到执行时间有300倍。此外,SVN声称可以跟踪重命名,以帮助合并已移动的文件。但是实际上,它仍然将它们存储为副本和单独的删除操作,并且在修改/重命名情况下,合并算法仍然会绊倒它们,也就是说,在一个分支上修改文件,然后在另一个分支上重命名,而这些分支是被合并。这样的情况仍然会产生虚假的合并冲突,在目录重命名的情况下,它甚至会导致修改的无提示丢失。(然后,SVN人员倾向于指出修改仍在历史记录中,但是当它们不在应显示的合并结果中时,这并没有太大帮助。另一方面,Git甚至不跟踪重命名,而是在事实发生之后(在合并时)将它们找出来,并且这样做非常神奇。SVN合并表示形式也存在问题;在1.5 / 1.6中,您可以按需要自动从主干合并到分支,但是需要宣布另一个方向上的合并(--reintegrate),并使分支处于不可用状态。后来,他们发现实际上并非如此,并且a)--reintegrate 可以自动找出,b)可以在两个方向上重复合并。但是经过了所有这些(恕我直言,IMHO缺乏对他们在做什么的理解),我会(好吧,我)非常谨慎地在任何非平凡的分支场景中使用SVN,并且理想情况下将尝试了解Git的想法。合并结果。答案中提到的其他要点,例如SVN中分支的强制全局可见性,与合并功能无关(但为了可用性)。同样,“ Git存储更改而SVN存储(有所不同)”大多不可行。Git在概念上将每个提交存储为单独的树(如tar文件),然后使用一些启发式方法有效地存储该提交。计算两次提交之间的更改与存储实现是分开的。事实是,Git以比SVN进行mergeinfo更直接的方式存储历史DAG。任何试图理解后者的人都会知道我的意思。简而言之:与SVN相比,Git使用了更简单的数据模型来存储修订,因此,它可以将大量精力投入到实际的合并算法中,而不是尝试应对表示形式=>实际上更好的合并。
打开App,查看更多内容
随时随地看视频慕课网APP
我要回答