在Git中,合并操作相对于SVN有何优势和/或原因?

411
我在一些地方听说,分布式版本控制系统比传统工具如SVN更擅长合并,这主要原因是什么呢?是由于这两个系统在工作方式上的固有差异导致的吗?还是像Git/Mercurial这样的特定DVCS实现只是拥有比SVN更聪明的合并算法而已?

我仍然没有从这里阅读的伟大答案中得到完整的答案。重新发布 - https://dev59.com/7W025IYBdhLWcg3wLilX - ripper234
参见:https://dev59.com/EnE95IYBdhLWcg3wDpqg - Jakub Narębski
这取决于你的模型。在简单情况下,SVN通常更好,因为它不会像Git一样在单个开发分支上推送/合并/拉取/推送时意外调用2路合并3路合并。请参见:https://svnvsgit.com/ - Erik Aronesty
7个回答

569

在分布式版本控制系统中,合并代码比在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的更改合并到主干时,我们需要在检出主干的文件夹中执行以下命令:

merge

请注意保留HTML标签。

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的仓库设计问题,想要创建一个分支,你需要在仓库中创建一个新的虚拟目录,它将包含trunk的副本,但不会存储任何关于合并信息的内容。这有时会导致恶意的合并冲突。更糟糕的是,Subversion默认使用双向合并,当两个分支头与它们的共同祖先不相同时,自动合并会有一些严重限制。
为了缓解这个问题,Subversion现在会存储分支和合并的元数据。这能解决所有问题吗?
顺便说一下,Subversion仍然非常烂...
对于像Subversion这样的集中式系统来说,虚拟目录很烂。为什么?因为每个人都可以访问它们...甚至是那些垃圾实验性的目录。如果你想要做实验,分支是很好的选择,但你不想看到每个人和他们家里的人都在进行实验,这是严重的认知干扰。你添加的分支越多,你看到的无用信息就会越多。
在一个仓库中有越多公共分支,就越难跟踪所有不同的分支。所以你需要问的问题是这个分支是否仍在开发中,还是已经死亡了,在任何集中式版本控制系统中都很难判断。
大多数情况下,据我所见,组织通常会默认使用一个大分支,这很遗憾,因为反过来,这将很难跟踪测试和发布版本,以及从分支中获得的其他好处。
那么为什么DVCS(例如Git、Mercurial和Bazaar)在分支和合并方面比Subversion更好呢?
原因非常简单:分支是一种一等概念。设计上没有虚拟目录,DVCS中的分支是硬对象,需要这样才能简单地与仓库同步工作(即推送和拉取)。

当使用分布式版本控制系统时,第一件要做的事情就是克隆存储库(git的clone,hg的clone和bzr的branch)。克隆在概念上与在版本控制中创建分支相同。有些人称此为forkingbranching(尽管后者常用于指代共同位于一个位置的分支),但实际上它们都是同一件事情。每个用户都运行自己的存储库,这意味着您有一个per-user branching正在进行。

