Recovering from Merge Errors in Mercurial with Sub-Repositories and Named Branches

Recently, I encountered some serious issues when dealing with the Mercurial repositories on my current project. We use named branches for each release, but also maintain sub-repositories for certain in-line Grails plugins. This allows us to share common code between multiple projects without the overhead of setting up and maintaining a artifact repository. With this design, we quickly encountered scenarios where we needed to create parallel named branches in the sub-repositories that matched the parent in order to handle concurrent development on different branches. This poses some problems when dealing with merges.

Baseline Repositories

Parent Repo Child Repo
@  2[tip]:0   4bbb37b28538
|    rev 2 commit
|
| o  1   036059911cdc
|/     rev 1 commit
|
o  0   70efd68d88dd
first commit
@  2[tip]:0   5127f4aebd40
|    rev 2 commit
|
| o  1   0a9776347d4c
|/     rev 1 commit
|
o  0   27490823962a
first commit

We start with a parent and child repository. Each has 2 named branches (r1 and r2). The branches in the parent repo are linked to the named branch of the child repo (i.e. parent r1 -> child r1 and parent r2 -> child r2)

Merging Branches

After some development along both branches, we find that we need to merge our r1 changes into r2. Here’s where the problems started. The proper way to perform this merge is to follow these steps

  1. Switch to the parent repo and change to r2 (hg update r2)
  2. Switch to child repo (should be pointing to r2)
  3. Merge r1 into r2 in child repo (hg merge r1)
  4. Commit the child repo (hg commit)
  5. Switch back to the parent repo and commit the parent repo to update .hgsubstate to the new child revision (hg commit)
  6. Merge r1 into r2 in the parent repo (hg merge r2)
  7. Commit the parent repo (hg commit)

We will now have the following structure

Parent Repo Child Repo
@    4[tip]:3,1   8247fc6a3b25n
|     merging parent r1 into r2
| |
| o  3   eea8e322f460
| |    updating to latest child r2
| |
| o  2:0   4bbb37b28538
| |    rev 2 commit
| |
o |  1   036059911cdc
|/     rev 1 commit
|
o  0   70efd68d88dd
first commit
@    3[tip]:2,1   921b561bfe5a
|     merging child r1 into r2
| |
| o  2:0   5127f4aebd40
| |    rev 2 commit
| |
o |  1   0a9776347d4c
|/     rev 1 commit
|
o  0   27490823962a
first commit

This was a good merge. We can verify by looking at the .hgsubstate file for each branch:

  • r2 -> 921b561bfe5adf7a214e536b7b207edc5ceddafb child (which corresponds to the new tip of the r2 branch in child)
  • r1 -> 0a9776347d4c884d616680b69da259390d956306 child (which corresponds to the current head of the r1 branch in child)

So, we’ve verified that we’ve done what we intended: merged r1 into r2 in both repositories while maintaining the links from the parent branches to the respective child branches.

Incorrect Merging

It’s possible to have inadvertently merged in the wrong direction. I have seen this happen on a couple of occasions without the developers knowledge, but I haven’t been able to re-create it without explicitly performing the merge in the wrong direction. But for the sake of argument, lets assume a developer accidentally merged r2 into r1 in the child repository. So we have a structure like so:

Parent Repo Child Repo
o  2[tip]:0   4bbb37b28538
|    rev 2 commit
|
| @  1   036059911cdc
|/     rev 1 commit
|
o  0   70efd68d88dd
first commit
@    3[tip]:1,2   d0672a2e138a
|     merge child r2 into r1
| |
| o  2:0   5127f4aebd40
| |    rev 2 commit
| |
o |  1   0a9776347d4c
|/     rev 1 commit
|
o  0   27490823962a
first commit

In this case we’ve made a bad commit in the child repository by merging the r2 branch into the r1 branch (revision 3). The parent repository is still ok which we can verify by checking .hgsubstate

  • r2 -> 5127f4aebd40420d54da017b7564d23514677aa8 child (child r2)
  • r1 -> 0a9776347d4c884d616680b69da259390d956306 child (original child r1, although there is a subsequent commit on this branch)

In this case, we want to recover the child repository. This is a very simple example, so a number of techniques could be used including applying the correction manually or something more surgical like ‘hg strip’. Let’s tackle this problem as though these techniques aren’t available and we want to keep track of the unaltered commit history. Our plan of action would be as follows:

  1. Reverse the r2 -> r1 merge in the child repository
  2. Merge in the correct direction from r1 -> r2

