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


400

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


从这里阅读出色的答案,我仍然没有得到完整的答案。转贴- stackoverflow.com/questions/6172037/...
ripper234


这取决于您的模型。在较简单的情况下,svn通常更好,因为它不会意外地调用2路合并如果您在单个开发分支上进行推/合并/拉/推操作,则git可以像git那样进行3路合并。参见:svnvsgit.com
Erik Aronesty

Answers:


556

为什么在DVCS中合并比在Subversion中更好的说法主要是基于前一段时间Subversion中分支和合并的工作方式。1.5.0之前的Subversion 没有存储有关合并分支的时间的任何信息,因此,当您要合并时,您必须指定必须合并的修订范围。

那么,为什么Subversion合并很烂

思考这个例子:

      1   2   4     6     8
trunk o-->o-->o---->o---->o
       \
        \   3     5     7
b1       +->o---->o---->o

当我们想要 b1的更改合并到中继中时,当站在已检出中继的文件夹上时,发出以下命令:

svn merge -r 2:7 {link to branch b1}

…它将尝试将更改合并b1到您的本地工作目录中。然后,在解决所有冲突并测试了结果之后,提交更改。当您提交修订树时,将如下所示:

      1   2   4     6     8   9
trunk o-->o-->o---->o---->o-->o      "the merge commit is at r9"
       \
        \   3     5     7
b1       +->o---->o---->o

但是,当版本树变大时,这种指定版本范围的方法很快就失控了,因为Subversion没有关于何时以及哪些版本合并在一起的元数据。思考以后会发生什么:

           12        14
trunk  …-->o-------->o
                                     "Okay, so when did we merge last time?"
              13        15
b1     …----->o-------->o

Subversion拥有的存储库设计在很大程度上是一个问题,为了创建分支,您需要在存储库中创建一个新的虚拟目录,该目录将存储主干副本,但不存储有关何时何地的任何信息。事情又重新融合了。有时会导致讨厌的合并冲突。更糟糕的是,Subversion默认情况下使用双向合并,当两个分支头未与其共同祖先进行比较时,它在自动合并方面存在一些严重的限制。

为了缓解这种情况,Subversion现在存储了用于分支和合并的元数据。那会解决所有问题吧?

哦,顺便说一句,Subversion仍然很烂……

在集中式系统(如Subversion)上,虚拟目录很烂。为什么?因为每个人都可以查看它们……甚至是垃圾实验的人。如果您想尝试,但不想看到每个人及其姨妈的实验,则分支是很好的。这是严重的认知噪音。您添加的分支越多,您看到的内容就越多。

您在存储库中拥有的公共分支越多,跟踪所有不同分支的难度就越大。因此,您将要问的问题是分支是否仍在开发中,或者它是否真的已经死了,这在任何集中式版本控制系统中都很难分辨。

从我所看到的大部分时间来看,组织将默认使用一个大分支。令人遗憾的是,这反过来将难以跟踪测试和发行版本,而分支带来的其他好处。

那么,为什么GCS,Mercurial和Bazaar等DVCS在分支和合并方面比Subversion更好?

原因很简单:分支是一流的概念。设计上没有虚拟目录,而分支是DVCS中的硬对象,为了与存储库同步(即pushpull)简单地工作,就必须如此。

使用DVCS时,要做的第一件事是克隆存储库(git clone,hg clone和bzr branch)。从概念上讲,克隆与在版本控制中创建分支相同。有人将其称为分叉分支(尽管后者通常也用于指代共处分支),但这是同一回事。每个用户都运行自己的存储库,这意味着每个用户都在进行分支

版本结构不是树,而是。更具体地说,是有向无环图(DAG,表示没有任何循环的图)。除了每个提交都有一个或多个父引用(该提交所基于的父引用)以外,您实际上不需要深入研究DAG的细节。因此,下图因此将反向显示修订之间的箭头。

这是一个非常简单的合并示例。想象一个名为的中央存储库origin,一个用户Alice将存储库克隆到她的计算机上。

         a…   b…   c…
origin   o<---o<---o
                   ^master
         |
         | clone
         v

         a…   b…   c…
alice    o<---o<---o
                   ^master
                   ^origin/master

克隆期间发生的情况是,每个修订版本都被完全复制到Alice(已通过唯一可识别的hash-id进行验证),并标记了原始分支的位置。

