svn - 如何和/或为什么在 Git 中 merge 比在 SVN 中更好?

标签 svn git version-control mercurial merge

我在一些地方听说过,分布式版本控制系统大放异彩的主要原因之一是 merge 比在 SVN 等传统工具中要好得多。
这实际上是由于两个系统工作方式的固有差异,还是像 Git/Mercurial 这样的特定 DVCS 实现只是具有比 SVN 更聪明的 merge 算法?

最佳答案

为什么在 DVCS 中 merge 比在 Subversion 中更好的说法主要基于不久前在 Subversion 中分支和 merge 的工作方式。之前的颠覆 1.5.0不存储有关何时 merge 分支的任何信息,因此当您想要 merge 时,您必须指定必须 merge 的修订范围。

那么为什么 Subversion merge 很糟糕呢?

思考这个例子:

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

当我们想merge b1 对主干的更改,我们将发出以下命令,同时站在已 check out 主干的文件夹上:
svn merge -r 2:7 {link to branch b1}

... 它将尝试 merge 来自 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 没有任何关于何时以及哪些修订 merge 在一起的元数据。想想以后会发生什么:
           12        14
trunk  …-->o-------->o
                                     "Okay, so when did we merge last time?"
              13        15
b1     …----->o-------->o

这主要是 Subversion 的存储库设计的一个问题,为了创建一个分支,您需要在存储库中创建一个新的虚拟目录,该目录将容纳主干的副本,但它不存储有关何时何地的任何信息事情又被 merge 了。这有时会导致令人讨厌的 merge 冲突。更糟糕的是,Subversion 默认使用双向 merge ,当两个分支头不与它们的共同祖先进行比较时,这在自动 merge 方面有一些严重的限制。

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

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

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

存储库中的公共(public)分支越多,跟踪所有不同分支的难度就越大。所以你会遇到的问题是分支是否仍在开发中,或者它是否真的死了,这在任何集中的版本控制系统中都很难说清楚。

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

那么为什么 DVCS(例如 Git、Mercurial 和 Bazaar)在分支和 merge 方面优于 Subversion?

原因很简单:分支是一流的概念 .设计上没有虚拟目录,分支是 DVCS 中的硬对象,它需要这样才能简单地与存储库同步(即推和 pull )一起工作。

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

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

一个非常简单的 merge 示例是这样的;想象一个名为 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

我在上面说明的用例 甚至不需要 merge 任何东西 .所以问题真的不在于 merge 算法,因为三路 merge 算法在所有版本控制系统之间几乎相同。 问题更多地与结构有关 .

那么你给我看一个真正 merge 的例子怎么样?

诚然,上面的例子是一个非常简单的用例,所以让我们做一个更扭曲的例子,尽管它更常见​​。请记住 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 下降来检测这一点。 's,在这种情况下没有。任何推送尝试都会导致系统说出类似于“Uh... I'm afraid can't let you do that Bob”的内容。

因此,Bob 必须引入并 merge 更改(使用 git 的 pull ;或 hg 的 pull merge ;或 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 过程的第二步是 merge 不同的提示并提交结果:
                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+
                             ^ origin/master

希望 merge 不会遇到冲突(如果您预料到它们,您可以在 git 中使用 fetch merge 手动执行这两个步骤)。稍后需要做的是将这些更改再次推送到 origin ,这将导致快进 merge ,因为 merge 提交是 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<--+

还有一个 merge git 和 hg 的选项,称为 rebase,它将把 Bob 的更改移到最新的更改之后。由于我不想让这个答案变得更冗长,我会让你阅读 git , mercurialbazaar相反的文档。

作为读者的练习,请尝试绘制出与其他用户一起使用的方法。与上面使用 Bob 的示例类似。存储库之间的 merge 比您想象的要容易,因为所有修订/提交都是唯一可识别的。

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

由于其结构与 Subversion 不同,因此通过使用 DAG,它可以更轻松地完成分支和 merge ,不仅对系统而且对用户而言。

关于svn - 如何和/或为什么在 Git 中 merge 比在 SVN 中更好?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2471606/

相关文章:

git - 为什么git fetch origin master失败?

git - 将一个分支从一个仓库复制到另一个仓库

api - 如何通过命令行在GitHub上发布版本?

独立开发人员的 Git 和开发设置

linux - SVN——添加用户

git - 适用于 Windows 的 PhpStorm 和 Github

svn - 在 Jenkins svn 结帐策略中,是否可以每天更新并在周末模拟干净的结帐?

java - 在没有工作区的情况下使用 Java SDK 以编程方式将文件提交到 TFS

git - 版本控制建议

visual-studio - 在 Subversion 控制的 Visual Studio 工作副本中使用 '_svn' 目录而不是 '.svn' 还有其他优势吗