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

我曾经在几个地方听说分布式版本控制系统发光的主要原因之一是比SVN等传统工具更好的合并。 这实际上是由于这两个系统如何工作的固有差异,或者像Git / Mercurial这样的具体 DVCS实现是否比SVN有更聪明的合并algorithm?

关于为什么合并在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的变化合并到主干时,我们会发出以下命令,而站在一个已经检出了trunk的文件夹中:

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

…将尝试将b1的更改合并到本地工作目录中。 然后在解决冲突并testing结果之后再提交更改。 当你提交修订树时,看起来像这样:

  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的存储库devise引起的一个问题,为了创build一个分支,你需要在存储库中创build一个新的虚拟目录 ,该目录将存放一个trunk的副本,但是它不存储关于何时和什么的信息事情就被重新合并了。这会导致有时令人讨厌的合并冲突。 更糟糕的是,Subversion默认使用双向合并,当两个分支头与其共同祖先相比时,自动合并有一些限制。

为了缓解这个问题,Subversion现在为分支和合并存储元数据。 那会解决所有问题吗?

哦,顺便说一句,颠覆仍然吸…

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

你在一个存储库中拥有的公共分支越多,就越难跟踪所有不同的分支。 所以如果这个分支还在开发中,或者它真的死了,那么在任何集中的版本控制系统中都很难分辨出来。

大多数时候,从我看到的情况来看,一个组织将会默认使用一个大分支。 这是一个耻辱,因为这反过来将很难跟踪testing和发布版本,而其他任何好事​​都来自分支。

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

有一个非常简单的原因: 分支是一stream的概念没有按devise的虚拟目录 ,分支是DVCS中的硬对象,为了简单地与存储库的同步(即, )一起工作,分支是DVCS中的硬对象。

你使用DVCS时要做的第一件事就是克隆仓库(git的clone ,hg的clone和bzr的branch )。 克隆在概念上与在版本控制中创build分支是一回事。 有些人称这种分支分支 (尽pipe后者通常也被用来指同一地点的分支机构),但它也是一样的。 每个用户运行他们自己的仓库,这意味着你有一个每用户分支正在进行。

版本结构不是一棵树 ,而是一个graphics 。 更具体地说是一个有向非循环图 (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-idvalidation的),并标记了原始分支所在的位置。

然后,爱丽丝在她的回购工作,在自己的仓库提交,并决定推动她的变化:

  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 

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

那么你如何给我看一个真正的合并的例子?

不可否认的是,上面的例子是一个非常简单的用例,所以让我们做一个更扭曲的一个,虽然更常见。 还记得origin有三个版本吗? 那么,做他们的人,叫他鲍勃 ,一直在自己的工作,并在自己的存储库作出承诺:

  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存储库。 系统如何检测到这一点是通过检查鲍勃的修改是否直接从origin降落,在这种情况下不是。 任何推动的尝试都会导致系统发出类似于“ 呃……我不敢让你那样做的鲍勃 ”的说法 。

所以Bob必须拉入,然后合并更改(使用git pull ;或者hg的pullmerge ;或者bzr的merge )。 这是一个两步过程。 首先鲍勃必须获取新的修订版本,它们将从origin库中复制它们。 现在我们可以看到graphics发散了:

  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中通过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<--+ 

还有另外一个选项可以在git和hg中合并,称为rebase ,在最新的更改之后,将把这个更改移到Bob。 由于我不想让这个答案变得更加冗长,我会让你阅读有关这个​​的git , mercurial或者bazaar文档。

作为读者的练习,试着绘制一下如何与其他用户合作。 这与Bob上面的例子类似。 合并存储库比您想象的要容易,因为所有的修订/提交都是唯一可识别的。

还有在每个开发者之间发送补丁的问题,这在Subversion中是一个巨大的问题,在git,hg和bzr中被唯一可识别的修订所减轻。 一旦有人合并了他的更改(即进行了合并提交),并将其发送给团队中的其他人使用,或者推送到中央存储库或发送补丁程序,则他们不必担心合并,因为它已经发生。 马丁·福勒把这种工作方式称为混杂的整合 。

