为什么在Git中合并比在SVN中更好?

我在一些地方听说过,分布式版本控制系统发光的主要原因之一,是比传统工具(如SVN)更好的合并。 这实际上是由于两个系统工作方式的内在差异,还是像Git/Mercurial这样的具体的 DVCS实现具有比SVN更聪明的合并算法?< / p >
69165 次浏览

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

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

如果你有时间的话,为什么我选择Git是一个不错的阅读

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

默认情况下,Git使用3-way合并算法,其中包括为合并的头部找到一个共同的祖先,并利用合并两侧存在的知识。这使得Git在避免冲突方面更加智能。

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

刚读了Joel博客上的一篇文章(很遗憾是他的最后一篇)。这篇文章是关于Mercurial的,但实际上它谈论的是分布式VC系统(如Git)的优势。

对于分布式版本控制 分布式部分实际上不是 最有趣的部分。有趣的是,这些系统从变化的角度考虑问题,而不是从版本的角度

阅读文章在这里

为什么在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时,我们会在站在已签出trunk的文件夹上时发出以下命令:

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这样的集中式系统上,虚拟目录很糟糕。为什么?因为每个人都可以看到它们,即使是垃圾实验。如果你想尝试但你不会想看到每个人和他们阿姨的实验,分支是很好的。这是严重的认知噪音。你添加的分支越多,你看到的垃圾就越多。

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

据我所见,大多数情况下,组织都会默认使用一个大分支。这是一种遗憾,因为这反过来将很难跟踪测试和发布版本,以及来自分支的任何其他好处。

那么,为什么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

在克隆过程中发生的事情是,每个修订都被原样复制到Alice(这是由唯一可识别的哈希id验证的),并标记了起源分支的位置。

然后Alice开始执行她的repo,在她自己的存储库中提交,并决定推动她的更改:

         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一开始有三个修订吗?好吧,做这些事情的人,我们称他为鲍勃,他一直在自己工作,并在他自己的存储库中进行了提交:

         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的修订是否直接从origin的修订下降,在这种情况下不是。任何尝试push的结果都会在系统中显示类似于“嗯…我恐怕不能让你这么做,鲍勃”的内容。

因此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

pull过程的第二步是合并分叉的尖端,并提交结果:

                                 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中合并,名为变基,它会将Bob的更改移动到最新更改之后。因为我不希望这个答案更啰嗦,所以我会让你阅读关于这个问题的git水银集市文档。

作为对读者的练习,试着画出当其他用户参与时它将如何工作。这与上面Bob的示例类似。存储库之间的合并比您想象的要容易,因为所有的修订/提交都是唯一可识别的。

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

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

编辑:这主要是解决问题的这部分:
这实际上是由于两个系统工作方式的内在差异,还是特定的DVCS实现(如Git/Mercurial)具有比SVN更聪明的合并算法? < br > TL;DR——那些特定的工具有更好的算法。分布式有一些工作流的好处,但与合并的好处是正交的 结束编辑< / p >

我读了公认的答案。这是完全错误的。

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.

水银, Git和Subversion(现在是本地的,以前使用svnmerge.py)都可以提供所有这三条信息。为了证明DVC从根本上更好,请指出在Git/Mercurial/DVC中可用而在SVN /集中式VC中不可用的第四点信息。

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

在其他答案中没有提到的一件事,也是DVCS真正的一大优势,是您可以在推送更改之前在本地提交。在SVN中,当我想要检入一些更改时,同时有人已经在同一分支上完成了提交,这意味着我必须在提交之前执行svn update。这意味着我的更改和其他人的更改现在混合在一起,没有办法中止合并(像git resethg update -C那样),因为没有提交可以返回。如果合并不是平凡的,这意味着在清理合并结果之前,您不能继续处理您的特性。

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

简单地说,合并实现在Git中比在SVN中做得更好。在1.5版本之前,SVN没有记录一个合并动作,所以在没有用户帮助的情况下,SVN无法进行以后的合并,因为用户需要提供SVN没有记录的信息。在1.5版中,它变得更好了,实际上,SVN存储模型比Git的DAG更强大一些。但是SVN以一种相当复杂的形式存储合并信息,这使得合并花费的时间比Git多得多——我观察到执行时间是Git的300倍。

此外,SVN声称可以跟踪重命名,以帮助合并移动的文件。但实际上它仍然将它们存储为一个副本和一个单独的删除操作,合并算法在修改/重命名情况下仍然会遇到它们,也就是说,一个文件在一个分支上被修改,在另一个分支上被重命名,而这些分支将被合并。这种情况仍然会产生虚假的合并冲突,在目录重命名的情况下,甚至会导致修改的无声损失。(SVN人员随后倾向于指出修改仍然在历史记录中,但是当它们不在应该出现的合并结果中时,这并没有多大帮助。

另一方面,Git甚至不跟踪重命名,而是在事后(在合并时)计算出它们,而且这样做非常神奇。

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

但是在所有这些之后(恕我直言,这表明我对他们正在做的事情缺乏理解),我会(好吧,我是)非常谨慎地在任何重要的分支场景中使用SVN,理想情况下会尝试看看Git对合并结果的看法。

答案中的其他观点,如SVN中分支的强制全局可见性,与合并功能无关(但与可用性有关)。此外,“Git存储变化,而SVN存储(一些不同的东西)”基本上是离题了。Git在概念上将每个提交存储为一个单独的树(比如焦油文件),然后使用相当多的启发式方法来有效地存储它。计算两次提交之间的更改与存储实现是分开的。真实的情况是,Git以一种比SVN使用mergeinfo更直接的形式存储历史DAG。任何试图理解后者的人都会明白我的意思。

简而言之:Git使用比SVN简单得多的数据模型来存储修订,因此它可以将大量精力放在实际的合并算法上,而不是试图处理=>这种实际上更好的合并。