elethan elethan - 1 year ago 140
Git Question

Git: switch branches mid-merge

I just spent the last few hours resolving merge conflicts that resulted from merging

big-feature-branch-B
into
big-feature-branch-A
. I am finally finished and all my resolutions are staged and ready to be committed. However, the process where I work is to:


  1. Create a branch off of
    big-feature-branch-A
    (I'll call this branch
    AB-merge-branch
    )

  2. Merge
    big-feature-branch-B
    into
    AB-merge-branch

  3. Create a PR for to merge
    AB-merge-branch
    into
    big-feature-branch-A
    , so that the resolutions can go through code review.



By the time I was most of the way through resolving merge conflicts, I realized that I was resolving them in
big-feature-branch-A
rather than a merge branch.

My question is, how can I safely change branches before committing my changes?

I am sure the answer is simple, and normally I would just stash my changes, switch branches, then pop my changes. However, I have never done this in the midst of a merge, and I am very skittish about just "trying it" in this case, because I don't want to risk having to resolve all those conflicts again, and I am not super confident with my git-fu. I have also read horror stories like this (but maybe my case is different, because I have already resolved all conflicts, and all changes are staged?), and don't feel like experimenting in this case. Thank you!

Answer Source

The short answer is that you can't (switch branches ... well, sort of, although you can, sort of, but it's not advisable—it involves monkeying directly with the innards of Git):

$ git checkout -b newbr
foo.txt: needs merge
error: you need to resolve your current index first

Your index is in this special "merging" state and therefore you cannot stash either. Fortunately, there is no need to do so. (If you resolve everything and then run git checkout -b newbr and commit, you get a non-merge commit. You can use this, but let's not do it that way.)

What you should do is go ahead and finish the merge: this gives you the merge result you want. You can then re-start the merge in the branch you want, and grab the result you just committed as the result you want, if that's even necessary. You then discard the original merge commit entirely (if necessary). (This is also what you need to do if you accidentally wrecked the merge with a successful git checkout -b as almost shown above.)

In some cases—including your case—the original merge's parent links are the ones you wanted, and it's just a matter of re-labeling the merge.

I'll show the recipe first, but then explain why it works:

... finish merging ...
$ git commit                         # commit the merge
$ git checkout -b AB-merge-branch    # create the merge branch
$ git checkout big-feature-branch-A  # get back to other branch
$ git reset --hard HEAD^             # take the merge off of it

Why this is an answer (there are multiple ways so I won't say the answer)

Let's draw your setup as it was when you first started to merge:

       o--...--o--o   <-- big-feature-branch-A (HEAD)
      /
...--*
      \
       o--...--o--o   <-- big-feature-branch-B

That is, you were, as git status would put it, "on branch big-feature-branch-A" and all was well. Each o represents a commit (and I marked the merge base, for the merge, with an asterisk).

You then intended to run git checkout -b AB-merge-branch. If you had done that, the picture would look like this:

    o--...--o--o   <-- big-feature-branch-A, AB-merge-branch (HEAD)
   /
--*
   \
    o--...--o--o   <-- big-feature-branch-B

You ran git merge, which failed with conflicts. You resolved (most of) the conflicts (you must resolve them all before you proceed). When you finally commit, you will get a new merge commit, and that will move the current branch (the one remembered by HEAD):

    o--...--o--o   <-- big-feature-branch-A
   /            \
--*              M   <-- AB-merge-branch (HEAD)
   \            /
    o--...--o--o   <-- big-feature-branch-B

Not shown here (because it is too hard to show) is that the first parent of the new merge M is the rightmost (latest) upper row commit, and the second is the lower. (This first vs second stuff matters later, if at all, when someone wants to follow the "main" branch vs "side features that were merged in": the main branch is always the first parent, by definition.)

You forgot to create a new branch name, so what happens now, instead, is this:

    o--...--o--o
   /            \
--*              M   <-- big-feature-branch-A (HEAD)
   \            /
    o--...--o--o   <-- big-feature-branch-B

The first parent is still the top-and-right-most commit, the second parent is still the bottom such commit. The new merge commit M is exactly the same. It's just that the label that moved, to point to new merge M, is big-feature-branch-A (the one that is HEAD), and not the nonexistent AB-merge-branch (which obviously isn't HEAD).

So all you have to do now is create the labeling you wanted. Anything that makes the new branch name AB-merge-branch, pointing to M, suffices for that part. You can use git checkout -b AB-merge-branch or git branch AB-merge-branch to do that.

If you do use git checkout -b you now have to get back to big-feature-branch-A to fix it up using git reset (there are other commands you could use, but I'm sticking with reset here). If you use git branch to create the new branch, your current branch is undisturbed: you are still on big-feature-branch-A.

In any case, you want this big-feature-branch-A branch name to move back one step, to the first parent of M, as if it had never moved forward to M in the first place. So you get back on (or stay on) this branch. Once you're on this branch, you can use git reset --hard HEAD^ to achieve this moving-back-one-step. HEAD^ means "find the first parent of HEAD", which—if HEAD names commit M (it does)—means the rightmost upper row commit, which is where you want the branch to point. The git reset command does this re-pointing of the branch, and also re-sets your index and work-tree (so that everything is cleanly on the new commit).

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download