9

There is lots of talk about how it's not easy to "undo" a merge in git. Short version: if you undo a merge commit, it also tells git to never merge those changes back in in the future.

Is there something I can do when doing the merge in order to abate this problem? There are plenty of situations where undoing a merge would be really, really useful, just in the normal course of software dev, and more crucially, in controlling the state of a release branch, when changes need to be rolled back.

edit

I have seen the solution in this article and don't really consider it a solution, more of an explanation of the problem. It requires

  1. always use --no-ff
  2. remember all your undone-merges when you want to bring back in code that depends on them (this could be a few hours, days, week, or months in the future…)

what I want

Here is how it works in Subversion. Let's say I have a branch called "release-candidate", which is what we run on the staging server and where we try out features. Let's say I merge in feature A branch. In Subversion, it's all one changeset, and all history for all files is merged. Let's say we don't like it, so we want to take it out. We just undo that single changeset, and don't have to think about anything else. We can merge feature branch A back in at any time in the future without having to remember that we at one point merged it in and took it out.

I'd like to be able to get as close to that flow as possible. I'd like to optimize for "not having to remember stuff in the future", even if it makes things take more steps along the way somehow. (this might be impossible...)

Aziz Shaikh
  • 15,104
  • 9
  • 55
  • 73
John Bachir
  • 21,401
  • 22
  • 137
  • 203
  • Is there a reason you can't roll back the commit using git reset --hard ORIG_HEAD? – urschrei Jan 19 '11 at 22:09
  • Does my answer need more info? – Adam Dymitruk Jan 20 '11 at 00:56
  • urschrei: if I did that on a branch on a server, then to push the results I would have to use `--force`, right? – John Bachir Jan 20 '11 at 20:21
  • okay so the answer to my question is "you can't do this", but adymitruk has given a great overview of what the options are after the fact. – John Bachir Jan 20 '11 at 21:30
  • I really need to know what your question is. There is nothing you can't do in git that svn does... more or less. – Adam Dymitruk Jan 20 '11 at 21:46
  • "I'd like to optimize for 'not having to remember stuff in the future'" :-) none of your 4 options allow for this -- I always have to remember that I undid a merge. I can't just remerge at will. – John Bachir Jan 20 '11 at 22:00
  • git revert sha1_of_merge -m2 from the mainline will act like the SVN equivalent. Git checkout sha1_of_merge^1 -- . will get your working dir to the state that it was in before you merged. It'll do exactly what you need it to do. It's a sharp knife ;) – Adam Dymitruk Jan 21 '11 at 00:21
  • so after i do `git revert sha1_of_merge -m2` I no longer have to remember that I undid the merge and I can just re-merge the same changes in the future? – John Bachir Jan 22 '11 at 20:08

1 Answers1

11

UPDATE:

A workflow that makes it easier to work with branch-per-feature is here: http://dymitruk.com/blog/2012/02/05/branch-per-feature/


(the SVN part of the question has been answered at the end)

Yes, here is how you reintroduce a feature that you unmerged. Consider the following history (assumes you "undid" a merge already):

x---x----x--x---x--M--U--L
     \            /
      x--x--x--x-F

F is the feature branch, M is the merge, U is the opposite when you unmerge that feature, L is the latest commit.

Here are your choices:

  1. revert U (no --force necessary to push):

    x---x----x--x---x--M--U--L--^U
        \            /
         x--x--x--x-F
    
  2. rebase F onto L (and then merge --ff-only F') (no --force necessary to push):

    x---x----x--x---x--M--U--L
         \            /       \
          x--x--x--x-x         x'--x'--x'--x'--F'
    
  3. rebase F onto L (and then merge --no-ff F' - preserves your new branch point) (no --force necessary to push):

    x---x----x--x---x--M--U--L-------------------M2
         \            /       \                 /
          x--x--x--x-x         x'--x'--x'--x'--F'
    
  4. rebase -i head^^ and eliminate U from the list (--force is necessary to push):

    x---x----x--x---x--M--L
         \            /
          x--x--x--x-F
    
  5. rebase --onto M^1 L^ L to get rid of the merge and unmerge. Now you can remerge F later.

                      L'
                     /
    x---x----x--x---x--M--U--L
         \            /
          x--x--x--x-F
    

To squash all the feature commits, use the --squash modifier on the initial merge. I'll let your imagination do the work on how that would look in history. There is a reason I don't recommend doing this. There is value in knowing how you got a feature working and what steps it took. Subsequent merges will be easier since Git can examine the history of why a certain file looks like it does. Squashing the commits together loses that information.

There are additional drawbacks that may or may not affect your ability to take advantage of rerere history.

What I recommend is always marking what is released with a blank merge in master. This is done via a merge with the --no-ff option. You never work on master and the only commits that are done there are those merges - no code change commits. In the QA branch, you tag the commit that marks the point at which you released. So when you do git merge --no-ff rc-12.2, you will autogenerate a commit comment "merged rc-12.2".

Check out git-flow.

Hope that provides you more detail.

Adam Dymitruk
  • 109,813
  • 21
  • 138
  • 137
  • 1
    That's a great overview of my options, but my question was if there is something I can do at the time of the original merge in order to make things more simple going forward. I just updated my question with the "what I want" section. – John Bachir Jan 20 '11 at 20:29
  • 1
    In your answer: those are 4 distinct options, right? Wanna number them? Also, which of them will require `--force` if all the branches have been published remotely? Would be nice if you mark those as such.. – John Bachir Jan 20 '11 at 20:31
  • The only thing I would worry about is if you had no work in the main line. This would result in a fast forward merge. This fails to preserve the merge point. In that case, when you do the merge ensure you specify --no-ff to make an empty commit with 2 parents. This will preserve your branch point. Let me know if you need anything else. – Adam Dymitruk Jan 20 '11 at 21:14
  • 1
    "assumes you "undid" a merge already" -- does this mean, `git revert `? – John Bachir Jan 20 '11 at 21:31
  • 1
    also-- want to make some notes about --no-ff in your answer? (it's always ideal to do merges with it, to make them more undoable, right?) – John Bachir Jan 20 '11 at 21:32
  • yes, but you must provide -m1 or -m2 which will specify which parent is the feature you want to unmerge. Try without it and it will prompt you to specify one or the other. It's -m2 if you are merging from the mainline. – Adam Dymitruk Jan 20 '11 at 21:42
  • --no-ff ensures that you have a commit even if a merge was going to be fast forward. The issue with fast forward is that you lose the branch point. – Adam Dymitruk Jan 20 '11 at 21:45
  • BTW, there are other ways to "undo" a merge. – Adam Dymitruk Jan 20 '11 at 21:57
  • 1
    git-flow looks cool, but seems to not have anything to do with unmerging and remerging. Am I missing something? – John Bachir Jan 22 '11 at 20:07
  • 1
    Hey @adymitruk, thanks again for this answer, I just marked it as the answer finally. I also went OCD on the formatting :-D. Wanna replace "revert U" and "rebase F onto L" with the actual commands used? I think this might turn into the best resource on the subject on the web... – John Bachir May 28 '11 at 02:34
  • Wow. Ok, I'll give it a go on the weekend :) – Adam Dymitruk May 28 '11 at 07:17
  • 1
    dang. well it looks perfect on the desktop – John Bachir May 28 '11 at 18:08