Merging: Hg/Git vs. SVN Merging: Hg/Git vs. SVN git git

Merging: Hg/Git vs. SVN


I too have been looking for a case where, say, Subversion fails to merge a branch and Mercurial (and Git, Bazaar, ...) does the right thing.

The SVN Book describes how renamed files are merged incorrectly. This applies to Subversion 1.5, 1.6, 1.7, and 1.8! I have tried to recreate the situation below:

cd /tmprm -rf svn-repo svn-checkoutsvnadmin create svn-reposvn checkout file:///tmp/svn-repo svn-checkoutcd svn-checkoutmkdir trunk branchesecho 'Goodbye, World!' > trunk/hello.txtsvn add trunk branchessvn commit -m 'Initial import.'svn copy '^/trunk' '^/branches/rename' -m 'Create branch.'svn switch '^/trunk' .echo 'Hello, World!' > hello.txtsvn commit -m 'Update on trunk.'svn switch '^/branches/rename' .svn rename hello.txt hello.en.txtsvn commit -m 'Rename on branch.'svn switch '^/trunk' .svn merge --reintegrate '^/branches/rename'

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.17, the newest version in Debian at the time of writing):

--- Merging differences between repository URLs into '.':A    hello.en.txt   C hello.txtSummary 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/hg-repohg init /tmp/hg-repocd /tmp/hg-repoecho 'Goodbye, World!' > hello.txthg add hello.txthg commit -m 'Initial import.'echo 'Hello, World!' > hello.txthg commit -m 'Update.'hg update 0hg rename hello.txt hello.en.txthg commit -m 'Rename.'hg merge

Before the merge, the repository looks like this (from 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.

The output of the merge is:

merging hello.en.txt and hello.txt to hello.en.txt0 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.


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'.

  • Subversion might not support advanced case of criss-cross merge.

    *---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


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.