然后,Alice在自己的存储库中工作,在自己的存储库中提交并决定推送她的更改:

         a…   b…   c…
origin   o<---o<---o
                   ^ master

              "what'll happen after a push?"


         a…   b…   c…   d…   e…
alice    o<---o<---o<---o<---o
                             ^master
                   ^origin/master

解决方案非常简单,origin存储库唯一需要做的就是获取所有新修订并将其分支移至最新修订(git称为“快进”):

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

         a…   b…   c…   d…   e…
alice    o<---o<---o<---o<---o
                             ^master
                             ^origin/master

我在上面说明的用例甚至不需要合并任何东西。因此,合并算法实际上不是问题,因为所有版本控制系统之间的三向合并算法几乎相同。问题更多的是结构问题

那么,如何向我展示一个具有真实合并的示例呢?

诚然,上面的示例是一个非常简单的用例,因此尽管更常见,但让我们做的更多。还记得origin从三个修订版开始吗?好吧,做这些的人叫他Bob,他一直在自己工作,并在自己的存储库中进行了提交:

         a…   b…   c…   f…
bob      o<---o<---o<---o
                        ^ master
                   ^ origin/master

                   "can Bob push his changes?" 

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

现在,Bob无法将其更改直接推送到origin存储库。系统如何通过检查Bob的修订版本是否直接从origins 下降而检测到的,在这种情况下不是这样。任何试图推动的尝试都会导致系统说出类似“ 呃...我怕不能让你那样做Bob”

所以,鲍勃有吸合,然后合并更改(用Git的pull;或HG的pullmerge;或BZR的merge)。这是一个两步过程。首先,Bob必须获取新修订,它将从origin存储库中复制它们。现在,我们可以看到图形有所不同:

                        v master
         a…   b…   c…   f…
bob      o<---o<---o<---o
                   ^
                   |    d…   e…
                   +----o<---o
                             ^ origin/master

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

拉取过程的第二步是合并不同的提示并提交结果:

                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+
                             ^ origin/master

希望合并不会发生冲突(如果您预计会发生冲突,则可以在git中使用fetch和手动进行两个步骤merge)。以后需要做的是将这些更改再次推送到中origin,这将导致快速合并,因为合并提交是origin存储库中最新消息的直接后代:

                                 v origin/master
                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+

                                 v master
         a…   b…   c…   f…       1…
origin   o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+

还有另一个合并到git和hg中的选项,称为rebase,它将在最新更改之后将Bob的更改移动到。由于我不希望这个答案过于冗长,因此我将让您阅读有关gitmercurialBazaar的文档。

作为读者的练习,请尝试确定如何与其他相关用户一起工作。与上述Bob的示例类似。存储库之间的合并比您想象的要容易,因为所有修订/提交都是唯一可识别的。

在每个开发人员之间也存在发送补丁的问题,这在Subversion中是一个巨大的问题,可通过唯一可识别的修订版本在git,hg和bzr中缓解。一旦某人合并了他的更改(即进行合并提交)并将其发送给团队中的其他所有人,则可以通过推送到中央存储库或发送补丁来使用它,因此他们不必担心合并,因为合并已经发生了。马丁·福勒(Martin Fowler)称这种工作方式为混杂集成

因为结构与Subversion不同,所以通过改用DAG,它不仅使系统而且对用户也使分支和合并更加容易。


6
我不同意您的branch == noise参数。许多分支机构不会混淆人们,因为首席开发人员应该告诉人们哪个分支用于大型功能...因此,两个开发人员可能会在X分支上工作以添加“飞行的恐龙”,而三个开发人员可能会在X分支上工作以添加“飞龙”汽车在人民”
男孩先生

16
约翰:是的,对于少数分支机构,几乎没有噪音并且可以管理。但是在您目睹了50多个分支和标记(在颠覆性或明确的情况下)后,您可以不知道它们是否处于活动状态来返回。除了工具之外的可用性问题;为什么在存储库中放满所有垃圾?至少在p4中(由于用户的“工作区”本质上是每个用户的分支),git或hg,您可以选择不让所有人都知道您所做的更改,除非将它们推送到上游,这是安全的-当更改与他人相关时,要当心。
斯派克