由于结构与Subversion不同,通过使用DAG,它使得分支和合并不仅可以用于系统,也可以用于用户。

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

Git使用默认的3路合并algorithm,包括find合并头部的共同祖先,并利用合并两边存在的知识。 这让Git在避免冲突方面更加聪明。

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

简而言之, Git中的合并实现在SVN中比在SVN中更好。 在1.5 SVN没有logging合并操作之前,没有SVN没有提供信息的用户的帮助,将来无法进行合并。 随着1.5的变得更好,SVN存储模型的确有点像Git的DAG。 但是,SVN以合并信息的forms存储了一个相当复杂的forms,可以让合并的时间比Git大得多 – 我已经观察到了300个执行时间的因素。

此外,SVN声称跟踪重命名,以帮助合并移动的文件。 但实际上它仍然将它们存储为一个副本和一个单独的删除操作,合并algorithm在修改/重命名的情况下仍然在它们之间绊倒,即在一个分支上修改文件并在另一个分支上重命名,而这些分支是被合并。 这种情况仍然会产生虚假的合并冲突,而在目录重命名的情况下,甚至会导致无声的修改损失。 (SVN的人们往往指出,这些修改仍然是在历史上,但是当它们不在它们应该出现的合并结果中时,这并没有什么帮助。

另一方面,Git甚至没有对重命名进行跟踪,而是在事实之后(合并时)将其重新计算出来,而且这样做很神奇。

SVN合并表示也有问题; 在1.5 / 1.6中,你可以自动地从树干到分支合并,但是需要通知另一个方向的合并( --reintegrate ),并使分支处于不可用状态。 不久之后,他们发现事实并非如此,而且a)重新计算可以自动计算出来,并且b)可以在两个方向上重复合并。

但是,毕竟这(恕我直言表明,他们正在做什么缺乏了解),我会(很好,我)非常小心使用SVN在任何不平凡的分支场景,并将理想地尝试看看Git认为合并结果。

答案中提到的其他问题,如SVN中分支机构的强制全局可见性,与合并能力(但是可用性)无关。 另外,“Git商店变化,而SVN商店(不同的东西)”大多是关键点。 Git在概念上将每个提交作为一个单独的树(像一个tar文件)存储,然后使用相当多的启发式来有效地存储。 计算两个提交之间的更改与存储实现是分开的。 什么是真正的是,Git存储历史DAG在一个更简单的formsSVN做合并信息。 任何想了解后者的人都知道我的意思。

简而言之,Git使用比SVN更简单的数据模型来存储修订版本,因此可以将大量的精力投入到实际的合并algorithm中,而不是试图处理表示法 – >实际上更好的合并。

我读了接受的答案。 这显然是错误的。

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

假设你想做“一些聪明的事情”Git是“更好的”。 而你的东西被检入SVN。

将你的SVN转换成等效的Gitforms,在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的一些优点,请指出在SVN /集中式VC中不可用的Git / Mercurial / DVC中提供的第四部分信息。

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

有一件事在其他答案中没有提到,这确实是DVCS的一大优势,就是您可以在推送更改之前在本地提交。 在SVN中,当我想要检查一些内容时,有人已经在同一个分支上做了一个提交,这意味着我必须先做一个svn update然后才能提交。 这意味着我的更改以及来自其他人的更改现在混合在一起,并且无法中止合并(例如使用git resethg update -C ),因为没有可以返回的提交。 如果合并不是微不足道的,这意味着您在清理合并结果之前无法继续处理您的function。

但是,对于那些愚蠢的人来说,使用单独的分支(如果我没有记错的话,我们只有一个分支,在我使用SVN的公司中被用于开发)。

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

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

一个很好的阅读,如果你有时间: 为什么我select了Git

只要阅读Joel博客上的一篇文章(不幸的是他的最后一篇)。 这个是关于Mercurial的,但实际上它谈到了分布式VC系统(如Git)的优点。

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

在这里阅读文章。