DEV Community

Cover image for How to revert a merge commit then merge again
JB
JB

Posted on • Originally published at dev.indooroutdoor.io

How to revert a merge commit then merge again

A few days ago, I stumbled upon a thread talking about the trade-offs of squash-merging a PR VS creating a merge commit. One of the topics of discussion was whether or not reverting merge commits was possible. Here’s the thread in question:

There seemed to be some confusion surrounding the subject which isn't surprising. Indeed, while reverting a merge commit is definitely possible, it can have unintended consequences that are a pain to deal with. Specifically, once you’ve reverted the merge, re-introducing the changes from your PR is not straightforward. Having struggled with this a few times myself, I thought I’d do a small write-up to clear things up!

By the end of this post, you should know how to revert merge commits, and more importantly, how to re-introduce your changes once you’re done with your fix. This assumes basic knowledge of git.

Let’s dive in!

What’s in a merge commit

First things first, we need to clarify a few things about merge commits, this will be important later on. When you’re merging a branch, say a feature branch into master, git does basically 3 things :

  • Determine the closest common ancestor of the two branches. This allows git to compute the diff between the two branches, and detect eventual conflicts.
  • Create a commit containing the changes from both branches. This is the merge commit
  • Add the last commits of the branches you’re merging as ancestors to the merge commit. This means every merge commit has two ancestors.

Once the merging is done, you’ll end up with something like this:

Merge Commit

So far so good. But now let’s say you missed something. Something big. The buggy-feature branch above unexpectedly introduces a critical bug in production.

Ideally, you would be able to quickly narrow down the issue to one commit and deploy a hotfix to resolve the issue. However sometimes you simply cannot pinpoint which changes broke something, and you can’t afford to let the bug sit in production much longer.

In this sort of situation, you might want to revert the merge and sort out the rest later. Let’s see how to do that!

Reverting the merge commit

First of all, you can definitely revert a merge commit. It just works a bit differently than a regular commit. Indeed, if you try to revert it using git revert 181c8d1 you’ll be greeted by the following error message:

Revert Error Message

The revert failed, and what’s this “-m option” stuff about?

Well, remember, merge commits have two ancestors. Thus, git must know which ancestor it should use as a reference to compute the diffs you want to remove. The content of the revert commit will differ depending on which ancestor you’re reverting to. And that’s what the -m option is for. Let’s see how it works.

The -m option takes the index of the ancestor you want to use as a reference. It can be either 1 or 2. Most of the time, if you’re reverting a merge commit from a PR into main, you want to revert to the previous main commit which means you’ll want -m 1.

If you want to be sure tho, just use git show on the merge commit:

Show ancestors

Notice the “Merge:” line. It indicates both ancestors. When you pass -m 1 or -m 2 to the revert command, it tells git to use the first or second ancestor as listed on this line.

Now you’ve reverted the merge, and production is working again. You can breathe. Seriously just take the time to breathe, it does wonder for your health. Not breathing is strongly discouraged by most medical practitioners.

This is not over though. Once you’ve determined what’s wrong with your branch, you’ll probably want to add a commit to fix it and re-merge it into the master. And this is where things start to get hairy.

Re-introducing the feature after the fix

Why re-merging doesn’t work

Now with production up, you had time to figure out what's wrong with your branch. You’ve added a commit to fix that, and you’re ready to re-merge the branch. If you do so, you should end up with the following git history :

Git History

This looks fine at first, but you’ll probably quickly notice something is wrong again and might even create a new bug. Why is that? Well if you inspect the content of the master branch, you’ll discover that the content of the commits prior to the fix, Good Commit #1 and Buggy Commit #1 is missing. This is probably not what you want.

To understand why the commits are missing, we need to discuss what git does when you revert a merge commit :

  • First, it looks at the difference between the merge commit and the ancestor you provide using the -m option
  • then, it inverts the diff and commits the result. This is the “revert” commit.

What it doesn’t do, however, is delete the meta-data that indicates that the feature branch has already been merged. In the example above, this means, that Buggy commit is still an ancestor of the first merge commits. Which means that when you re-merge your branch, the following things happen:

  • Git determines the closest common ancestor. Since the first merge is still in history, the closest common ancestor here is Buggy Commit.
  • Git determines the difference. In this case, it creates a diff between the Revert commit and Buggy Commit .
  • Git creates the merge commits, with two ancestors.

Good Commit #1 (and Buggy Commit #1) are part of the master branch history now. They cannot be included again.

What can you do then? Well, the git documentation isn’t really optimistic about the subject …

Reverting a merge commit declares that you will never want the tree changes brought in by the merge

… but there are solutions! Let’s look at a few of them!

How to “re-merge” your PR

Re-merging directly doesn’t do the trick, so we’ll have to be clever to force git to accept the changes from the branch. To do so they are a few alternatives

Reverting the revert

This is the most straightforward way to do it. To make your changes part of the master again, you can simply revert the revert commit. This will make it so the initial revert never happened, with both reverts canceling each other out. This might sound a bit confusing, so here’s what it looks like with our example :

Reverting The Revert

Once you’ve done that you can simply merge the fix you’ve made to your PR and be done

Be careful tho, between the instant you revert the revert and the moment you merge the fix, the bug will be re-introduced. So if you’re using some kind of automation to auto-deploy pushes made to the main branch, be sure to perform this operation, before deploying. Otherwise, you risk re-deploying the bug.

This method is described in more detail in the official git how-to. (The not-so-obvious location of this doc might explain some of the confusion around this subject)

Cherry-Picking and Rebasing

Another alternative is starting a new branch from the main, and cherry pick the commit from your previous branch, including the fix. This will change the hash of the commit, so it won’t detect them in the branch history. (This also works using rebase --on-to , but not rebase alone as it will automatically use the closes common ancestor, which again will by “Buggy Commit #1”). If all goes well you should end up with something like this.

Rebased changes

The commits from “buggy-feature” are duplicated with a different hash and can be “re-merged” into master.

Conclusion

Hope this clears things up! I think the main thing to take away here is that reverting a merge commit is very painful. Even tho they are alternatives, you might still end up with a lot of conflict resolution, especially if other people are working on the same repo. You could always ask your coworkers to stop working for a bit while you’re sorting this out, but this isn’t ideal either, especially on a big project.

What should you do then? Well, Linus explains it best:

If you find a problem that got merged
into the main tree, rather than revert the merge, try really hard to
bisect the problem down into the branch you merged, and just fix it, or
try to revert the individual commit that caused it.

Top comments (0)