Why are Mercurial backouts in one branch affecting other branches?

亡梦爱人 提交于 2019-11-30 04:55:37
Martin Geisler

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:

  1. Mercurial finds the common ancestor for the two changesets.

  2. 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:

  • Revision 8: no backout

  • Revision 11: feature branch has been backed out

  • Revision 12: no backout

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.

Martin's answer is, as usual, on the money, but I just wanted to add my 2p.

Another way of thinking about this is that backout doesn't remove anything, it adds the reverse change.

So when you merge you're not doing:

Branch after changes <-> Branch before changes => Result with changes

you're doing:

Branch after changes <-> Branch after changes with removal of changes => Result with changes removed.

Basically the first release was done badly. It would be better to cherry-pick the features into the release, than include everything and cherry-pick features out. Graft may help you here, but I've not tried using it in anger yet to know all the pitfalls.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!