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):
If you want to follow along with the test repository, clone it and run
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 -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
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
$ 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
locally or prefix the branch names with
origin/ for the command above to
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).