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

Mr.*_*Boy 398 svn git version-control merge mercurial

我在一些地方听说分布式版本控制系统闪耀的主要原因之一是比SVN这样的传统工具更好地融合.这实际上是由于两个系统如何工作的固有差异,或者像Git/Mercurial 这样的特定 DVCS实现是否只有比SVN更聪明的合并算法?

Spo*_*ike 553

为什么合并在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
Run Code Online (Sandbox Code Playgroud)

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

svn merge -r 2:7 {link to branch b1}
Run Code Online (Sandbox Code Playgroud)

...将尝试将更改合并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
Run Code Online (Sandbox Code Playgroud)

然而,当版本树增长时,这种指定修订范围的方式很快就会失控,因为subversion没有关于何时和哪些修订合并在一起的任何元数据.关于以后发生的事情的思考:

           12        14
trunk  …-->o-------->o
                                     "Okay, so when did we merge last time?"
              13        15
b1     …----->o-------->o
Run Code Online (Sandbox Code Playgroud)

这主要是Subversion拥有的存储库设计的一个问题,为了创建一个分支,你需要在存储库中创建一个新的虚拟目录,它将容纳一个主干的副本,但它不存储任何关于何时和什么的信息.事情已经合并回来.这有时会导致令人讨厌的合并冲突.更糟糕的是,Subversion默认使用双向合并,当两个分支头与其共同祖先不进行比较时,自动合并存在一些严重的限制.

为了缓解这种颠覆,现在存储分支和合并的元数据.这会解决所有问题吗?

哦,顺便说一句,Subversion仍然很糟糕......

在集中式系统上,如颠覆,虚拟目录很糟糕.为什么?因为每个人都可以查看它们......甚至是垃圾实验的.如果你想进行实验,分枝是好的,但你不想看到每个人和他们的阿姨实验.这是严重的认知噪音.你添加的分支越多,你就会看到越多的垃圾.

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

大多数时候,从我看到的情况来看,组织无论如何都会默认使用一个大分支.这是一种耻辱,因为反过来很难跟踪测试和发布版本,以及其他任何好处来自分支.

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

有一个非常简单的原因:分支是一流的概念.设计中没有虚拟目录,分支是DVCS中的硬对象,它需要这样才能简单地同步存储库(即推送拉取).

使用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
Run Code Online (Sandbox Code Playgroud)

在克隆期间发生的事情是每个修订都完全按原样复制到Alice(由唯一可识别的哈希id验证),并标记原点的分支所在的位置.

爱丽丝然后处理她的回购,在她自己的存储库中提交并决定推送她的更改:

         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
Run Code Online (Sandbox Code Playgroud)

解决方案相当简单,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
Run Code Online (Sandbox Code Playgroud)

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

那么你怎么样给我看一个真正合并的例子呢?

不可否认,上面的例子是一个非常简单的用例,所以让我们做一个更加扭曲的例子,尽管是一个更常见的例子.还记得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
Run Code Online (Sandbox Code Playgroud)

现在Bob无法将他的更改直接推送到origin存储库.系统如何检测到这一点是通过检查Bob的修订是否直接来自于origins,在这种情况下不是.任何推动的尝试都会导致系统说出类似于" 呃......恐怕不能让你做那个鲍勃."

