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


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

This is largely an issue by the repository design that Subversion has, in order to create a branch you need to create a new virtual directory in the repository which will house a copy of the trunk but it doesn't store any information regarding when and what things got merged back in. That will lead to nasty merge conflicts at times. What was even worse is that Subversion used two-way merging by default, which has some crippling limitations in automatic merging when two branch heads are not compared with their common ancestor.

为了缓解这种情况,Subversion现在为分支和合并存储元数据。这样就能解决所有问题了,对吧?

哦,顺便说一下,Subversion仍然很糟糕……

在像subversion这样的集中式系统上,虚拟目录很糟糕。为什么?因为每个人都可以看到它们,即使是垃圾实验。如果你想要尝试,但你不想看到每个人都在尝试,那么分支是很好的选择。这是严重的认知噪音。你添加的分支越多,你看到的垃圾就越多。

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

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

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

原因很简单:分支是一级概念。DVCS中没有设计虚拟目录,分支是硬对象,它需要这样才能简单地处理存储库的同步(即推和拉)。

使用DVCS时要做的第一件事是克隆存储库(git的克隆、hg的克隆和bzr的分支)。克隆在概念上等同于在版本控制中创建一个分支。有些人称之为分叉或分支(尽管后者通常也用于指位于同一位置的分支),但这是同一件事。每个用户都运行自己的存储库,这意味着每个用户都有分支。

版本结构不是树,而是图。更具体地说,是有向无环图(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

解决方案相当简单,原始存储库需要做的唯一一件事就是接受所有的新修订,并将其分支移动到最新的修订(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

我在上面描述的用例甚至不需要合并任何东西。所以问题不在于合并算法,因为三向合并算法在所有版本控制系统中几乎是一样的。这个问题更多的是结构问题。

你能给我举个合并的例子吗?

无可否认,上面的例子是一个非常简单的用例,所以让我们做一个更复杂的,但更常见的用例。还记得起源有三次修订吗?好吧,做这些的人,我们叫他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不能将他的更改直接推到原始存储库。系统检测的方法是检查Bob的修改是否直接从原点修改,在这种情况下不是。任何尝试推送都会导致系统显示类似于“呃…我恐怕不能让你这么做,鲍勃。”

因此Bob必须拉入,然后合并更改(与git的拉;或者hg的拉并合并;或者bzr的归并)。这是一个两步的过程。首先Bob必须获取新的修订,这将从原始存储库复制它们。我们现在可以看到图形发散:

                        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中手动执行fetch和merge这两个步骤)。之后需要做的是将这些更改再次推入到原点,这将导致一个快进合并,因为合并提交是原始存储库中最新提交的直接后代:

                                 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中通过唯一可识别的修订得到了缓解。一旦有人合并了他的更改(即进行了合并提交),并通过推送到中央存储库或发送补丁将其发送给团队中的其他人来使用,那么他们就不必担心合并,因为它已经发生了。Martin Fowler称这种工作方式为混杂集成。

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

编辑:这主要是解决这部分问题: 这实际上是由于两个系统工作方式的内在差异,还是特定的DVCS实现(如Git/Mercurial)具有比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中不可用的第四点信息。

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

One thing that hasn't been mentioned in the other answers, and that really is a big advantage of a DVCS, is that you can commit locally before you push your changes. In SVN, when I had some change I wanted to check in, and someone had already done a commit on the same branch in the meantime, this meant that I had to do an svn update before I could commit. This means that my changes, and the changes from the other person are now mixed together, and there is no way to abort the merge (like with git reset or hg update -C), because there is no commit to go back to. If the merge is non-trivial,this means that you can't continue to work on your feature before you have cleaned up the merge result.

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

Put simply, the merge implementation is done better in Git than in SVN. Before 1.5 SVN did not record a merge action, so it was incapable to do future merges without help by the user which needed to provide information that SVN did not record. With 1.5 it got better, and indeed the SVN storage model is slightly more capable that Git's DAG. But SVN stored the merge information in a rather convoluted form that lets merges take massively more time than in Git - I've observed factors of 300 in execution time.

Also, SVN claims to track renames to aid merges of moved files. But actually it still stores them as a copy and a separate delete action, and the merge algorithm still stumbles over them in modify/rename situations, that is, where a file is modified on one branch and rename on the other, and those branches are to be merged. Such situations will still produce spurious merge conflicts, and in the case of directory renames it even leads to silent loss of modifications. (The SVN people then tend to point out that the modifications are still in the history, but that doesn't help much when they aren't in a merge result where they should appear.

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

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

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

Other points made in the answers, as the forced global visibility of branches in SVN, aren't relevant to merge capabilities (but for usability). Also, the 'Git stores changes while SVN stores (something different)' are mostly off the point. Git conceptually stores each commit as a separate tree (like a tar file), and then uses quite some heuristics to store that efficiently. Computing the changes between two commits is separate from the storage implementation. What is true is that Git stores the history DAG in a much more straightforward form that SVN does its mergeinfo. Anyone trying to understand the latter will know what I mean.

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