Backing Out A Merge

To tackle this problem, we are going to use the ‘backout’ command of mercurial. Backout will allow you to reverse a commit, but ordinarily it won’t allow you to reverse a merge. That is unless you know the correct options for the command. First, we identify the revision we want to reverse. In this case, we want revision 3. Next, we identify the parents of the backout revision: revision 2 and revision 0 in this case. Now, we choose the parent revision which we want to backup to, in this case revision 1 (this is the revision this branch should be at).

So now we backout our bad merge. Switch to the branch that holds the bad merge, in this case r1, and perform the following: ‘hg backout –rev=3 –parent=1’

We now have the following structure for our child repository:

Child Repo
@  4[tip]   fe225b8fe4a3
|    Backed out changeset d0672a2e138a
|
o    3:1,2   d0672a2e138a
|     merge child r2 into r1
| |
| o  2:0   5127f4aebd40
| |    rev 2 commit
| |
o |  1   0a9776347d4c
|/     rev 1 commit
|
o  0   27490823962a
first commit

We check our named branch status by using ‘hg branches’
r1 4:0c4b415777b2
r2 2:5127f4aebd40 (inactive)
default 0:27490823962a (inactive)

Now, we are prepared to merge in the correct direction, but we have to be careful. We don’t if we do a merge preview for r1 -> r2, we see the following:
1 0a9776347d4c 2012-01-04 18:46 -0600 jengelman
rev 1 commit

3:1,2 d0672a2e138a 2012-01-04 22:22 -0600 jengelman
merge child r2 into r1

4[tip] fe225b8fe4a3 2012-01-04 22:24 -0600 jengelman
Backed out changeset d0672a2e138a

Notice the revision 4 is being included. However, this commit was specifically mean to undo the bad merge in the r1 branch. By including it, we would be undoing commits in the r2 branch that we don’t want to undo. So, our process will be 3 steps:

  1. Merge from r1 up to (but not including) the backout revision and commit
  2. Merge the backout revision and drop changes
  3. Merge any subsequent commits on r1

First, merge up to our backout merge: ‘hg merge -r 3’

Observer the repository structure:

Child Repo
@    5[tip]:2,3   f29414826710
|     merging child r1 into r2 up to backout merge.
| |
| | o  4   fe225b8fe4a3
| |/     Backed out changeset d0672a2e138a
| |
| o  3:1,2   d0672a2e138a
|/|    merge child r2 into r1
| |
o |  2:0   5127f4aebd40
| |    rev 2 commit
| |
| o  1   0a9776347d4c
|/     rev 1 commit
|
o  0   27490823962a
first commit

And our current branch state:
r2 5:f29414826710
r1 4:fe225b8fe4a3
default 0:27490823962a (inactive)
We can’t leave it here, because any subsequent r1 -> r2 merge will attempt to bring in revision 4, so we need to merge that revision but NOT include any of its changes. So we do a merge, but DO NOT commit: ‘hg merge r1’.

Now we revert the changes from the merge, but don’t record the revert in Mercurial: ‘hg revert-a –no-backup -r 5’ (where revision 5 is the revision that we know is good). And now we can commit and observe our repository structure:

Child Repo
@    6[tip]:5,4   76cdc32f7728
|     merging backout commit in r1 into r2.
| |
| o    5:2,3   f29414826710
| |     merging child r1 into r2 up to backout merge.
| | |
o—+  4   fe225b8fe4a3
  | |    Backed out changeset d0672a2e138a
 / /
| o  3:1,2   d0672a2e138a
|/|    merge child r2 into r1
| |
o |  2:0   5127f4aebd40
| |    rev 2 commit
| |
| o  1   0a9776347d4c
|/     rev 1 commit
|
o  0   27490823962a
first commit

And our current branch state:
r2 6:76cdc32f7728
r1 4:fe225b8fe4a3 (inactive)
default 0:27490823962a (inactive)

And finally, our repo is back into a good state to continue development with our 2 named branches and everything merged across correctly, so that other developers don’t run into any unexpected merge issues.

One thought on “Recovering from Merge Errors in Mercurial with Sub-Repositories and Named Branches

  1. Paul Atrides says:

    Hi,

    for non native english speaker is hard to follow your diagrams, please, can you put a some pictures?

    Best regards

  2. Paul Atrides says:

    The backout command is wrong, it must be:

    hg backout –rev=3 –parent=1

    HTH

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*