24
我也没有收到“太多的实验性分支都是噪音参数,@ Spoike。我们有一个“用户”文件夹,每个用户都有自己的文件夹。在那儿他可以随意分支。在Subversion和如果您忽略其他用户的文件夹(无论如何仍要关心它们),那么您就不会看到杂音。但是对我来说,合并到SVN中不会很麻烦(而且我经常这样做,不,这并不小) 。项目),所以也许我做错了什么;)不过Git和水银的合并是优越的,你指出了这一点很好。
约翰·史密瑟斯

11
在svn中,杀死不活动的分支很容易,只需删除它们即可。人们不会删除未使用的分支,因此会造成混乱,这实际上只是内部管理的问题。您也可以轻松地在Git中建立许多临时分支。在我的工作场所中,除了标准目录外,我们还使用“临时分支”顶级目录-个人分支和实验分支进入其中,而不是使保留“官方”代码行的分支目录混乱(我们不这样做)使用功能分支)。
刘坚

10
那么,这是否意味着从v1.5到Subversion至少可以合并以及git可以合并?
山姆

29

从历史上看,Subversion只能执行直接双向合并,因为它没有存储任何合并信息。这涉及进行一组更改并将其应用于树。即使有了合并信息,这仍然是最常用的合并策略。

Git默认情况下使用3向合并算法,该算法涉及为要合并的磁头找到一个共同祖先,并利用合并双方的知识。这使得Git在避免冲突方面更加智能。

Git也有一些复杂的重命名查找代码,这也很有帮助。它存储变更集或存储任何跟踪信息-它仅存储每次提交时的文件状态,并使用启发式方法根据需要定位重命名和代码移动(磁盘存储比这要复杂得多,但是界面它呈现给逻辑层不暴露任何跟踪)。


4
您是否有一个svn具有合并冲突但git没有的冲突示例?
Gqqnbig

17

简而言之,合并实现在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)可以在两个方向上重复合并。

但是经过了所有这些(恕我直言,这表明他们对自己的工作缺乏了解),我会(好吧,我很谨慎)在任何非平凡的分支场景中使用SVN,并且理想情况下会尝试了解Git的想法合并结果。

答案中提到的其他要点,例如SVN中分支的强制全局可见性,与合并功能无关(但为了可用性)。同样,“ Git存储更改而SVN存储(有所不同)”大多不可行。Git在概念上将每个提交存储为单独的树(如tar文件),然后使用一些启发式方法有效地存储该提交。计算两次提交之间的更改与存储实现是分开的。事实是,Git以比SVN进行mergeinfo更直接的方式存储历史DAG。任何试图理解后者的人都会知道我的意思。

简而言之:与SVN相比,Git使用了更简单的数据模型来存储修订,因此,它可以将大量精力投入到实际的合并算法中,而不是试图应对表示形式=>实际上更好的合并。


11

在其他答案中没有提到的一件事,而这确实是DVCS的一大优势,那就是您可以在进行更改之前本地提交。在SVN中,当我要进行一些更改时要签入,并且同时有人已经在同一分支上进行过提交,这意味着我必须先执行“ svn update先提交”。这意味着我的更改和其他人的更改现在混合在一起,并且没有方法可以中止合并(如使用git resethg update -C),因为没有提交可以返回。如果合并非常重要,则意味着您无法在清理合并结果之前继续使用功能。

但是,那也许对那些笨拙而无法使用单独分支的人来说只是一个优势(如果我没记错的话,在我使用SVN的公司中,我们只有一个分支用于开发)。


10

编辑:这主要是解决问题的这一部分
这实际上是由于两个系统在工作方式上的固有差异,还是Git / Mercurial之类的特定DVCS实现比SVN具有更聪明的合并算法?
TL; DR-那些特定的工具具有更好的算法。分发具有一些工作流程优势,但与合并优势正交。
结束编辑

我阅读了接受的答案。这是完全错误的。

SVN合并可能会很痛苦,也很麻烦。但是,请忽略它实际上如何工作一分钟。Git没有保留或可以得出的信息,SVN也没有保留或可以得出的信息。更重要的是,没有理由为什么保留单独的(有时是部分的)版本控制系统副本会为您提供更多实际信息。这两个结构是完全等效的。

假设您想做“一些聪明的事情”,Git是“更好的”。而您正在将事情签入SVN。

将您的SVN转换为等效的Git形式,在Git中进行处理,然后(可能使用多次提交)一些额外的分支来检查结果。如果您可以想象将SVN问题转换为Git问题的自动化方法,那么Git没有根本的优势。

