0

I had the merge conflict while pulling from another branch and used "Use mine" instead of "Use theirs" mistakenly for a single file. the other conflicted files were taken good care.

Now i want to move to the old state of this file (before conflict) and pull again from the branch to create the conflict and select the right option.

I have used checkout git checkout "commit#" PathToFile which rightly retrieves the file to the state it was in (before the conflict), but now when i pull from other branch the conflict is not there.

Fast Mani
  • 171
  • 8

1 Answers1

2

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 diffs, 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).

torek
  • 448,244
  • 59
  • 642
  • 775