合并: Hg/Git vs. SVN

我经常读到 Hg (以及 Git 和...)比 SVN 更擅长合并,但我从未见过 Hg/Git 可以在 SVN 失败(或 SVN 需要手动干预)的情况下合并某些东西的实际例子。您是否可以发布一些分支/修改/提交/...-操作的分步列表,这些列表显示在 Hg/Git 高兴地继续前进的同时 SVN 将在哪里失败?实用的,非常特殊的情况下,请..。

一些背景知识: 我们有几十个开发人员使用 SVN 开发项目,每个项目(或类似项目组)都有自己的存储库。我们知道如何应用发布和特性分支,这样我们就不会经常遇到问题(也就是说,我们曾经遇到过这样的问题,但是我们已经学会了克服 乔尔的问题,即“一个程序员给整个团队带来创伤”或者“需要六个开发人员花两周时间重新集成一个分支”)。我们有非常稳定的发布分支,只用于应用 bug 修复。我们的主干应该足够稳定,能够在一周内创建一个版本。我们还有单个开发人员或开发人员组可以处理的特性分支。是的,它们在重新整合后被删除,这样它们就不会弄乱存储库。;)

因此,我仍然试图找到 Hg/Git 相对于 SVN 的优势。我想获得一些实践经验,但没有任何更大的项目,我们可以移动到 Hg/Git,所以我只能玩小的人工项目,只包含一些虚构的文件。我正在寻找一些案例,在这些案例中你可以感受到 Hg/Git 令人印象深刻的力量,因为到目前为止,我经常读到它们,但是我自己没有找到它们。

23928 次浏览

不用说通常的优势(离线提交、 出版过程 、 ... ...) ,这里有一个我喜欢的“合并”例子:

我一直看到的主要场景是在一个分支上实际开发了与 无关的任务
(它从一个特性开始,但是它导致了另一个特性的发展。
或者它是从一个补丁开始的,但是它导致了另一个特性的开发)。

如何在主分支上仅合并两个特性中的一个?
或者,如何将这两个特性在它们自己的分支中隔离开来?

你可以尝试生成某种补丁,问题是你不再确定 函数依赖关系可能存在于:

  • 补丁中使用的提交(或 SVN 的修订)
  • 另一个不提交补丁的一部分

Git (我认为 Mercurial 也是如此)提出了一个分支的 rebase —— on 选项来 rebase (重置分支的根)部分:

来自 杰弗米的岗位

- x - x - x (v2) - x - x - x (v2.1)
\
x - x - x (v2-only) - x - x - x (wss)

你可以把 v2的补丁和一个新的 wss 特性分解为:

- x - x - x (v2) - x - x - x (v2.1)
|\
|  x - x - x (v2-only)
\
x - x - x (wss)

让你:

  • 单独测试每个分支,以检查是否所有的编译/工作都按预期进行
  • 只合并你想要的东西。

我喜欢的另一个特性(影响合并)是 < strong > 南瓜提交 (在一个分支还没有推向另一个回购)的能力,以便呈现:

  • 更干净的历史
  • 提交哪些内容更加连贯(不是用于函数1的 committee 1,而是用于函数2的 committee 2,再次用于函数1的 committee 3...)

这样可以确保合并更容易,冲突更少。

在 subversion 1.5之前(如果我没记错的话) ,subversion 有一个明显的缺点,那就是它不会记住合并历史。

让我们看看 VonC 列出的案例:

- x - x - x (v2) - x - x - x (v2.1)
|\
|  x - A - x (v2-only)
\
x - B - x (wss)

注意修订 A 和 B。假设您将“ wss”分支上的修订 A 的更改合并到修订 B 上的“ v2-only”分支上(不管出于什么原因) ,但是继续使用这两个分支。如果您试图使用 mercurial 再次合并两个分支,那么它只会在修订 A 和 B 之后合并更改。使用 subversion,您必须合并所有内容,就好像您以前没有合并过一样。

