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 /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'
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.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/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
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.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.
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:
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:
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:
That ensure merges which are a lot easier, with less conflicts.
We recently migrated from SVN to GIT, and faced this same uncertainty. There was a lot of anecdotal evidence that GIT was better, but it was hard to come across any examples.
I can tell you though, that GIT is A LOT BETTER at merging than SVN. This is obviously anecdotal, but there is a table to follow.
Here are some of the things we found:
When we were evaluating GIT we ran the following tests. These show GIT as the winner when it comes to merging, but not by that much. In practice the difference is much larger, but I guess we haven't managed to replicate the situations that SVN handles badly.
Others have covered the more theoretical aspects of this. Maybe I can lend a more practical perspective.
I'm currently working for a company that uses SVN in a "feature branch" development model. That is:
In general, it works. SVN can be used for a flow like this, but it's not perfect. There are some aspects of SVN which get in the way and shape human behaviour. That gives it some negative aspects.
^/trunk
. This litters merge information records throughout the tree, and eventually breaks the merge tracking. False conflicts start appearing, and confusion reigns.svn merge
does what you want. Merging your changes back requires (we're told) --reintegrate
on the merge command. I've never truly understood this switch, but it means that the branch can't be merged into trunk again. This means it's a dead branch and you have to create a new one to continue work. (See note)What tends to happen is that an engineer creates a branch on day 1. He starts his work and forgets about it. Some time later a boss comes along and asks if he can release his work to trunk. The engineer has been dreading this day because reintegrating means:
...and because the engineer does this as little as they can, they can't remember the "magic incantation" to do each step. Wrong switches and URLs happen, and suddenly they're in a mess and they go get the "expert".
Eventually it all settles down, and people learn how to deal with the shortcomings, but each new starter goes through the same problems. The eventual reality (as opposed to what I set out at he start) is:
...but...
Thankfully the team is small enough to cope, but it wouldn't scale. Thing is, none of this is a problem with CVCS, but more that because merges aren't as important as in DVCS they're not as slick. That "merge friction" causes behaviour which means that a "Feature Branch" model starts to break down. Good merges need to be a feature of all VCS, not just DVCS.
According to this there's now a --record-only
switch that could be used to solve the --reintegrate
problem, and apparently v1.8 chooses when to do a reintegrate automatically, and it doesn't cause the branch to be dead afterwards
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With