There is a way to do this for just one file, without using the full fancy Git "time machine" aspects of version control. But you might as well use the full fancy version, since it works for more cases.
First, remember that git pull
just means: "run git fetch
(with some arguments), then if that succeeds, run git merge
(with some other arguments), or maybe git rebase
(with some arguments)." It looks like you have not told it to use git rebase
instead of git merge
, so I will assume you want and are using the merge behavior.
Note that you are using git merge
, and to repeat the merge properly now, you will want to run git merge
yourself, not let git pull
do it. So you need to know how to run git merge
, and what it does. The "need to know what it does" part is true even if you let git pull
run git merge
". To know what git merge
does, you also need to know what git fetch
does. I find that people new to Git actually do better if they run git fetch
and git merge
(or git rebase
) separately: it actually takes some of the mystery and confusion away.
What git fetch
does
Your Git works on your repository, which has your commits, remembered (pointed-to) by your branches. Your Git also talks with a second Git; that other Git has its own repository, with its own commits and branches. At various times, you need to have your Git call up their Git and bring over their commits—specifically, any commits they have, that you don't.
You do this by running git fetch
and giving it the name of a remote, probably origin
. If you have only one remote—most people do—it's usually just called origin
, and your Git can pretty easily figure out which one to fetch from, since there's only the one anyway. But let's just write it out:
git fetch origin
This uses the saved URL—a "remote" saves a URL—to call up the other Git and get its commits and branches and bring them into your repository. It also renames all their branches, so that their master
becomes your origin/master
, their develop
becomes your origin/develop
, and so on. Once you have their commits, they become "yours" too (not in terms of "author" or "committer", but in that you now have them, in your repository), so now you can work with them.
Draw a graph of commits
Before we get to the merge step, it's important to have some way to visualize what is happening.
If you use a GUI like gitk
or gitg
or one of the various fancy web ones, a lot of them will draw graphs. Even on the command line, you can run git log --graph --oneline --decorate --all
to get a crude graph drawing of your commits.
For StackOverflow, I prefer to just draw the commits left-to-right, mostly-horizontally. Newer commits are towards the right. Since a branch name like master
points to the tip-most commit of the branch, I put the branch names on the right as well, with an arrow pointing to the tip of the branch:
o <- o <- o <-- master
Each commit, each o
, points "backwards" to its previous (parent) commit. These backwards arrows are mainly just annoying and distracting, so let's draw the commits with connecting lines instead. And, let's draw a branching structure, where you have your own commits, and your git fetch
has brought in some other commits. Now we have this:
o--o--o <-- master
\
o--o <-- origin/master
Again, the newer commits are towards the right, so the tip of master
is the last commit on the first line, and the tip of origin/master
is the last commit on the second line of commits.
Each commit has one single "true name", which is the big ugly hash ID like 17fac9e...
or ea6631f...
. To be able to talk about them more conveniently, let's go with one-letter names or symbols instead, and redraw the above as:
o--*--A <-- master
\
o--B <-- origin/master
The round o
nodes are "boring" commits, that we don't need to mention again. Commit *
is special because it's where master
and origin/master
join up, if you work backwards from both. (If you work forwards, it's where they split apart. Git always works backwards, though, so you will find it easier if you do too.) Commit A
is special because it is the tip commit of master
, and commit B
is special because it is the tip of origin/master
.
Let's also remember here that each commit has a complete snapshot of all your files (made from "the index", which is why you have to git add
files to the index before you commit).
What git merge
does
When you are on your master
and run git merge origin/master
, Git finds two commits: the tip of your current branch, and the tip of the one you name. Here these are A
and B
. Then, Git finds the merge base, which is the first place these two branches come back together. That's our commit *
, and this is why we marked it.
The merge operation then runs two git diff
commands:
git diff <id-of-*> <id-of-A>
and:
git diff <id-of-*> <id-of-B>
The first diff represents "what we changed", and the second diff represents "what they changed". As with all git diff
s, you have to ask: "changed with respect to what?" The answer is: with respect to the merge base—commit *
.
Git then attempts to combine these two sets of changes. If it succeeds on its own, it assumes it got everything right, which is not always true, but is true surprisingly often: "surprisingly" because Git has no idea about code, it just uses simple text rules. It then commits the result.
If Git fails, it makes you finish the merge, and then commit the result.
(The merging all takes place using the index and your work-tree, but we can mostly ignore that here. That matters a lot when Git is making you finish the merge, though.)
Either way, you get a new commit, so let's add that to our graph. The new commit is a merge commit, which means that it has two parents instead of just one:
o--o--A---M <-- master
\ /
o--B <-- origin/master
Note that the old merge base is boring again, so it no longer has a star (*
). The new merge commit M
points back to both A
and B
. This means that commit B
is now on branch master
. It's also still on the remote-tracking branch named origin/master
.
You say you goofed up the merge M
. You don't say whether you added more commits afterwards. If you did, this all gets a little bit harder, because you will probably want to save those commits. If you've pushed these commits (including M
itself), it gets harder still because now you probably should save them forever. But for now, let's just look at what happens if you run git pull
again.
git pull = git fetch && git merge
We still have this when we start:
o--o--A---M <-- master
\ /
o--B <-- origin/master
Now we run git fetch
. This finds whatever is new on origin
, which is probably nothing. So it brings nothing over, and leaves origin/master
pointing to commit B
.
Now we run git merge
, telling it to merge origin/master
, i.e., commit B
, with our current commit, M
. This finds the merge base of M
and B
, which is the first commit that's on both branches.
That first commit is ... well, look at the graph, and follow M
backwards. Is it M
? No, because origin/master
points to B
, and you are not allowed to move rightwards, only leftwards. It can't be A
either, for the same reason. What about B
? That's certainly on origin/master
, which points right at it. And, aha, that's on master
too, because M
points back to B
!
So the merge base is B
, and if Git did a merge, it would make two diffs:
git diff B M # what we did
git diff B B # what they did
Obviously there's no difference from B
to B
though, so Git can simply skip all this: combining "what we did" (change B
to make M
) and "what they did" (nothing) leaves us with M
. So git merge
does nothing at all.
What happens if you extract the old file, then commit it? Well, now you have:
o--o--A---M--C <-- master
\ /
o--B <-- origin/master
where C
has the version of the file extracted from A
. Now we run git merge origin/master
. Git finds the merge base of C
and B
. What is the merge base? Why, it's just B
again. There is nothing to merge, and Git does nothing.
How you get Git to re-merge
To make Git do the merge again, you must make the current commit be commit A
. To do that, you have several options:
- You can make a new branch name pointing to commit
A
. (Use git branch <newname> <id-of-A>
and then git checkout <newname>
, or equivalently, git checkout -b <newname> <id-of-A>
.)
- You can use
git reset
to discard commits M
and C
, so that master
points to A
. (Use git reset --hard
, making sure you have no unsaved work.)
- You can
git checkout
the commit itself, getting a "detached HEAD", where HEAD
points to A
.
Let's draw the middle option since it's probably the one you want for this particular case. I can't "grey out" text, so I'll redraw the graph bigger and just label M
and C
as abandoned
:
o--o------A <-- master
\ \
\ M--C [abandoned]
\ /
o--B <-- origin/master
Since commit C
no longer has a name pointing to it, you won't see it any more. Since C
was the only path that let us find M
, you won't see M
any more either. We can still find commit A
, by its name master
, and B
, by its name origin/master
, though, so let's re-draw this more simply as:
o--o--A <-- master
\
o--B <-- origin/master
Look at that, we're back to what we had before! Now we can just run git merge
to re-do the merge. The merge base of A
and B
is the commit just to the left of A
(the one I had marked *
—might be time to put the *
back). Git will re-attempt the merge, and fail in the same way, with the same merge conflict.
When to do something more complicated
If you pushed commit M
and any subsequent commits, or otherwise gave them to other people, you will make more work for them by "retracting" your merge. In this case, consider whether this is OK. If it is, you can still do it. If not, leave M
(and any subsequent commits that you should also keep), in there.
In this case, what you can do is use the "detached HEAD" method, or create a new branch pointing to A
:
o--o------A <-- HEAD
\ \
\ M--C <-- master
\ /
o--B <-- origin/master
Now you can git merge origin/master
or git merge <id-of-B>
to retry the merge commit, and resolve it correctly this time. Make a new commit and it will be added to HEAD
as if that were a regular branch (it mostly is, it's just one with no name!):
o--o------A-----M2 <-- HEAD
\ \ /
\ M-/-C <-- master
\ /_/
o--B <-- origin/master
(The graph gets messy and there are no great ways to draw it now.) This new merge M2
has the correctly-merged file in it, because you did it right this time.
Now you can git checkout master
again and simply grab the correct file from merge M2
(whose ID you can either save, and cut-and-paste, or you can give this a branch-name for a little while, then delete that branch).