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 commit
writes 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 merge
command 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 add
to 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 paths
form, to copy specificpaths
from theHEAD
commit 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.txt
has conflicting changes from base commit toHEAD
vs 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 add
tells Git to toss out the three extra index versions and store just one:0:conflict.txt
file as the correctly-merged index version, ready for the nextgit commit
command. 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 orHEAD
commit, Git requires that you add the--allow-empty
flag. 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 thatHEAD
represented 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 reset
away to theL
versions. Due to the conflicts, Git made you rungit commit
yourself, and when you did, you got this:The new merge commit
M
has, 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 feature
and wrote more commits, giving:Then you went back to
mainbranch
and rangit merge feature
again. I can't call the new tipsL
andR
any more, so I will stick withM
andS
. Let's find the merge base ofM
andS
, by following all the connections backwards (leftwards, including left-and-down), from bothM
andS
simultaneously:M
orS
themselves, we'd have to go back and then forward fromM
.o
beforeS
either.R
looks pretty good: we can go back one step fromM
toR
by going down too; and we can go back two steps fromS
to get toR
.So the merge base is
R
, and Git runsgit diff
onR M
and onR S
. Git then sees thatR
-to-M
takes feature away from thefeature
code, whileR
-to-S
adds 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
R
toM
, vs the "doing" going fromR
toS
.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 compareR
toN
).Option 1: remove
M
entirelyIf we don't have commit
N
at all, and if it's safe to "remove" commitM
entirely, we can just do that, usinggit reset
. We cangit checkout mainbranch
(to attachHEAD
to it),git reset --hard
to commitL
, and have this:Then
git merge feature
will run diff onB L
and a second diff onB S
, and try to combine the diffs. You'll have mostly the same merge conflicts asB L
vsB R
, plus perhaps some more.You can resolve them the same way (repeating earlier work). Or you can save the identity of commit
M
and extract the resolutions fromM
. Although we have erasedM
from 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. SinceM
is still in there, we can extract files from it:We can even attach a name to
M
before 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-merge
to 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
M
If commit
M
has been pushed elsewhere, though, and/or if commitN
exists and/or has been pushed, it may not be such a great idea to attempt to removeM
from 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 needN
we'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 commitN
in the drawings from here on; ifN
doesn'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 --abort
if we haven't actually madeM2
yet, or bygit reset --hard
if 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
mainbranch
for 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
N2
is what we would like to have in branchmainbranch
. The reason we are doing all this hairy stuff, though, is to preserve the existing hash IDsM
andN
, 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 merge
step starts the merge, but with--no-commit
, never finishes it. We use--ours
just 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
N2
commit. Thegit checkout temp-merge -- .
command replaces everything we have with the contents of the commit to which the nametemp-merge
points, 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
M3
is 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 (M3
and 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-pickN
toN2
ifN
exists. But then, instead of merging this withmainbranch
, rename the oldmainbranch
and 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
mistake
branch later and draw our graph, we get:which is quite normal looking. (And, note that we can now merge
S
atopN2
in 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 ofL
andR
, grabbing the correctly-merged files out of commitM
, then we tossed commitM
andN
aside in favor ofM2
andN2
. This is effectively the same as Option 1, except that we keep the mistake branch around until we're done with it.