所以鲍勃必须拉入然后合并更改(使用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
Run Code Online (Sandbox Code Playgroud)

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

                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+
                             ^ origin/master
Run Code Online (Sandbox Code Playgroud)

希望合并不会碰到冲突(如果你预计他们,你可以手动做git的两个步骤,fetchmerge).后来需要做的是再次推送这些更改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<--+
Run Code Online (Sandbox Code Playgroud)

还有另一个选项可以在git和hg中合并,称为rebase,它会在最新的更改后将Bob的更改移动到其中.因为我不希望这个答案更加冗长,所以我会让你阅读git,mercurialbazaar docs.

作为读者的练习,试着弄清楚它将如何与其他用户合作.它与Bob的上述示例类似.存储库之间的合并比您想象的更容易,因为所有修订/提交都是唯一可识别的.

还有在每个开发人员之间发送补丁的问题,这是Subversion中的一个巨大问题,它通过唯一可识别的修订在git,hg和bzr中得到缓解.一旦有人合并了他的更改(即进行合并提交)并通过推送到中央存储库或发送补丁将其发送给团队中的其他人消费,那么他们就不必担心合并,因为它已经发生了.Martin Fowler称这种混杂的方式是混杂的.

因为该结构与Subversion不同,所以通过使用DAG,它使得分支和合并能够以更容易的方式完成,不仅对于系统而且对于用户也是如此.

  • 我没有得到你的"太多实验分支也是噪音参数,@ Spoike.我们有一个"用户"文件夹,每个用户都有自己的文件夹.在那里他可以按照自己的意愿分支.分支在Subversion中很便宜如果你忽略了其他用户的文件夹(你为什么还要关心它们),那么你就不会看到噪音了.但是对于我来说,合并SVN并不会很糟糕(而且我经常这样做,不,它不是一个小的因此,也许我做错了;)然而,Git和Mercurial的合并是优越的,你很好地指出了它. (24认同)
  • 约翰:是的,对于少数分支,几乎没有噪音并且可以管理.但是,在你看到50多个分支和标签左右,在颠覆或明显的情况下,大多数你无法判断他们是否活跃,请回来.除了工具之外的可用性问题; 为什么你的存储库中有所有垃圾?至少在p4中(因为用户的"工作空间"本质上是每用户分支),git或hg你可以选择不让每个人知道你所做的更改,直到你将它们推向上游,这是一个安全的保护变更与他人相关的时间. (16认同)
  • 在svn中,很容易杀死非活动分支,你只需删除它们即可.人们不删除未使用的分支因此造成混乱的事实只是家务管理的问题.你也可以轻松地在Git中找到许多临时分支.在我的工作场所,我们使用"临时分支"顶级目录以及标准目录 - 个人分支和实验分支进入那里而不是混乱分支目录,其中保留"官方"代码行(我们不使用功能分支). (11认同)
  • 这是否意味着,从v1.5颠覆可以至少合并以及git可以? (10认同)
  • 我不同意你的branches == noise参数.很多分支都不会让人混淆,因为领导开发者应该告诉人们哪个分支用于大功能...所以两个开发人员可能会在分支X上添加"飞行恐龙",3可能会在Y上工作"让你扔掉汽车人" (6认同)
  • @约翰·史密瑟斯(John Smithers):我确实承认我的某些说法可能有点发炎;但这几乎就是SVN受到批评的本质。我并不是很讨厌SVN,但是我看到了一个项目,其中所有分支都位于一个虚拟目录中,并且将分支与主干重新集成在一起,使人们再次梳理头发。因此,整个branch == noise参数对于组织良好的项目无效,但是颠覆并没有按照惯例强制实施组织良好的项目。 (2认同)
  • 我认为这个答案没有回答这个问题.Git/Mercurial与Subversion合并的最大区别在于DVCS跟踪修订图,而Subversion历史是树.但那*本来就是*所以?是否有任何理由Subversion无法将历史建模为树,同时保留集中式VCS? (2认同)
  • @ ripper234:没有简单的TL; DR对这个主题的回答没有它默认为完全没有动力的书呆子愤怒,技术讨厌或火焰诱饵. (2认同)
  • @teambob:不。我已经在第二句话中提到了这一点,即SVN并未使用完全相同的链接存储1.5.0之前的合并信息。 (2认同)
  • 如果你为SVN分支做正确的步骤,SVN实际上就像Git一样工作.当您准备将分支合并回主干时,您应该将最近的更改从主干合并到分支(拉)中,此时您将获得合并冲突.然后,当您将更改合并(推送)到主干时,它将完美地工作. (2认同)

And*_*ett 29

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

Git默认使用3向合并算法,包括找到合并头部的共同祖先,并利用合并两侧存在的知识.这使Git能够更加智能地避免冲突.

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

  • 你有没有svn有合并冲突但git没有的例子? (3认同)

And*_*rey 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)可以在两个方向上重复合并.

