Fixing a committed unresolved merge conflict

10,378

If you've already merged and committed, re-merging will not do anything useful. As far as git knows, you're happy with the merge. (You just said you're not happy with it, but you can't tell that to git. :-) )

If the bad merge is not shared with anyone else, the easiest and probably best fix is to "un-do" it, and then re-do it as a corrected merge. (If there are additional commits after that, you can cherry-pick those commits to add copies to the corrected branch.)

If the bad merge is shared with others, you can still do the reset / re-merge / cherry-pick sequence, but all those "others" who are sharing this need to know how to recover from that. Or, you can leave the bad merge in, but fix it some other way. There's no single best answer for this case.

To "un-do" a merge, you need to understand how git does the merge in the first place, and how branch labels work with the commit graph. Let's draw a sample graph-fragment:

... - C - D - E ----- M - I  <-- main
        \           /
          F - G - H          <-- feature

Here, you had, at some point, a main branch-name that pointed to commit C. You (or someone else) created branch feature. Then you (or someone else) went back to branch main and made two commits D and E, at the same time as you (or someone else) worked on branch feature and made three commits F, G, and H.

(Each of these single letters stands in for one of the big ugly 40-character SHA-1 IDs like d1574b852963482d4b482992ad6343691082412f.)

Eventually, you (or someone else) got on branch main and ran git merge feature. This merge had a bunch of conflicts, but you (or the someone else) just added and committed the mishmash of files that git left behind, rather than correctly resolving the merge. This is what made the merge commit M.

If you haven't actually committed yet, commit M does not exist (and therefore, neither can commit I). All you have to do is run git merge --abort to stop the in-progress merge. That will leave you with this graph, with you on branch main (I assume, anyway; you could be on branch feature though, in which case, once you do make M, it will be on branch feature, on the lower line, instead of on branch main, on the upper).

... - C - D - E        <-- main
        \
          F - G - H    <-- feature

This is the graph you want to end up with at this point, if you intend to "un-do" the merge, even if commit M exists.

It's easy to get there, even if M exists. The reason is that these branch-tip pointers—the things stored in the branch name—merely point to some commit. I'm assuming that main now points to commit I. Well, we can leave commits M and I in the graph, but just make main point to commit E again:

... - C - D - E              <-- main
       \       \
        \       `--- M - I   <-- ???
         \          /
          F - G - H          <-- feature

We should first make sure we save the identity of commit I though, by making some label (branch or tag name) point to it. That is, let's fill in the three question marks. We can make a tag for this with git tag:

git tag save-main main

or make a branch with git branch. Either one will work fine.

Now we just need to make main point back to E, like in the drawing. The easiest way to do that is with cut-and-paste or by counting back. In this case, counting back shows that two hops back will get us to E (one hop back gets us to M), so:

git reset --hard HEAD~2

will move the current branch (i.e., main) back two steps in the graph (to E), and also re-set the work-tree to match the given commit (again, commit E: we just use the name HEAD~2 to name it). If you prefer, you can use the raw SHA-1, which you can find with git log.


Remember, all of the stuff above was just to get us back to the situation where we have not yet attempted to git merge feature into main. We've done that now; we have our graph looking the way we want it to.

Now we can simply do the merge the way we wanted to:

git merge -X theirs feature

This time, we should carefully inspect the results to make sure that the files in our work-tree look the way we want them to. Otherwise we'll make a screwed-up merge commit again, and be back where we started. But let's assume it's all gone well. The git merge will succeed and actually make the merge commit. There used to be a commit M, and this is a different commit and M is still there—we have our save-main tag pointing to I and I points back to M—so the graph we have now has a new merge M2:

... - C - D - E ----  M2      <-- main
       \       \    /
        \       `--/- M - I   <-- save-main
         \        | /
          F - G - H           <-- feature

As our last step, we probably want to restore the stuff from I by copying I to a new commit I'. To do that, we can simply use git cherry-pick:

git cherry-pick save-main

The name save-main points to commit I, so this copies whatever we did in I and adds it on to our current branch, main:

... - C - D - E ----  M2 - I' <-- main
       \       \    /
        \       `--/- M - I   <-- save-main
         \        | /
          F - G - H           <-- feature

and now we're all done with the name save-main so we can delete it:

git tag -d save-main

since we don't care about commits M and I any more.

(Note that cherry-picking I might go wrong, since I was built on top of the bad merge M. If it does fail, you'll get a merge conflict when attempting to do the cherry-pick. You will have to fix this up manually.)


Note that the process described above, of "un-doing" merge commit M by "rewinding" the branch-name main, causes headaches for anyone who is sharing your work (using your repository directly, or if you have git push-ed the bad merge so that others can see it, or whatever). They will have to retract the bad commits as well, and if they have based their own work on those bad commits, they may have to repeat their work. In this case you may want to take a different approach, by adding new commits that simply repair the failed merge (e.g., manually cleaning up the mess). In this case, those people sharing your work will simply pick up your fixes as fixes, which is something git is built to do, so this will be easier for them.

Share:
10,378
Koci Kocev
Author by

Koci Kocev

Updated on June 26, 2022

Comments

  • Koci Kocev
    Koci Kocev almost 2 years

    There are some files in the repository that contain the merge conflict syntax with HEAD and another commit. Those files were somehow committed and I cannot view the log now. I searched and found the command to merge with "git merge -X theirs" on the file. However, this gives an error "file does not point to a commit".

    How can I automatically remove everything from <<<<< HEAD to ==== and leave the other commit in each case? Doing this manually is very difficult as the files contain 70k lines and it would be very tedious.