I ran into a gap in my understanding of git and merge commits yesterday. Most of the time, the projects I contribute to work with a “mergeless” commit history, so we usually rebase+squash our changes into a single commit and apply those to master leaving us with a commit history that looks like:

1---2---3---4---5 master

Generally, if I need to revert commit 3, all I need to do is git revert 3 and that commit gets reverted and committed as commit 6:

1---2---3---4---5---6 master

Yesterday, I needed to revert a change in a project that works with multiple remote branches that are merged to master via a merge commit:

1---2---3---4---5a master
     \a---b---c/ feature

So, I tried git revert 5a and got a message I’d never seen before:

error: commit 5a is a merge but no -m option was given.

WAT?!? I pulled up the git revert docs and it turns out that since a merge commit has two parents (4 and c, I think – nope, 2 and c) there is no way for git to know which one should be considered “mainline.” That’s my paraphrase of the docs, here’s the actual relevant section:

Usually you cannot revert a merge because you do not know which side of the merge should be considered the mainline. This option specifies the parent number (starting from 1) of the mainline and allows revert to reverse the change relative to the specified parent.

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.

Ok, so I have to tell git which parent is the mainline, but how do I figure that out? If I look at the merge commit (git show 5a) there’s a line that says: parents 2, c. In my simple example, I think the mainline is 2, so I think I want to do git revert -m 1 (since 2 is the first of the parents listed) since commit 2 is on master, which I think is equivalent to mainline, but I’m not really sure.

I believe that this will then revert all the differences between master and feature branch at c, but I find this fairly confusing.

Also, what is that bit about “you will never want the tree changes brought in by the merge”? Does that mean these changes that I’m reverting will never be applied if I try to re-merge them later from the same branch? So, if I add a new commit to my feature branch, and then try to merge that branch back into master, will I only get the new changes and not the changes I reverted? If so, how do I get the changes I reverted back?

I haven’t had a chance to sort all this out fully or test my assumptions as after about an hour of futzing around with this I figured it would be easier to just find the problem and fix it instead of trying to revert the change that caused it.

This definitely makes me feel like a “mergeless” git history is easier to work with since it’s simpler and each commit is a discreet unit which can be easily backed out, but I know some people prefer the merge commit workflow so I’ll have to spend some more time playing with that to figure it out.