I'm working in a feature branch. After running git merge from the main branch, I had two conflicts.
I manually resolved them, and then committed only those two files, and ran git reset --hard to remove the other changes.
The result - git thinks the other files were merged, while in reality, they're 100% from my feature branch.
How do I fixed this and "re-merge" with the main branch?
The problem statement itself is just a little off, because
git commitwrites all the files, not just some of them. I'll tweak it a bit first, but feel free to skip forward to the next section. :-)More specifically—this should help you think about Git in the future—Git writes whatever is in the index (which must be fully merged). The
git mergecommand builds, in the index, the result of merging two (or more but let's stick with two) commits. If there are conflicts, Git stores the conflicted files in both the work-tree and in the index.You can then edit the files in the work-tree, which you did. You can use
git addto copy the work-tree files into the index, replacing the conflicted versions with the single resolved version.1 You did that too. You can usegit reset, in itsgit reset pathsform, to copy specificpathsfrom theHEADcommit into the index, replacing whatever is currently in the index.It's this last step that created the problem: Git had merged those other files, successfully (at least in Git's tiny little mind :-) ). Then you told Git that it should copy the
HEAD(current commit, pre-merge) versions of those files into the index, undoing the effect of the merge on those files.1The gritty detail here is that when there is a conflict, the index winds up holding not one but three versions of some file. For instance, suppose you have run
git merge other, andconflict.txthas conflicting changes from base commit toHEADvs base commit toother. These conflicts show up in the work-tree copy, but at the same time, Git actually puts all three versions—base,HEAD, andother—into the index, under the nameconflict.txt. You can see them withgit show :1:conflict.txt,git show :2:conflict.txt, andgit show :3:conflict.txt, for instance.Running
git addtells Git to toss out the three extra index versions and store just one:0:conflict.txtfile as the correctly-merged index version, ready for the nextgit commitcommand. Git will allow another commit any time the index has only zero-numbered versions. Whatever is in the index at that time, becomes the contents of the new commit. (If these contents exactly match the current orHEADcommit, Git requires that you add the--allow-emptyflag. This makes it seem like the index is empty, but it's not: it's the diff that's empty.)Diagramming the problem
There are several ways to go about fixing this. To find the best and simplest, we really need to draw some diagrams.
Specifically, before you did the merge you want to re-do, you had some series of commits:
You were on
mainbranch(hence it wasHEAD). This means thatHEADrepresented commitL(for Local or Left-side). You rangit merge feature, which identified commitR(for Remote or Right-side or otheR or featuRe; anyway, I generally call itR). Git then computed the merge base commitB, which is the first place the histories of the two selected left and right commits join up.Git then ran, in effect:
and combined the two diffs. There were some conflicts, which you resolved, and some non-conflicts, which you
git resetaway to theLversions. Due to the conflicts, Git made you rungit commityourself, and when you did, you got this:The new merge commit
Mhas, as its contents, whatever was in the index when you rangit commit.It's my guess that you did not discover the issue until later. You probably did another
git checkout featureand wrote more commits, giving:Then you went back to
mainbranchand rangit merge featureagain. I can't call the new tipsLandRany more, so I will stick withMandS. Let's find the merge base ofMandS, by following all the connections backwards (leftwards, including left-and-down), from bothMandSsimultaneously:MorSthemselves, we'd have to go back and then forward fromM.obeforeSeither.Rlooks pretty good: we can go back one step fromMtoRby going down too; and we can go back two steps fromSto get toR.So the merge base is
R, and Git runsgit diffonR Mand onR S. Git then sees thatR-to-Mtakes feature away from thefeaturecode, whileR-to-Sadds or modifies the features. Git tries to combine these changes, and the result is a big mess. If the merge itself works, we get this:though it might not work well due to conflicts in the "undoing" going from
RtoM, vs the "doing" going fromRtoS.But we might even have this, depending on what happened on the main branch:
(in which case the merge base was still
R, but the first diff was to compareRtoN).Option 1: remove
MentirelyIf we don't have commit
Nat all, and if it's safe to "remove" commitMentirely, we can just do that, usinggit reset. We cangit checkout mainbranch(to attachHEADto it),git reset --hardto commitL, and have this:Then
git merge featurewill run diff onB Land a second diff onB S, and try to combine the diffs. You'll have mostly the same merge conflicts asB LvsB R, plus perhaps some more.You can resolve them the same way (repeating earlier work). Or you can save the identity of commit
Mand extract the resolutions fromM. Although we have erasedMfrom the drawing, it's actually still in the repository. It's just hidden; it will really go away later, if you don't do anything else, in about a month. SinceMis still in there, we can extract files from it:We can even attach a name to
Mbefore we do thegit reset. For instance, using a tag name:we can then:
to get the saved files. Once we're done, we can just
git tag -d temp-save-mergeto drop the special name for commitM(which will, as before, let Git remove it automatically some time after 30 days, now that there are no names by which we can see/reach it).Option 2: keep
MIf commit
Mhas been pushed elsewhere, though, and/or if commitNexists and/or has been pushed, it may not be such a great idea to attempt to removeMfrom existence. We may succeed in removing it from our repository, but it might come back from another repository later. We'd have to force-push to make it go away from any central repository, and convince everyone else who uses that repository to discard their copy ofM. If we needNwe'd have to bring it back as well.Instead, we'd probably like to reconstruct the files we wish we had not
git reset, and bring those changes in atopN. I'll leave commitNin the drawings from here on; ifNdoesn't actually exist, we're just building atopM, and the result is all the same anyway.First, let's get rid of
M2(which is unsuccessful at best and presumably has not been pushed anywhere) by runninggit merge --abortif we haven't actually madeM2yet, or bygit reset --hardif we have, so that we have a clean index and work-tree and have this graph:Next, let's make a new branch pointing to commit
L:which gives us this diagram:
We'll stop drawing
mainbranchfor a bit since it's getting all messy. We can now run:to repeat the same merge we did incorrectly earlier. This will get us a "new and improved" merge, although it will stop with the same conflicts as before. We'll get the correctly-merged files out of
M, add them, and commit:Now we have this:
Now let's add a copy of
N, usinggit cherry-pick mainbranch, and attempt to draw it all:This final commit
N2is what we would like to have in branchmainbranch. The reason we are doing all this hairy stuff, though, is to preserve the existing hash IDsMandN, so what we want now is something Git doesn't quite offer: a "theirs strategy" merge. (See Is there a "theirs" version of "git merge -s ours"?)But we can get what we need anyway, using one of the methods in that other answer. Here's the most obvious one, which assumes you are sitting at the top level of your work-tree:
The
git mergestep starts the merge, but with--no-commit, never finishes it. We use--oursjust to make it go fast: this means "keep the contents ofN", which is quite wrong. Then we remove everything! The index is now truly empty.Now we use the real trick: we re-fill the index from the
N2commit. Thegit checkout temp-merge -- .command replaces everything we have with the contents of the commit to which the nametemp-mergepoints, i.e.,N2. This happens in both index and work-tree, so now we are all set to commit the final merge result. When we do we get this graph:where the contents—the tree—for commit
M3is exactly the same as that forN2, which is the corrected tree.We can now delete the branch name
temp-merge. The history is full of our double merge, so the graph is always messy to draw. That's because we chose Option 2, in which we don't rewrite history to erase our earlier mistake. The mistake is preserved for all time, but this also means that we're only adding new commits (M3and its side chain that used to be calledtemp-merge), so other people using clones of this repository can work in their normal fashion.Summary
If you can use Option 1 (rewrite history, pretend the bad merge never happened), you probably should.
You can even use a sort of hybrid: start with the temporary merge branch method, make commit
M2, and cherry-pickNtoN2ifNexists. But then, instead of merging this withmainbranch, rename the oldmainbranchand rename the temporary merge tomainbranch, giving:You may have to force-push the renamed temporary branch, and this makes headaches for other users if there are any, but if we then delete the
mistakebranch later and draw our graph, we get:which is quite normal looking. (And, note that we can now merge
SatopN2in the usual way.) It does mean we have rewritten history, but we did it in a sort of easy-ish way: we re-did the merge ofLandR, grabbing the correctly-merged files out of commitM, then we tossed commitMandNaside in favor ofM2andN2. This is effectively the same as Option 1, except that we keep the mistake branch around until we're done with it.