版本结构不是树形结构,而是图形结构。更具体地说,是一个有向无环图(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在她的仓库上工作,在自己的仓库中提交并决定推送她的更改。
         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的修订版本是否直接来自“origin”的修订版本,而在这种情况下并不是。任何尝试推送的行为都会导致系统显示类似于“哎呀...我恐怕不能让你这样做Bob”的消息。
所以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

拉取过程的第二步是合并分叉的代码,并将结果提交为一个 commit:
                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+
                             ^ origin/master

希望合并不会遇到冲突(如果您预见到了,可以使用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的更改移动到最新更改之后。由于我不希望这个答案变得更加冗长,所以我让你阅读gitmercurialbazaar文档来了解更多相关信息。
作为读者的练习,请尝试画出涉及另一个用户的情况下它将如何运作。与上面的Bob示例类似完成。在仓库之间进行合并比您想象的要容易,因为所有修订/提交都是唯一可识别的。

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

由于结构与Subversion不同,而是采用DAG,因此使得分支和合并不仅对系统而言更容易,对用户也更容易。


6
我不同意你所说的“分支等于噪音”的论点。许多分支并不会使人感到困惑,因为主要开发人员应该告诉团队使用哪个分支来实现重大功能...因此,两个开发人员可以在X分支上工作以添加“飞行恐龙”,而另外三个开发人员可以在Y分支上工作以“让你向人们投掷汽车”。 - Mr. Boy
16
约翰:小规模分支很少噪音,易于管理。但是,如果你见过 Subversion 或者 ClearCase 中有50多个分支和标签之后再来看看,其中大部分你无法确定是否活跃,你就会知道这是一个使用上的问题了,不考虑工具的可用性问题;为什么要在代码库中留下那么多垃圾呢?至少在 P4(因为用户的“工作区”本质上是每个用户的分支)、Git 或者 Hg 中,你可以选择在将更改推送到上游之前不让每个人都知道你所做的更改,这是一个保护机制,适用于当更改与其他人相关时。 - Spoike
24
我不理解你的“太多实验性分支是噪音”的论点,@Spoike。我们有一个“用户”文件夹,每个用户都有自己的文件夹。他可以随意分支。在Subversion中,分支是廉价的,如果你忽略其他用户的文件夹(反正你为什么要关心他们呢),那么你就看不到噪音了。但对我来说,在SVN中合并并不糟糕(我经常这样做,而且不,它不是一个小项目)。所以也许我做错了 ;) 尽管如此,Git和Mercurial的合并方式更加优秀,而你恰巧指出了这一点。 - John Smithers
11
在 svn 中,删除不活跃的分支很容易。人们不移除未使用的分支导致杂乱无章只是一个日常管理问题。在 Git 中,你也可能会轻易地遇到大量临时分支。在我的工作场所,我们除了使用标准的个人分支和实验性分支目录外,还会使用一个名为 "temp-branches" 的顶层目录来存放它们,以避免让它们混杂在保存 "official" 代码行的分支目录中(我们不使用特性分支)。 - Ken Liu
10
那么这是否意味着,从版本1.5开始,Subversion至少可以像Git一样进行合并操作? - Sam
显示剩余14条评论

30

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

Git默认使用3路合并算法,它涉及查找正在合并的头的公共祖先,并利用存在于合并的两侧的知识。这使得Git在避免冲突方面更加智能。

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


6
你有没有一个例子,可以说明svn会出现合并冲突而git不会? - Gqqnbig

17
简单来说,Git 的合并实现比 SVN 做得更好。在 1.5 之前,SVN 没有记录合并操作,因此无法在没有用户提供信息的情况下进行未来的合并。随着 1.5 的到来,它变得更好了,实际上 SVN 的存储模型略微比 Git 的 DAG 更强大。但是,SVN 将合并信息存储在一个相当复杂的形式中,使得合并需要比 Git 大大花费更多的时间 - 我观察到执行时间的因素为 300。

此外,SVN 声称跟踪重命名以帮助移动文件的合并。但实际上,它仍将其存储为复制和单独的删除操作,并且合并算法仍会在修改/重命名情况下遇到问题,即在一个分支上修改文件,在另一个分支上重命名,在这些分支被合并时。这种情况仍会产生虚假的合并冲突,并且在目录重命名的情况下,甚至会导致修改的静默丢失。(然后 SVN 的人们倾向于指出修改仍然在历史记录中,但当它们不在应该出现的合并结果中时,这并没有太大帮助。)

Git则完全不跟踪重命名,但会在合并时(merge)神奇地发现它们。SVN合并表示法也存在问题;在1.5/1.6版本中,你可以随意从主干自动合并到分支,但反向合并需要手动指定(--reintegrate选项),而且会导致分支处于无法使用的状态。最终他们发现这实际上并非如此,并且a)--reintegrate选项可以自动设置,b)在两个方向上进行多次合并是可能的。但在经历了所有这一切后(这表明他们缺乏对自己正在做什么的理解),我将非常谨慎地在任何复杂的分支场景中使用SVN,并希望能够查看Git对合并结果的看法。

在答案中提到的其他观点,例如SVN中分支的强制全局可见性,并不涉及合并能力(但对于可用性很重要)。此外,“Git存储更改而SVN存储(不同的东西)”大多数情况下都不是关键点。Git在概念上将每个提交存储为单独的树(类似于tar文件),然后使用相当多的启发式方法来有效地存储它们。计算两个提交之间的更改与存储实现是分开的。真正的是,Git以比SVN更直接的方式存储历史DAG,而SVN则存储其合并信息。任何试图理解后者的人都会知道我的意思。

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


11

