views:

2203

answers:

4

I often read that Hg (and Git and...) are better at merging than SVN but I have never seen practical examples of where Hg/Git can merge something where SVN fails (or where SVN needs manual intervention). Could you post a few step-by-step lists of branch/modify/commit/...-operations that show where SVN would fail while Hg/Git happily moves on? Practical, not highly exceptional cases please...

Some background: we have a few dozen developers working on projects using SVN, with each project (or group of similar projects) in its own repository. We know how to apply release- and feature-branches so we don't run into problems very often (i.e., we've been there, but we've learned to overcome joel's problems of "one programmer causing trauma to the whole team" or "needing six developers for two weeks to reintegrate a branch"). We have release-branches that are very stable and only used to apply bugfixes. We have trunks that should be stable enough to be able to create a release within one week. And we have feature-branches that single developers or groups of developers can work on. Yes, they are deleted after reintegration so they don't clutter up the repository. ;)

So I'm still trying to find the advantages of Hg/Git over SVN. I'd love to get some hands-on experience, but there aren't any bigger projects we could move to Hg/Git yet, so I'm stuck with playing with small artificial projects that only contain a few made up files. And I'm looking for a few cases where you can feel the impressive power of Hg/Git, since so far I have often read about them but failed to find them myself.

+12  A: 

Without speaking about the usual advantages (offline commits, publication process, ...) here is a "merge" example I like:

The main scenario I keep seeing is a branch on which ... two unrelated tasks are actually developed
(it started from one feature, but it lead to the development of this other feature.
Or it started from a patch, but it lead to the development of another feature).

How to you merge only one of the two feature on the main branch?
Or How do you isolate the two features in their own branches?

You could try to generate some kind of patches, the problem with that is you are not sure anymore of the functional dependencies which could have existed between:

  • the commits (or revision for SVN) used in your patches
  • the other commits not part of the patch

Git (and Mercurial too I suppose) propose the rebase --onto option to rebase (reset the root of the branch) part of a branch:

From Jefromi's post

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

you can untangle this situation where you have patches for the v2 as well as a new wss feature into:

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

, allowing you to:

  • test each branch in isolation to check if everything compile/work as intended
  • merge only what you want to main.

The other feature I like (which influence merges) is the ability to squash commits (in a branch not yet pushed to another repo) in order to present:

  • a cleaner history
  • commits which are more coherent (instead of commit1 for function1, commit2 for function2, commit3 again for function1...)

That ensure merges which are a lot easier, with less conflicts.

VonC
+2  A: 

