Quick tip: reverting an octopus-merge

January 16th 2015 Tim Pettersen in Git, Quick tip

Following up on yesterday's blog about octopus merges, @emmajanehw was pondering how you could 'unmerge' an octopus merge if one or more of the branches turned out to be bad:

It turned out to be a pretty interesting question, so I thought a I'd write a short article talking about what I found. However, before we look at a couple of ways you can revert an octopus merge, you should know that reverting a merge isn't always the best idea:

Reverting a merge commit declares that you will never want the tree changes brought in by the merge. As a result, later merges will only bring in tree changes introduced by commits that are not ancestors of the previously reverted merge. This may or may not be what you want.

Basically, if you later decide that you do want to merge that branch in again, the fact that the commits that were reverted are still in the history of the target branch means that only changes from commits created since the revert will be included in the merge commit.

With that in mind, let's look at the options. For these examples, I'm going to use a simple repository containing an octopus merge commit with eight parents (of course):

Octopus merge

If you want to follow along with the test repository, clone it and run git reset 383804b --hard after each of the example commands below to reset the master branch to its inital state.

Option 1: Reset your branch

If you want to get your repository back into the state that it was before the merge occured and you haven't pushed the merge commit to the server, the simplest option is to reset the branch. Since the first parent of a merge commit is the branch that you ran the merge command on, you can reset by looking up the the SHA of the merge commit (383804b in our example repository) and running:

$ git reset 383804b~1 --hard

Resetting is effectively rewriting the history of your branch, which is typically not a good idea if your merge commit has already been pushed to the server. Even if you haven't pushed, this method will remove all commits after the octopus merge was introduced, which might not be what you want either.

So next let's look at how to revert an octopus merge without rewriting history.

Option 2: Revert the merge

Git doesn't know or make any assumptions about which parent of a merge commit a particular branch used to point to. To fully revert an octopus merge, you have to specify which parent was the "mainline" commit: that is, the commit that contains the changes you want to keep around after the revert.

The first parent of the merge commit is the tip of the branch that you ran the merge command from. If that's the commit you want to revert back to (it usually is) you can simply look up the SHA of the merge commit and then revert all changes relative to it's first parent using:

$ git revert -m 1 383804b

-m refers to the position of the parent commit in the merge commit's list of parents. This is pretty awkward - I'm not sure idea why the developer didn't elect to just accept a SHA - but the position can be obtained from the list of parents output from git log.

$ git log -1
commit 383804b906f390bef358b165786cfcedb73a16a6
Merge: c81554d ed85c6d 27d6071 a9742aa fabdf39 002ec65 dbb4797 e583d84 7a64908

For example, if we wanted to keep the history of fabdf39 (the tip of the branch leg-4 from our example repository) we'd need to use -m 5 as it's the fifth parent in the list:

$ git revert -m 5 383804b

In both of these cases, we're reverting all of the changes introduced by a merge commit except the original branch. Next, let's look at what we need to do to remove the changes introduced by one particular branch in an octopus merge, leaving the changes intact.

Option 3: Reverting the changes introduced by a single branch

There's no simple command that reverts the changes introduced by a single parent of an octopus merge. However, the git-revert command does let you specify a range of commits to revert. So to revert a branch, all we need to do is find a way of expressing the commits on that branch as a range.

If we wanted to find the range of commits introduced by leg-3 in our example repository, we could simply compare it against leg-1:

$ git log --oneline leg-1..leg-3
a9742aa Leg 3 commit 3
abb3812 Leg 3 commit 2
e871ea5 Leg 3 commit 1

(If you're following along, you'll need to checkout leg-1 and leg-3 locally or prefix the branch names with origin/ for the command above to work)

We can pass the same commit range to git-revert to indicate that these are the changes we want to revert:

$ git revert -n leg-1..leg-3

Note that I'm not passing the -m flag that we used earlier, as we're manually specifying which commits to revert, rather than asking Git to figure it out for us.

We are passing the -n flag though. This makes Git apply the revert to the current index, but prevents it from actually commiting the changes. We have to manually commit the revert ourselves:

$ git commit -m "Reverting changes introduced by leg-3 at 383804b"

If you don't pass -n, Git will create a separate revert commit for every single commit that was introduced on the branch that you're reverting, which is probably a little excessive.

Thanks for reading! If you have any further questions about reseting, reverting or octoups merging feel free to hit me up on Twitter (I'm @kannonboy).