但毕竟这个(IMHO表示对他们正在做的事情缺乏了解),我会(好吧,我)非常谨慎地在任何非平凡的分支场景中使用SVN,理想情况下会尝试看看Git的想法合并结果.

答案中的其他要点,因为SVN中分支的强制全局可见性与合并功能无关(但是对于可用性).此外,'Git存储变化,而SVN存储(不同的东西)'大多不合时宜.Git在概念上将每个提交存储为一个单独的树(如tar文件),然后使用相当多的启发式来有效地存储它.计算两次提交之间的更改与存储实现是分开的.真实的是,Git以更直接的形式存储历史DAG,SVN执行其mergeinfo.任何试图理解后者的人都知道我的意思.

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


dan*_*ann 11

其他答案中没有提到的一件事,那就是DVCS的一大优势,就是你可以在推送更改之前在本地提交.在SVN中,当我进行一些更改时,我想要检查,并且有人在此期间已经在同一分支上完成了提交,这意味着我必须先做一次svn update才能提交.这意味着我的更改以及来自其他人的更改现在混合在一起,并且无法中止合并(例如with git resethg update -C),因为没有提交可以返回.如果合并非常重要,则意味着在清理合并结果之前无法继续处理功能.

但是,对于那些过于愚蠢而无法使用单独分支的人来说,这可能只是一个优势(如果我没记错的话,我们在使用SVN的公司中只有一个用于开发的分支).


Pet*_*ter 10

我读了接受的答案.这是完全错的.

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.
Run Code Online (Sandbox Code Playgroud)

另外,对于合并它也是有用的(或关键的)知道

3. The set of changes have been merged into a given branch/revision.
Run Code Online (Sandbox Code Playgroud)

Mercurial,Git和Subversion(现在原来使用svnmerge.py)都可以提供所有三条信息.为了通过DVC更好地展示一些东西,请指出Git/Mercurial/DVC中提供的第四条信息,这些信息在SVN /集中式VC中不可用.

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

  • 直到版本1.5 Subversion**没有**存储所有必要的信息.使用1.5之后SVN的Wven存储的信息是不同的:Git存储合并提交的所有父项,而Subversion存储已经合并到分支中的修订. (5认同)
  • 难以在svn存储库上重新实现的工具是`git merge-base`.使用git,你可以说"分支a和b分裂在修订版x".但是svn存储"文件从foo复制到bar",因此你需要使用启发式方法来确定复制到栏是创建一个新的分支而不是复制项目中的文件.诀窍是svn中的修订版本由修订号*和*基本路径定义.即使可以在大多数时间假设"主干",但如果确实存在分支则会咬人. (4认同)
  • 回复:"没有git保留或可以推导出svn不会保留或可以派生的信息." - 我发现SVN不记得什么时候合并了.如果你想把工作从行李箱拉到你的分支机构并来回走动,那么合并会变得很困难.在Git中,其修订图中的每个节点都知道它来自何处.它有两个父母和一些当地的变化.我相信Git能够合并比SVN更多.如果您合并SVN并删除分支,则分支历史记录将丢失.如果您在GIT中合并并删除分支,则图表仍然存在,并使用"blame"插件. (2认同)

use*_*uld 8

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

我仍然大量使用SVN,但我很高兴我几次使用Git.

如果你有时间,这是一个很好的阅读:为什么我选择Git


rub*_*eet 6

刚读一篇关于Joel博客的文章(遗憾的是他的最后一篇).这个是关于Mercurial的,但它实际上讨论了像Git这样的分布式VC系统的优点.

使用分布式版本控制,分布式部分实际上不是最有趣的部分.有趣的是,这些系统考虑的是变化,而不是版本.

阅读这里的文章.

  • 这是我在发布之前想到的文章之一.但"在变化方面考虑"是一个非常模糊的营销理念(请记住Joel的公司现在销售DVCS) (5认同)
  • 我认为这也很模糊......我一直认为变更集是版本(或修订版)不可或缺的一部分,令我感到惊讶的是,一些程序员并没有考虑到变化. (2认同)