Prior to subversion 1.5 (if I'm not mistaken), subversion had a significant dissadvantage in that it would not remember merge history.

Let's look at the case outlined by VonC:

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

Notice revisions A and B. Say you merged changes from revision A on the "wss" branch to the "v2-only" branch at revision B (for whatever reason), but continued using both branches. If you tried to merge the two branches again using mercurial, it would only merge changes after revisions A and B. With subversion, you'd have to merge everything, as if you didn't do a merge before.

This is an example from my own experience, where merging from B to A took several hours due to the volume of code: that would have been a real pain to go through again, which would have been the case with subversion pre-1.5.

Another, probably more relevant difference in merge behaviour from Hginit: Subversion Re-education:

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.

In short, Mercurial's way of analyzing differences is (was?) superior to subversion's.

Tomislav Nakic-Alfirevic
i have read hginit. too bad it doesn't show more practical examples of where hg is doing better than svn.. basically it tells you to "trust joel" that hg is just better. the simple examples he's shown could probably be done with svn as well.. actually that's why i've opened this question.
stmax
Based on how this is said, the naive question comes to mind: what if Mercurial's merge algorithm were put into Subversion? Would svn then be as good as hg? No, because the advantage of hg is in higher level organization, not the low level text-math of merging lines from files. That's the novel idea we svn users need to grok.
DarenW
+26  A: 

I do not use Subversion myself, but from the release notes for Subversion 1.5: Merge tracking (foundational) it looks like there are the following differences from how merge tracking work in full-DAG version control systems like Git or Mercurial.

  • Merging trunk to branch is different from merging branch to trunk: for some reason merging trunk to branch requires --reintegrate option to svn merge.

    In distributed version control systems like Git or Mercurial there is no technical difference between trunk and branch: all branches are created equal (there might be social difference, though). Merging in either direction is done the same way.

  • You need to provide new -g (--use-merge-history) option to svn log and svn blame to take merge tracking into account.

    In Git and Mercurial merge tracking is automatically taken into account when displaying history (log) and blame. In Git you can request to follow first parent only with --first-parent (I guess similar option exists also for Mercurial) to "discard" merge tracking info in git log.

  • From what I understand svn:mergeinfo property stores per-path information about conflicts (Subversion is changeset-based), while in Git and Mercurial it is simply commit objects that can have more than one parent.

  • "Known Issues" subsection for merge tracking in Subversion suggests that repeated / cyclic / reflective merge might not work properly. It means that with the following histories second merge might not do the right thing ('A' can be trunk or branch, and 'B' can be branch or trunk, respectively):

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

In the case the above ASCII-art gets broken: Branch 'B' is created (forked) from branch 'A' at revision 'x', then later branch 'A' is merged at revision 'y' into branch 'B' as merge 'M1', and finally branch 'B' is merged into branch 'A' as merge 'M2'.

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

In the case the above ASCII-art gets broken: Branch 'B' is created (forked) from branch 'A' at revision 'x', it is merged into branch 'A' at 'y' as 'M1', and later merged again into branch 'A' as 'M2'.

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

Git handles this situation just fine in practice using "recursive" merge strategy. I am not sure about Mercurial.

  • In "Known Issues" there is warning that merge tracking migh not work with file renames, e.g. when one side renames file (and perhaps modifies it), and second side modifies file without renaming (under old name).

    Both Git and Mercurial handle such case just fine in practice: Git using rename detection, Mercurial using rename tracking.

HTH

Jakub Narębski
Outstanding (as usual). +1
VonC
somehow (error in Markdown parser?) the part after `<pre>...</pre>` block is not indented as it should be...
Jakub Narębski
+1 for the many detailed examples. I don't yet understand why the example in the first ascii-art might cause problems. it looks like the standard way to treat feature branches: assume A is the trunk, B is a feature branch. you merge weekly from A to B and when you're done with the feature you merge everything from B to A and then delete B. that has always worked for me. have i misunderstood the diagram?
stmax
Note that I don't know (I have not checked) that examples given above **really give problems in Subversion**. Renames and criss-cross merge are real problem in SVN, I think.
Jakub Narębski
+26  A: 

I too have been looking for a case where, say, Subversion fails to merge a branch and Mercurial does.

The SVN Book describes how renamed files are merged incorrectly and I have tried to recreate the situation it talks about below:

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

According to the book, the merge should finish cleanly, but with wrong data in the renamed file since the update on trunk is forgotten. Instead I get a tree conflict (this is with Subversion 1.6.9, the newest version at the time of writing):

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

There shouldn't be any conflict at all -- the update should be merged into the new name of the file. While Subversion fails, Mercurial handles this correctly:

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

Before the merge, the repository looks like this:

@  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 on trunk.
|
o  changeset:   0:d08bcebadd9e
   user:        Martin Geisler 
   date:        Thu Apr 01 12:29:18 2010 +0200
   summary:     Initial import.

The output of the merge is:

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)

In other words: Mercurial took the change from revision 1 and merged it into the new file name from revision 2 (hello.en.txt). Handling this case is of course essential in order to support refactoring and refactoring is exactly the kind of thing you will want to do on a branch.

Martin Geisler
+1 tried that. fails like a charm :( thanks :)
stmax
+1 for detailed example one can tap into the keyboard and see for oneself what happens. As a Mercurial noob, I wonder if the hg version of this example is follows in an obvious way, line by line?
DarenW
+1 good example for me
yves Baumes
@DarenW: I've added the corresponding Mercurial commands, I hope it makes things clearer!
Martin Geisler
@Martin: Excellent! Now how do I give this answer a 2nd upvote? <g>
DarenW