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.
Top comments (7)
There is a nice explanation here: git-scm.com/book/en/v2/Git-Tools-A... Look for the "Undoing merges" section further down.
If you revert a merge commit and later on you want to "re-merge" that same branch, you should revert the earlier revert commit (the commit that reverted the first merge) which effectively restores that merge. And if you have new commits in the feature branch since you merged for the first time you'll have to
git merge <feature>
again.So:
You create a merge:
git merge <feature>
You revert that (creating a new commit, say,
<revmerge>
):git revert -m 1 <mergecommit>
Later, you revert the revert:
git revert <revmerge>
And merge in any changes to after the initial merge:
git merge <feature>
Ah, this is the piece of documentation I was missing on Thursday, thanks for that!
This is esoteric but easy to resolve: start your new branch where you fix the merge commit with a revert of the revert of the merge commit.
Thanks for the tip Dag!
Git can be so confusing sometimes. :(
I would move the pointer for master back to commit a:
git branch -f master 4
I'm not sure I follow exactly, but I'll play with this and see how it goes. Thanks!