I believe the problem is that merges work differently than you think. You write
Since the new release branch contains all of the feature branch changesets (nothing has been backed out), why does the default branch not receive all of these changesets too?
When you merge two branches, it's wrong to think of it as applying all changes from one branch onto another branch. So the default
branch does not "receive" any changesets from release2
. I know this is how we normally think of merges, but it's inaccurate.
What really happens when you merge two changesets is the following:
Mercurial finds the common ancestor for the two changesets.
For each file that differ between the two changesets Mercurial runs a three-way merge algorithm using the ancestor file, the file in the first changeset and the file in the second changeset.
In your case, you are merging revision 11 and 12. The least common ancestor is revision 8. This means that Mercurial will run a three-way merge between files from there revisions:
In a three-way merge, a change always trumps no change. Mercurial sees that the files have been changed between 8 and 11 and it sees no change between 8 and 12. So it uses the changed version from revision 11 in the merge. This applies for any three-way merge algorithm. The full merge table looks like this where old
, new
, ... are the content of matching hunks in the three files:
ancestor local other -> merge
old old old old (nobody changed the hunk)
old old new new (they changed the hunk)
old new old new (you changed the hunk)
old new new new (hunk was cherry picked onto both branches)
old foo bar <!> (conflict, both changed hunk but differently)
I'm afraid that a merge changeset shouldn't be backed out at all because of this surprising merge behavior. Mercurial 2.0 and later will abort and complain if you try to backout a merge.
In general, one can say that the three-way merge algorithm assumes that all change is good. So if you merge branch1
into dev
and then later undo the merge with a backout, then the merge algorithm will think that the state is "better" than before. This means that you cannot just re-merge branch1
into dev
at a later point to get the backed-out changes back.
What you can do is to use a "dummy merge" when you merge into default
. You simply merge and always keep the changes from the release branch you're merging into default
:
$ hg update default
$ hg merge release2 --tool internal:other -y
$ hg revert --all --rev release2
$ hg commit -m "Release 2 is the new default"
That will side-step the problem and force default
be just like release2
. This assumes that absolutely no changes are made on default
without being merged into a release branch.
If you must be able to make releases with skipped features, then the "right" way is to not merge those features at all. Merging is a strong commitment: you tell Mercurial that the merge changeset now has all the good stuff from both its ancestors. As long as Mercurial wont let you pick your own base revision when merging, the three-way merge algorithm wont let you change your mind about a backout.
What you can do, however, is to backout the backout. This means that you re-introduce the changes from your feature branch onto your release branch. So you start with a graph like
release: ... o --- o --- m1 --- m2
/ /
feature-A: ... o --- o /
/
feature-B: ... o --- o --- o
You now decided that the A feature was bad and you backout the merge:
release: ... o --- o --- m1 --- m2 --- b1
/ /
feature-A: ... o --- o /
/
feature-B: ... o --- o --- o
You then merge another feature into your release branch:
release: ... o --- o --- m1 --- m2 --- b1 --- m3
/ / /
feature-A: ... o --- o / /
/ /
feature-B: ... o --- o --- o /
/
feature-C: ... o --- o --- o --- o --- o
If you now want to re-introduce the A feature, then you can backout b1
:
release: ... o --- o --- m1 --- m2 --- b1 --- m3 --- b2
/ / /
feature-A: ... o --- o / /
/ /
feature-B: ... o --- o --- o /
/
feature-C: ... o --- o --- o --- o --- o
We can add the deltas to the graph to better show what changes where and when:
+A +B -A +C --A
release: ... o --- o --- m1 --- m2 --- b1 --- m3 --- b2
After this second backout, you can merge again with feature-A
in case new changesets have been added there. The graph you're merging looks like:
release: ... o --- o --- m1 --- m2 --- b1 --- m3 --- b2
/ / /
feature-A: ... o -- a1 - a2 / /
/ /
feature-B: ... o --- o --- o /
/
feature-C: ... o --- o --- o --- o --- o
and you merge a2
and b2
. The common ancestor will be a1
. This means that the only changes you'll need to consider in the three-way merge are those between a1
and a2
and a1
and b2
. Here b2
already have the bulk of the changes "in" a2
so the merge will be small.