3

We had two branches that existed simultaneously, like so:

A--B---C--------D--H---> (Branch A)
    \             /
     \---E--F--G-/--> (Branch B)

The issue is that we decided that we didn't want to merge Branch B into Branch A just yet, this was a mistake.

So, we (on Branch A) reverted the merge commit:

git checkout branch-a
git revert H

Resulting in:

A--B---C--------D--H--I--> (Branch A)
    \             /
     \---E--F--G-/--> (Branch B)

Which restored Branch A to its proper state. (Via the revert commit I) We then did a bit more work on both branches, and now want to merge in again:

A--B---C--------D--H--I--J--K------?> (Branch A)
    \             /               /
     \---E--F--G-/---L---M----N--/ (Branch B)

The problem is that according to Git, when we do the merge, the revert commit I is newer than the commits E, F, and G, and also newer than the merge commit H so it's not adding the new files (and changes) from Branch B.

We can do some quick reverting of the reverting (yikes), but we'd like to prevent this from happening in the future. Is there any proper solution for undoing a merge (to a public repository), while still maintaining the ability to merge that branch back in in the future?

Edit: This Git blog post: http://git-scm.com/blog/2010/03/02/undoing-merges.html suggests that we should just 'revert the revert', as we're doing. But we'd like to find a better way, if possible, to undo a merge while maintaining the ability to merge the branch back in in the future - without having to remember that we reverted a similar merge sometime in the past.

Ben Collins
  • 20,538
  • 18
  • 127
  • 187
Craig Otis
  • 31,257
  • 32
  • 136
  • 234
  • Marking this as duplicate, although it's a good question. Should'a checked that before answering :-(. There are lots of good answers on the other question. – Ben Collins Jan 15 '14 at 18:48
  • @BenCollins I did check, and found the same question, but it explicitly mentions that he never pushed the changes to the origin. In that case, a `reset` is perfect. But in my case, I was looking for an easy way to properly undo a *pushed* merge. The two possible solutions are the same, but the premise is very different. – Craig Otis Jan 15 '14 at 18:55
  • There are answers that deal with pushed changes, but you're right - the questions don't mention it, so I'll reopen. – Ben Collins Jan 15 '14 at 20:19

1 Answers1

2

The straightforward way to undo a merge

A better way to undo a merge is to use git reset. If you don't care to save any of the results of the merge, and assuming a clean working copy, you can just do the following with branch A checked out to your working copy.

git reset --hard D

If the workspace is not clean, or you want to save the results of the merge for some reason, you can do this:

git reset --mixed D # actually, you don't have to specify 'mixed', as this is the default

Both commands will leave your graph like this:

A--B---C--------D---> (Branch A)
    \             
     \---E--F--G--> (Branch B)

However, the second command will leave changes in your working copy. See the git help reset manpage for more info. Here are the key parts:

git reset [<mode>] [<commit>]
           This form resets the current branch head to <commit> and possibly updates the index (resetting it to the tree of <commit>) and
           the working tree depending on <mode>. If <mode> is omitted, defaults to "--mixed". The <mode> must be one of the following:

--mixed
               Resets the index but not the working tree (i.e., the changed files are preserved but not marked for commit) and reports what
               has not been updated. This is the default action.

--hard
               Resets the index and working tree. Any changes to tracked files in the working tree since <commit> are discarded.

Doing it this way makes future merges perfectly normal because there is no history of reversion. As far as git is concerned, the next time you do a merge will be the first time and your life will carry on.

As a side note, the original merge commit, H, will still actually remain in your repository - however: if you have gc.auto set, then some commands will cause a gc, which prunes unreachable commits from your repository, which would include H. If you for some reason want to hang on to the merge commit, you need to create another branch to hold a reference to it or turn off gc.auto and write down the first 7-8 characters of its commit id so you can find it again.

Oh crap, I did this on a public branch

Official guidance would have you avoid rewriting history on a published branch at all costs. I think the right way to think about it is to evaluate your costs. How many downstream clones do you have? Do any of them actually contribute work, or are they just mirrors? Is it really going to bother you that much to possibly have duplicate commits in your log history (only one time, hopefully)?

The nut of the question:

Is there any proper solution for undoing a merge (to a public repository), while still maintaining the ability to merge that branch back in in the future?

doesn't have a nice and tidy answer. One way is to revert the merge as suggested, leaving you with commit I:

A--B---C--------D--H--I--> (Branch A)
    \             /
     \---E--F--G-/--> (Branch B)

To make this more obvious, let's relabel the revert as H'. When you're ready to merge again, first enable rerere (rerere.enabled) and then revert the revert:

A--B---C--------D--H--H'--J--K--H''--O-?> (Branch A)
    \             /                 /
     \---E--F--G-/---L---M----N----/ (Branch B)

If there are conflicts, then work through them the best you can and let rerere record the results (see the man page for details and how to do it right).

Junio Hamano and Linus Torvalds wrote a thorough explanation of this "revert-the-revert" scenario, and since writing this answer, I think "revert-the-revert" is the right answer for your scenario.

Community
  • 1
  • 1
Ben Collins
  • 20,538
  • 18
  • 127
  • 187
  • Right, the problem with doing the reset is the merge was made in a public repository. At that point, are our options to (1) revert the revert the old-fashioned way (losing our ability to easily merge the branch back in in the future), or (2) reset and *force* the push? – Craig Otis Jan 15 '14 at 17:14
  • I know the guidance about public repositories, but I would evaluate the tradeoffs. This reset solution is clean and simple - the only complicating factor is downstream. Is it really going to be very painful if you rewrite history upstream? I think the answer for most projects - even public ones - is "not really". However, if you think it *will* be too painful, then I suggest that you could turn on `rerere` and then do a manual merge to record the results, and then future merges will use that as guidance. I'll update my answer to specifically address the issue of public repositories. – Ben Collins Jan 15 '14 at 17:18
  • Thanks Ben. We're not completely against rewriting public history, we just like to avoid it as much as possible. Any time we see stripped commits on our Bitbucket pages, little red flags go up in our heads. – Craig Otis Jan 15 '14 at 17:22