合并:Hg / Git与SVN

Merging: Hg/Git vs. SVN

我经常读到hg(以及git和…)比svn更擅长合并,但是我从来没有见过实际的例子,在hg/git可以合并svn失败(或者svn需要人工干预)的地方。你能发布一些分支/modify/commit/…-操作的分步列表吗?这些操作显示了当hg/git高兴地继续运行时,SVN在哪里会失败?实用的,不是非常特殊的情况,请…

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

所以我仍在努力寻找hg/git比svn的优势。我很想获得一些实际操作的经验,但是我们还没有任何更大的项目可以转移到hg/git,所以我一直在玩小的人工项目,这些项目只包含一些虚构的文件。我在找一些能让你感受到hg/git强大力量的例子,因为到目前为止,我经常读到关于它们的文章,但是我自己找不到它们。


我也一直在寻找这样一个例子,比方说,颠覆不能合并一个分支,而mercurial(和git,bazaar,…)做了正确的事情。

SVN手册描述了重命名的文件如何被错误地合并。这适用于第1.5、1.6、1.7和1.8版!我试图重现以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的最新版本)发生的冲突):

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@  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.

合并的输出是:

1
2
3
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)。处理这种情况对于支持重构来说当然是必要的,而重构正是您希望在分支上做的事情。


我自己不使用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"可以是分支或主干):

    1
    2
    3
    *---*---x---*---y---*---*---*---M2        <-- A
             \       \             /
              --*----M1---*---*---/           <-- B

    在上述ASCII艺术被打破的情况下:分支"B"是从分支"A"在修订版"X"处创建(分叉)的,然后分支"A"在修订版"Y"处合并到分支"B"中作为合并"M1",最后分支"B"作为合并"M2"合并到分支"A"中。

    1
    2
    3
    *---*---x---*-----M1--*---*---M2          <-- A
             \       /           /
              \-*---y---*---*---/             <-- B

    在上述ASCII艺术被打破的情况下:分支"B"是从分支"A"在修订版"X"创建(分叉)的,它被合并到分支"A"在"Y"作为"M1",然后再次合并到分支"A"作为"M2"。

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

    1
    2
    3
    4
    5
    *---b-----B1--M1--*---M3
         \     \ /        /
          \     X        /
           \   / \      /
            \--B2--M2--*

    Git在实践中使用"递归"合并策略可以很好地处理这种情况。我对反复无常不太确定。

  • 在"已知问题"中,存在合并跟踪MIGH不适用于文件重命名的警告,例如,当一方重命名文件(可能修改文件)时,另一方不重命名文件(在旧名称下)。

    Git和Mercurial在实践中都处理得很好:Git使用重命名检测,Mercurial使用重命名跟踪。

高温高压


我们最近从SVN迁移到了Git,并且面临着同样的不确定性。有很多轶事证据表明吉特更好,但很难找到任何例子。

不过,我可以告诉你,Git比SVN更擅长合并。这显然是奇闻轶事,但后面还有一张桌子。

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

  • SVN过去常常在看起来不应该的情况下引发很多树冲突。我们从来没有深入到这个问题的底部,但它不会在Git中发生。
  • 更好的是,git要复杂得多。花些时间在训练上。
  • 我们习惯于乌龟,这是我们喜欢的。乌龟球没那么好,这可能会让你推迟。不过,我现在使用的是git命令行,我更喜欢Tortoise SVN或任何git GUI。

当我们评估Git时,我们运行了以下测试。当谈到合并的时候,这些都显示出吉特是赢家,但没有那么多。实际上,差异要大得多,但我想我们还没有成功地复制SVN处理得很糟糕的情况。

GIT vs SVN Merging Evaluation


其他人则涵盖了更多的理论方面。也许我可以借一个更实际的视角。

我目前在一家公司工作,该公司在"功能分支"开发模型中使用SVN。即:

  • 后备箱上不能做任何工作
  • 每个开发人员都可以创建自己的分支
  • 分支机构应在所承担任务的持续时间内
  • 每个任务都应该有自己的分支
  • 需要授权合并回主干(通常通过Bugzilla)
  • 在需要高水平控制的时候,合并可以由一个门卫完成。

一般来说,它是有效的。SVN可以用于这样的流,但它并不完美。SVN的一些方面阻碍了人类行为的发展和形成。这给了它一些负面的方面。

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

通常情况下,工程师会在第一天创建一个分支。他开始工作却忘了。过了一段时间,一个老板走了过来,问他是否可以把工作放在后备箱里。工程师一直担心这一天,因为重新融入意味着:

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

…而且因为工程师尽可能少地做这件事,他们不记得每一步都要用"魔法咒语"。错误的开关和URL会发生,突然它们陷入混乱,它们会得到"专家"。

最终一切都解决了,人们学会了如何处理缺点,但每个新的开始者都会遇到同样的问题。最终的现实(与我在他开始时提出的相反)是:

  • 后备箱上没有工作
  • 每个开发者都有一个主要分支
  • 分支机构持续到需要发布工作为止
  • 勾选的错误修复往往会得到自己的分支
  • 当授权时,合并回主干

…但是…

  • 有时工作会使它在不该干的时候变成主干,因为它和其他东西在同一个分支中。
  • 人们避免所有的合并(甚至是简单的东西),所以人们经常在自己的小泡泡里工作。
  • 大合并往往会发生,并导致有限的混乱。

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

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


在颠覆1.5之前(如果我没有弄错的话),颠覆有一个重大的不利于,因为它不记得合并的历史。

让我们看看VONC概述的案例:

1
2
3
4
5
- 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之前版本的情况。

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

Imagine that you and I are working on
some code, and we branch that code,
and we each go off into our separate
workspaces and make lots and lots of
changes to that code separately, so
they have diverged quite a bit.

When we have to merge, Subversion
tries to look at both revisions—my
modified code, and your modified
code—and it tries to guess how to
smash them together in one big unholy
mess. It usually fails, producing
pages and pages of"merge conflicts"
that aren’t really conflicts, simply
places where Subversion failed to
figure out what we did.

By contrast, while we were working
separately in Mercurial, Mercurial was
busy keeping a series of changesets.
And so, when we want to merge our code
together, Mercurial actually has a
whole lot more information: it knows
what each of us changed and can
reapply those changes, rather than
just looking at the final product and
trying to guess how to put it
together.

简而言之,Mercurial分析差异的方法是(曾经是?)优于颠覆者。