归根结底,任何版本控制系统都会让我

1. Generate a set of objects at a given branch/revision.
2. Provide the difference between a parent child branch/revisions.

此外,对于合并而言,了解信息也非常有用(或至关重要)

3. The set of changes have been merged into a given branch/revision.

Mercurial,Git和Subversion(现在本机使用,以前使用svnmerge.py)都可以提供全部三项信息。为了从DVC根本上展示出更好的东西,请指出Git / Mercurial / DVC中提供的第四条信息,而SVN /集中式VC中没有这些信息。

这并不是说它们不是更好的工具!


1
是的,我在细节上回答了问题,而不是标题。svn和git可以访问相同的信息(实际上svn通常具有更多信息),因此svn 可以执行git所做的任何事情。但是,他们做出了不同的设计决策,因此实际上没有。DVC /集中式的证明是,您可以将git作为集中式VC运行(可能会强加一些规则),并且可以运行svn分布式(但完全糟透了)。但是,对于大多数人来说,这太学术了-git和hg比svn更好地进行分支和合并。选择工具真的很重要:-)。
彼得

5
直到1.5版为止,Subversion 并未存储所有必要的信息。与1.5以后的SVN相比,存储的信息是不同的:Git存储合并提交的所有父级,而Subversion存储已经合并到分支中的修订。
JakubNarębski2012年

4
很难在svn存储库上重新实现的工具是git merge-base。使用git时,您可以说“在修订版x处拆分了分支a和b”。但是svn会存储“文件已从foo复制到bar”,因此您需要使用试探法来确定复制到bar正在创建新分支,而不是在项目中复制文件。诀窍是svn中的修订版由修订版号基本路径定义。即使大多数时候都可以假定是“主干”,但如果确实有分支,它就会被咬住。
道格拉斯

2
回复:“没有任何信息可以保留或可以得到,而svn也不能保留或可以得到。” -我发现SVN不记得何时进行合并。如果您想将工作从主干拉到分支中来回移动,那么合并可能会变得很困难。在Git中,其修订图中的每个节点都知道其来源。它最多可以有两个父母,并且需要进行一些本地更改。我相信Git能够比SVN合并更多。如果在SVN中合并并删除分支,则分支历史记录将丢失。如果您合并到GIT中并删除该分支,则该图将保留下来,并带有“ blame”插件。
理查德·科菲尔德 Richard Corfield)2013年

1
但是,不是git和mercurial在本地拥有所有必要的信息,而svn需要同时查看本地和中央数据以获取信息吗?
沃伦·露

8

SVN跟踪文件,而Git跟踪内容更改。跟踪从一个类/文件重构到另一个的代码块足够聪明。他们使用两种完全不同的方法来跟踪您的源。

我仍然大量使用SVN,但是对使用Git的几次感到非常满意。

如果您有时间,请读一读:为什么我选择Git


这也是我的读物,也是我指望的内容,但实际上它不起作用。
罗尔夫(Rolf)2013年

Git跟踪文件的内容,它仅将内容显示为更改
Ferrybig

6

只需阅读Joel博客上的一篇文章(可悲的是他的最后一篇)。这是关于Mercurial的内容,但实际上是关于分布式VC系统(如Git)的优点的。

使用分布式版本控制,分布式部分实际上并不是最有趣的部分。有趣的是,这些系统是根据更改而不是版本来考虑的。

这里阅读文章。


5
那是我在这里发表之前在想的文章之一。但是“从变化的角度考虑”是一个非常模糊的市场营销用语(请记住,乔尔的公司现在出售DVCS)
男孩先生2010年

2
我也觉得这很模糊。我一直认为变更集是版本(或相当是修订版)的组成部分,这让我感到惊讶的是,有些程序员没有考虑变更。
斯派克

对于真正“根据变化来思考”的系统,请查看Darcs
Max

@Max:当然,但是当推到推时,Git提供了Darcs在进行实际合并时基本上与Subversion一样痛苦的地方。
Tripleee 2012年

Git的三个缺点是:a)对于诸如文档管理这样的二进制文件来说并不是很好,因为人们不太可能希望分支和合并b)假设您想克隆所有内容c)它将所有内容的历史记录甚至存储在克隆中对于频繁更改的二进制文件导致克隆膨胀。我认为对于这些用例而言,集中式VCS更好。对于常规开发,尤其是合并和分支,Git更好。
洛克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.