S V S V - 1 month ago 5x
Git Question

Git : there are commits I did not create. How to get rid of those

I always do a git pull --rebase even if I do not have a single pending commit.

I just did a pull.

Suddenly, when I run git status, I am seeing that my local master is 11 commits ahead of the remote master.

I dont know where they came from and want to get rid of them.

What do I do?


It's clear that you're a victim of what is usually called an "upstream rebase". You can jump right to the solution below but this background is pretty important.


This why I hate git pull. It's not git pull's fault, really, but because git pull is a convenience command that simply runs git fetch followed by a second command, it winds up hiding the two underlying Git commands from new Git users. The fact that there are two different second commands, both of which can mess things up in different ways, is spectacularly unhelpful.

Fortunately, the first part—the git fetch step—almost always works.1 It's the second step where, if all goes well, everyone says "oh that was easy" and thinks that git pull is great and then when it explodes, as it always does eventually, they're in trouble, because they have never had to deal with the second part failing. And now it's an emergency and they have to spend a bunch of hours learning a bunch of complicated stuff: the way to recover from failures, and how merge and rebase interact with the git fetch step.

Anyway, this time you are the one stuck with the messy situation, so let's just press on with it. The second half of what git pull does is either git merge or git rebase. Exactly which command it does when is beyond the scope of this answer, except that we can say for sure that if you add --rebase, it always uses git rebase. So you only have one complicated Git command to learn in a hurry. (Yay? :-) )

Hence, as Tim Biegeleisen answered, these new mystery commits came from using git rebase. So it's time now to learn all2 about rebase, and how it interacts with git fetch.

1There are still a few ways it can go wrong, but usually the one that actually happens is "the network is down and thus we can't get anything from origin" which is really obvious and makes everything stop right at the beginning, and you just try again once the network is fixed.

2Well, let's not go that far. :-)

When fetch-and-rebase goes well

Rebase can be complicated but we can sum it all up very simply: git rebase copies commits.

That's it. Rebase copies commits. That's actually not bad at all, is it? Well, it's not bad when they are your commits, but you just had it copy someone else's commits. The tricky part here is defining precisely which commits get copied, when, and how. This is where git fetch comes into the picture again.

Tim's drawing here covers the more normal case, when you have made some commits. I'm going to make my own drawing though. Let's say there are a bunch of commits, ending with a series F--G--H, when you start out. That is, your master and origin/master both point to commit H, which points back to G, which points back to F, and so on. You got all these when you did your initial git clone or your last git pull --rebase or whatever: it doesn't really matter how you got them, just that you do have them.

...--F--G--H      <-- master, origin/master

Now, normally, you might add your own commits, say I and J:

             I--J   <-- master
...--F--G--H        <-- origin/master

Meanwhile someone else, using their own Git, adds their own different commits. You don't have them now. Then they publish those commits, so now origin has them too. You still don't! But now you run git fetch and now you get them, and here's what happens in your repository. Let's say they made just one commit, which we'll label K:

             I--J   <-- master
             K      <-- origin/master

Note how the label, origin/master, has "moved forward" from H to K. Meanwhile your own master is already moved forward from H to J: yours points to your commit J, which points back to your commit I, which points back to H.

This is where you normally bring git rebase in. You ask it to copy your commits, I and J, to new commits that are "just as good" but are based on commit K:

             I--J   <-- master
...--F--G--H   I'-J'
            \ /
             K      <-- origin/master

Let's say the copy goes well, with no problematic merge conflicts and such. You now have new commits that are just like your originals, except for their new "base" commit K. That's a rebase!

All your Git has to do now is peel your master label off the old J and make it point to the new copied J':

             I--J   [abandoned]
...--F--G--H    I'-J'   <-- master
             \ /
              K      <-- origin/master

and now that the old commits are officially abandoned, we can straighten the whole picture out:

                I'-J'   <-- master
...--F--G--H--K         <-- origin/master

When the rebase goes badly

There are multiple ways for git rebase to go awry. Let's ignore some of them and just concentrate on the one that bit you.

Let's draw out more of the stuff that comes before F--G--H this time. Also, you didn't have your own commits (you said, and I believe you).

A--B--C--D--E--F--G--H   <-- master, origin/master

Now, someone else did something upstream to "rebase", i.e., to copy some commits. I don't know exactly what they did, but let's say they discovered that commit D was Defective, and perhaps even deleted it entirely. They now have:

        D--E--F--G--H   [abandoned]
        E'-F'-G'-H'     <-- master

Now you run your git fetch. What this does is to pick up their master and forcibly adjust your origin/master to match theirs. So you still have A-...-H as usual, with your master pointing to that, but now you have origin/master pointing to H':

        D--E--F--G--H   <-- master
        E'-F'-G'-H'     <-- origin/master

That's your git fetch step. Now your git pull runs git rebase. What rebase does, or tries to do, is to copy commits. Which commits should it copy? The answer here can get complicated, but to simplify things a lot, it will think, at this point, that D--E--F--G--H are your commits. Those are the obvious copy candidates: they're the commits on your master, that are not on origin/master.

It does matter that E'-F'-G'-H' are copies themselves. But let's just ignore that. Whether your Git detects them as copies or not, your Git is at least going to restore commit D, thinking it's your work, rather than something that got stripped out. In any case, because you're seeing 11 commits, your Git has somehow restored or resurrected or copied 11 commits. Let's just draw all of D-through-H since our solution will be the same either way.

Your git rebase copies "your" commits (not really yours, it just thinks they're yours) and moves your master label, and you now have this:

        D--E--F--G--H   [abandoned]
A--B--C             D'-E"-F"-G"-H"   <-- master
       \           /
        E'-F'-G'-H'     origin/master

Suddenly you're five commits "ahead of" origin/master.

Fixing it

In this case, since you're sure you really have no commits of your own, what you need to do is tell your Git to abandon all these new copies entirely.

The command that does all this for you is git reset --hard. The thing about git reset --hard is that it does let you throw away a bunch of your own work, so you must be careful about when you use it.

In this particular case, though, you promised you had no commits of your own here. Remember, it's just this particular case: no commits of your own, no uncommitted work in the work-tree, etc. So it's safe to wipe things out.

So, what you want to do in this case is to tell your Git to move your master to point to the same commit as origin/master, abandoning all the copies that your git rebase made:

        D--E--F--G--H   [abandoned]
A--B--C             D'-E"-F"-G"-H"   [abandoned]
       \           /
        E'-F'-G'-H'     <-- master, origin/master

If we now stop drawing in the abandoned commits, you'll see that your graph is now just:

A--B--C--E'-F'-G'-H'   <-- master, origin/master

which is what you want: you're no longer "ahead of" origin/master.

If you had commits of your own

Suppose, in all that mess, you really did have some commits to keep. In this case, there are a bunch of ways to fix this, but probably the easiest is to use git rebase -i (interactive rebase: copy selected commits). You can git rebase -i to get the 11 commits that Git thinks are yours into a set of instructions. Then you delete, from the instruction sheet, all but the ones that really are yours, and your Git copies those yet again, and abandons the un-copied ones.

Since you promised there were no such commits, we were able to use the simpler git reset --hard. (Depending on your Git version, we might have to use reset. All versions of Git refuse to let you delete everything from the instruction sheet. Newer versions let you put in a "no-op" instruction so that there's at least one instruction left, even if there are no copy instructions.)