这是我个人经验中的一个例子,由于代码量的原因,从 B 合并到 A 需要几个小时: 通过 再来一次将是一个真正的痛苦,在1.5之前的 subversion 就是这种情况。

另一个可能与 颠覆再教育合并行为更相关的区别是:

想象一下你和我 一些代码,然后我们分支这些代码, 然后我们各走各的路 工作空间,制造很多很多 分别更改该代码,所以 他们分歧很大。

当我们必须合并的时候,颠覆者 试图同时看两个修订版ーー我的 修改过的代码,以及您修改过的 代码ーー并尝试猜测如何 把他们挤成一团 混乱。它通常失败,生产 一页又一页的“合并冲突” 并不是真正的冲突 Subversion 失败的地方 搞清楚我们做了什么。

相比之下,我们工作的时候 分别在 Mercurial,Mercurial 是 忙着保存一系列的变更集。 因此,当我们想要合并代码时 Mercurial 实际上有一个 更多信息: 它知道 我们每个人的改变和能力 重新应用这些变化,而不是 只要看看最终产品 试着去猜怎么说 一起。

简而言之,Mercurial 分析差异的方法优于 subversion。

我自己并没有使用 Subversion,但是从 Subversion 1.5的发行说明: 合并跟踪(基础)来看,在像 Git 或 Mercurial 这样的完全 DAG版本控制系统中,合并跟踪的工作方式有以下不同。

  • 合并主干到分支不同于合并分支到主干: 由于某种原因,合并主干到分支需要 --reintegrate选项到 svn merge

    在诸如 Git 或 Mercurial 这样的分布式版本控制系统中,主干和分支之间没有 技术上的差异: 所有分支都被创建为相同的(尽管可能存在 社交差异)。向任何一个方向进行合并的方法都是相同的。

  • 您需要为 svn logsvn blame提供新的 -g(--use-merge-history)选项,以考虑合并跟踪。

    在 Git 和 Mercurial 中,当显示历史记录(日志)和责备时,合并跟踪会被自动考虑在内。在 Git 中,你可以要求只使用 --first-parent(我想 Mercurial 也有类似的选项)来跟随第一个父节点,以便在 git log中“丢弃”合并跟踪信息。

  • 据我所知,svn:mergeinfo属性存储关于冲突的每个路径的信息(Subversion 是基于变更集的) ,而在 Git 和 Mercurial 中,只是提交可以有多个父对象的对象。

  • Subversion 中合并跟踪的“已知问题”小节表明,重复/循环/反射合并可能无法正常工作。这意味着,在以下历史记录中,第二次合并可能不会做正确的事情(‘ A’可以是主干或分支,‘ B’可以分别是分支或分支) :

    *---*---x---*---y---*---*---*---M2        <-- A
    \       \             /
    --*----M1---*---*---/           <-- B
    

    如果上面的 ASCII 艺术被破坏: 分支‘ B’在修订‘ x’时从分支‘ A’创建(分叉) ,然后在修订‘ y’时将分支‘ A’合并到分支‘ B’中,作为合并‘ M1’,最后将分支‘ B’合并到分支‘ A’中,作为合并‘ M2’。

    *---*---x---*-----M1--*---*---M2          <-- A
    \       /           /
    \-*---y---*---*---/             <-- B
    

    在上面的 ASCII 艺术被破坏的情况下: 分支‘ B’在修订‘ x’时从分支‘ A’创建(分叉) ,在‘ y’时合并到分支‘ A’,作为‘ M1’,然后再次合并到分支‘ A’,作为‘ M2’。

  • Subversion 可能不支持 交叉合并的高级情况。

    *---b-----B1--M1--*---M3
    \     \ /        /
    \     X        /
    \   / \      /
    \--B2--M2--*
    

    Git 在实践中使用“递归”合并策略很好地处理了这种情况。

  • “已知问题”中有一个警告,合并跟踪可能不适用于文件重命名,例如,当一方重命名文件(或者修改它) ,和第二方修改文件而不重命名(旧名称下)。

    Git 和 Mercurial 在实践中都能很好地处理这种情况: Git 使用 重命名检测,Mercurial 使用 重命名跟踪