编辑:这主要针对问题的此部分
这是因为两个系统本质上的差异,还是像Git/Mercurial这样的特定DVCS实现只是拥有更聪明的合并算法?
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做得更好。这才是选择工具时真正重要的事情 :-)。 - Peter
5
直到1.5版本,Subversion 没有 存储所有必要的信息。即使在1.5版本之后,Subversion存储的信息也不同:Git存储合并提交的所有父节点,而Subversion则存储哪些修订版本已经合并进分支中。 - Jakub Narębski
4
在 SVN 仓库中难以重新实现的工具是 git merge-base。使用 Git,您可以说“分支 a 和 b 在修订版 x 处分开”。但 SVN 存储“从 foo 复制文件到 bar”,因此需要使用启发式方法来确定复制到 bar 是创建新分支还是在项目内复制文件。诀窍在于 SVN 中的修订版由修订号和基本路径定义。尽管大多数情况下可能会假设“主干”,但如果实际上有分支,则会出现问题。 - Douglas
2
回复:“没有信息是Git保留或可以推导出的,而SVN不保留或无法推导出的。” - 我发现SVN不记得何时合并了东西。如果您喜欢从主干中拉取工作到您的分支并来回移动,则合并可能会变得困难。在Git中,其修订图中的每个节点都知道它来自哪里。它有最多两个父级和一些本地更改。我相信Git能够比SVN更好地进行合并。如果您在SVN中合并并删除分支,则分支历史记录将丢失。如果您在GIT中合并并删除分支,则图形仍然存在,并带有“blame”插件。 - Richard Corfield
1
Git和Mercurial是不是本地就包含了所有必要的信息,而SVN需要同时查看本地和中央数据来获取信息呢? - Warren Dew
显示剩余6条评论

11

在其他答案中没有提到的一件事,而DVCS真正的优势之一是,在推送更改之前,您可以本地提交。在SVN中,当我有一些更改要检入时,并且与此同时有人已将同一分支提交,则意味着我必须在提交之前执行svn update。这意味着我的更改和其他人的更改现在混合在一起,没有办法中止合并(例如使用git resethg update -C),因为没有提交可以回退。如果合并不太简单,这意味着在清理合并结果之前,您无法继续在您的功能上工作。

但是,也许这只是那些太傻以致于不会使用独立分支的人的优势(如果我记得正确,我们在使用SVN的公司中只有一个用于开发的分支)。


8
SVN跟踪文件,而Git跟踪内容的变化。它足够聪明,可以跟踪从一个类/文件重构的代码块。它们使用两种完全不同的方法来跟踪您的源代码。
我仍然经常使用SVN,但我非常满意我使用Git的几次经历。
如果你有时间,这是一篇不错的阅读:为什么选择Git

这也是我读到的,也是我所依赖的,但在实践中它却没有起作用。 - Rolf
Git跟踪文件的内容,它只显示内容的更改。 - Ferrybig

6
我刚刚阅读了一篇关于Mercurial的文章(遗憾的是,这是Joel发布的最后一篇博客),但它实际上谈到了分布式版本控制系统,如Git的优点。使用分布式版本控制系统时,“分布式”这个词并不是最有趣的部分。最有趣的部分是这些系统从“变更”而非“版本”的角度思考。请在 这里 阅读该文章。

5
在发布这里之前,我想过其中一篇文章就是这个。但是,“以变革为思考”是一个非常模糊、听起来像营销用语的术语(请记住,Joel的公司现在销售分布式版本控制系统)。 - Mr. Boy
2
我也觉得那很模糊...我一直认为变更集是版本(或修订)的一个重要组成部分,有些程序员不从变更的角度思考这一点让我感到惊讶。 - Spoike
如果你想要一个真正“以变化为思考方式”的系统,请看看Darcs。 - Max
@Max:当然,但是当真正需要时,Git能够发挥作用,而Darcs在实际合并方面基本上和Subversion一样痛苦。 - tripleee
Git的三个缺点是:a)对于二进制文件(如文档管理)不太好,因为人们很少需要分支和合并;b)它假设您想克隆所有内容;c)即使是经常更改的二进制文件,它也会在克隆中存储所有历史记录,导致克隆膨胀。我认为,针对这些用例,集中式版本控制系统要好得多。Git对于常规开发特别是合并和分支要好得多。 - locka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接