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


狗头军师
2025-03-18 08:48:48 (24天前)

我在一些地方听说过,分布式版本控制系统之所以如此出色的主要原因之一,是与诸如SVN之类的传统工具相比,合并起来要好得多。这实际上是由于两个系统在工作方式上的固有差异,还是Git / Mercurial之类的特定 DVCS实现仅具有比SVN更聪明的合并算法?

2 条回复
  1. 1# v-star*위위 | 2020-08-18 16-16

    为何在DVCS中合并比在Subversion中更好的说法主要是基于前一段时间Subversion中分支和合并的工作方式。1.5.0之前的Subversion 不会存储有关合并分支的时间的任何信息,因此,当您要合并时,您必须指定必须合并的修订范围。

    那么为什么Subversion合并很烂?
    思考这个例子:

    1. 1 2 4 6 8
    2. trunk o-->o-->o---->o---->o
    3. \
    4. \ 3 5 7
    5. b1 +->o---->o---->o

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

    svn merge -r 2:7 {link to branch b1}
    …它将尝试将更改合并b1到您的本地工作目录中。然后,在解决所有冲突并测试了结果之后,提交更改。当您提交修订树时,将如下所示:

    1. 1 2 4 6 8 9
    2. trunk o-->o-->o---->o---->o-->o "the merge commit is at r9"
    3. \
    4. \ 3 5 7
    5. b1 +->o---->o---->o

    但是,当版本树变大时,这种指定修订范围的方法很快就失控了,因为Subversion没有关于何时以及哪些修订合并在一起的元数据。思考以后会发生什么:

    1. 12 14
    2. trunk …-->o-------->o
    3. "Okay, so when did we merge last time?"
    4. 13 15
    5. b1 …----->o-------->o

    Subversion拥有的存储库设计在很大程度上是一个问题,为了创建分支,您需要在存储库中创建一个新的虚拟目录,该目录将存储主干副本,但不存储有关何时何地的任何信息。事情又重新融合了。有时会导致讨厌的合并冲突。更糟糕的是,Subversion默认情况下使用双向合并,当两个分支头未与其共同祖先进行比较时,它在自动合并方面存在一些严重的局限性。

    为了缓解这种情况,Subversion现在存储了用于分支和合并的元数据。那会解决所有问题吧?

    哦,顺便说一句,Subversion仍然很烂……
    在集中式系统(如Subversion)上,虚拟目录很烂。为什么?因为每个人都可以查看它们……甚至是垃圾实验的人。如果您想尝试但不想看到每个人及其姨妈的实验,则分支是很好的。这是严重的认知噪音。添加的分支越多,您看到的内容就越多。

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

    从我所看到的大部分时间来看,组织将默认使用一个大分支。令人遗憾的是,这反过来将难以跟踪测试和发行版本,而分支带来的其他好处。

    那么,为什么GCS,Mercurial和Bazaar等DVCS在分支和合并方面比Subversion更好?
    原因很简单:分支是一流的概念。设计上没有虚拟目录,而分支是DVCS中的硬对象,为了与存储库同步(即push和pull)简单地工作,就必须如此。

    使用DVCS时,要做的第一件事是克隆存储库(git clone,hg clone和bzr branch)。从概念上讲,克隆与在版本控制中创建分支相同。有人将其称为分叉或分支(尽管后者通常也用于指代共处分支),但这是同一回事。每个用户都运行自己的存储库,这意味着每个用户都在进行分支。

    版本结构不是树,而是图。更具体地说,是有向无环图(DAG,表示没有任何循环的图)。除了每个提交都有一个或多个父引用(该提交所基于的父引用)以外,您实际上不需要深入研究DAG的细节。因此,下图因此将反向显示修订之间的箭头。

    这是一个非常简单的合并示例。想象一个名为的中央存储库origin,一个用户Alice将存储库克隆到她的计算机上。

    1. a b c
    2. origin o<---o<---o
    3. ^master
    4. |
    5. | clone
    6. v
    7. a b c
    8. alice o<---o<---o
    9. ^master
    10. ^origin/master

    克隆期间发生的情况是,每个修订版本都被完全复制到了Alice(已通过唯一可识别的hash-id进行验证),并标记了原始分支的位置。

    然后,Alice在她的仓库中工作,在自己的存储库中提交并决定推送她的更改:

    1. a b c
    2. origin o<---o<---o
    3. ^ master
    4. "what'll happen after a push?"
    5. a b c d e
    6. alice o<---o<---o<---o<---o
    7. ^master
    8. ^origin/master

    解决方案非常简单,origin存储库唯一需要做的就是接受所有新修订并将其分支移至最新修订(git称为“快进”):

    1. a b c d e
    2. origin o<---o<---o<---o<---o
    3. ^ master
    4. a b c d e
    5. alice o<---o<---o<---o<---o
    6. ^master
    7. ^origin/master

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

    那么,如何向我展示一个具有真实合并的示例呢?
    诚然,上面的示例是一个非常简单的用例,因此尽管更常见,但让我们做的更多。还记得origin从三个修订版开始吗?好吧,做这些的人叫他Bob,他一直在自己工作,并在自己的存储库中进行了提交:

    1. a b c f
    2. bob o<---o<---o<---o
    3. ^ master
    4. ^ origin/master
    5. "can Bob push his changes?"
    6. a b c d e
    7. origin o<---o<---o<---o<---o
    8. ^ master

    现在,Bob无法将其更改直接推送到origin存储库。系统如何通过检查Bob的修订版本是否直接从origins 下降而检测到的,在这种情况下不是这样。任何试图推动的尝试都会导致系统说出类似“ 呃…我怕不能让你那样做Bob”。

    所以,鲍勃有吸合,然后合并更改(用Git的pull;或HG的pull和merge;或BZR的merge)。这是一个两步过程。首先,Bob必须获取新修订,它将从origin存储库中复制它们。现在,我们可以看到图形有所不同:

    1. v master
    2. a b c f
    3. bob o<---o<---o<---o
    4. ^
    5. | d e
    6. +----o<---o
    7. ^ origin/master
    8. a b c d e
    9. origin o<---o<---o<---o<---o
    10. ^ master

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

    1. v master
    2. a b c f 1
    3. bob o<---o<---o<---o<-------o
    4. ^ |
    5. | d e |
    6. +----o<---o<--+
    7. ^ origin/master

    希望合并不会发生冲突(如果您预计会发生冲突,则可以在git中使用fetch和手动进行两个步骤merge)。以后需要做的是将这些更改再次推送到中origin,这将导致快速合并,因为合并提交是origin存储库中最新消息的直接后代:

    1. v origin/master
    2. v master
    3. a b c f 1
    4. bob o<---o<---o<---o<-------o
    5. ^ |
    6. | d e |
    7. +----o<---o<--+
    8. v master
    9. a b c f 1
    10. origin o<---o<---o<---o<-------o
    11. ^ |
    12. | d e |
    13. +----o<---o<--+

    还有另一个在git和hg中合并的选项,称为rebase,它将在最新更改之后将Bob的更改移动到。由于我不希望这个答案过于冗长,因此我将让您阅读有关git,mercurial或Bazaar的文档。

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

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

    因为结构与Subversion不同,所以通过改用DAG,它不仅使系统而且为用户提供了更容易的分支和合并方式。

登录 后才能参与评论