高温

我也一直在寻找这样一种情况: Subversion 未能合并一个分支,而 Mercurial (以及 Git、 Bazaar...)做了正确的事情。

SVN 书籍 描述如何不正确地合并 < strong > 重命名的文件。这适用于 Subversion 1.51.61.71.8!我试图重现以下情况:

cd /tmp
rm -rf svn-repo svn-checkout
svnadmin create svn-repo
svn checkout file:///tmp/svn-repo svn-checkout
cd svn-checkout
mkdir trunk branches
echo 'Goodbye, World!' > trunk/hello.txt
svn add trunk branches
svn commit -m 'Initial import.'
svn copy '^/trunk' '^/branches/rename' -m 'Create branch.'
svn switch '^/trunk' .
echo 'Hello, World!' > hello.txt
svn commit -m 'Update on trunk.'
svn switch '^/branches/rename' .
svn rename hello.txt hello.en.txt
svn commit -m 'Rename on branch.'
svn switch '^/trunk' .
svn merge --reintegrate '^/branches/rename'

根据这本书,合并应该干净利落地完成,但与错误的数据在重命名的文件,因为对 trunk的更新被遗忘。相反,我得到了一个树冲突(这是 Subversion 1.6.17,在撰写本文时 Debian 的最新版本) :

--- Merging differences between repository URLs into '.':
A    hello.en.txt
C hello.txt
Summary of conflicts:
Tree conflicts: 1

不应该有任何冲突ーー更新应该合并到文件的新名称中。当 Subversion 失败时,Mercurial 会正确地处理这个问题:

rm -rf /tmp/hg-repo
hg init /tmp/hg-repo
cd /tmp/hg-repo
echo 'Goodbye, World!' > hello.txt
hg add hello.txt
hg commit -m 'Initial import.'
echo 'Hello, World!' > hello.txt
hg commit -m 'Update.'
hg update 0
hg rename hello.txt hello.en.txt
hg commit -m 'Rename.'
hg merge

在合并之前,存储库如下所示(来自 hg glog) :

@  changeset:   2:6502899164cc
|  tag:         tip
|  parent:      0:d08bcebadd9e
|  user:        Martin Geisler
|  date:        Thu Apr 01 12:29:19 2010 +0200
|  summary:     Rename.
|
| o  changeset:   1:9d06fa155634
|/   user:        Martin Geisler
|    date:        Thu Apr 01 12:29:18 2010 +0200
|    summary:     Update.
|
o  changeset:   0:d08bcebadd9e
user:        Martin Geisler
date:        Thu Apr 01 12:29:18 2010 +0200
summary:     Initial import.

Merge 的输出是:

merging hello.en.txt and hello.txt to hello.en.txt
0 files updated, 1 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)

换句话说: Mercurial 从修订1中获取更改,并将其合并到修订2(hello.en.txt)中的新文件名中。处理这种情况当然是必不可少的,以支持重构和重构是 没错的事情,你会希望在一个分支上做。

其他人已经涵盖了这个问题更多的理论方面,也许我可以提供一个更实际的视角。

我目前在一家公司工作,该公司在“特性分支”开发模型中使用 SVN,即:

  • 无法在主干上完成任何工作
  • 每个开发人员都可以创建自己的分支
  • 分支应该在所承担的任务期间持续存在
  • 每个任务都应该有自己的分支
  • 合并回主干需要授权(通常通过 bugzilla)
  • 在需要高级别控制的时候,合并可以由看门人完成

总的来说,还是有用的。SVN 可以用于这样的流,但它并不完美。SVN 的一些方面阻碍了人类行为的形成。这给它带来了一些负面影响。

  • 我们遇到了一些问题,人们从低于 ^/trunk的点分支。这会在整个树中合并信息记录,并最终中断合并跟踪。虚假的冲突开始出现,混乱占据了主导地位。
  • 从树干到树枝拾取变化是相对直接的。svn merge做你想做的。将更改合并回来需要(我们被告知) merge 命令上的 --reintegrate。我从未真正理解过这个开关,但它意味着这个分支不能再次合并到主干中。这意味着这是一个死的分支,你必须创建一个新的继续工作。(见注)
  • 在创建和删除分支时,通过 URL 在服务器上进行操作的整个过程真的让人感到困惑和害怕。所以他们避开了。
  • 在树枝之间切换很容易出错,让树的一部分看树枝 A,而让另一部分看树枝 B。所以人们更喜欢在一个分支中完成他们所有的工作。

通常情况下,工程师在第一天就创建了一个分支。他开始工作,然后就忘记了。过了一段时间,一个老板来了,问他是否可以把他的工作放到主干上。工程师一直担心这一天的到来,因为重新整合意味着:

  • 将他长期使用的分支合并回主干,解决所有冲突,并释放本应在单独分支中但没有的不相关代码。
  • 删除他的分支
  • 创建一个新的分支
  • 将他的工作副本切换到新的分支机构

... 因为工程师尽可能少做这些,他们不记得每一步的“魔法咒语”。错误的开关和网址发生,突然他们在一个烂摊子,他们去得到“专家”。

最终,一切都安定下来,人们学会了如何处理缺点,但每个新手都会经历同样的问题。最终的现实(与我一开始提出的观点相反)是:

  • 没有工作在主干上完成
  • 每个开发人员都有一个主要分支
  • 分支一直持续到需要释放工作为止
  • 票据错误修复程序往往有自己的分支
  • 重新合并到主干是在授权时完成的

但是..。

  • 有时工作在不应该进入主干的时候进入主干,因为它与其他东西在同一个分支中。
  • 人们避免所有的合并(即使是简单的事情) ,所以人们经常在自己的小泡泡中工作
  • 大型合并往往会发生,并导致有限数量的混乱。

谢天谢地,这个团队足够小,可以应付,但它无法扩大规模。问题是,这些都不是 CVCS 的问题,但更重要的是,因为合并不像 DVCS 那么重要,它们不那么灵巧。“合并摩擦”导致行为,这意味着“特征分支”模型开始崩溃。好的合并需要成为所有 VCS 的一个特性,而不仅仅是 DVCS。


根据 这个,现在有一个 --record-only开关,可以用来解决 --reintegrate的问题,和 很明显 v1.8选择何时自动重新整合,它不会导致分支死亡后

我们最近从 SVN 迁移到 GIT,面临着同样的不确定性。有很多轶事证据认为 GIT 更好,但是很难找到任何例子。

但是我可以告诉你,GIT 好多了在融合方面比 SVN 要好。这显然是轶事,但还有一个表格可以参考。

以下是我们发现的一些东西:

  • SVN 过去常常在看起来不应该发生树冲突的情况下抛出许多树冲突。我们从来没有得到这个底部,但它不会发生在 GIT。
  • 尽管 GIT 更好,但是明显更复杂。花一些时间进行培训。
  • 我们习惯了乌龟 SVN,我们喜欢。乌龟 GIT 没有那么好,这可能会让你感到厌烦。然而,我现在使用 GIT 命令行,我更喜欢 Tortoise SVN 或任何 GIT GUI。

在评估 GIT 时,我们运行了以下测试。当涉及到合并时,这些表明 GIT 是胜利者,但不是那么多。在实践中,这种差异要大得多,但我想我们还没有成功复制 SVN 处理糟糕的情况。

GIT vs SVN Merging Evaluation