Git fu: identify commit(s) that are "behind its remote counterpart" Git fu: identify commit(s) that are "behind its remote counterpart" heroku heroku

Git fu: identify commit(s) that are "behind its remote counterpart"


I suspect you have a basic misconception or two here that's leading you in the wrong direction. In git, a commit is simply an object identifying some particular version of the source tree (well, along with the usual commit message and a list of parent commit IDs; the last are important in that they are how git constructs the "commit graph"—which leads to "branches" below).

What this means, in particular, is that a commit is not "ahead" or "behind" anything else. It's just there, or not there: that's all it can be, present or absent.

Within git—and obviously this is exposed through heroku—there is also the concept of a "branch". This term is unfortunately rather heavily overloaded. It can refer to one (or more) of these:

  • a subset of the commit graph (a "DAGlet")
  • a branch label name
  • a "remote branch" label

See also this question for more, and some nice graphics illustrating how branch labels point to parts of the commit graph that git constructs based on the parent commit IDs stored in each branch-tip commit.

It's these branch labels that are "ahead" and/or "behind" their remote counterparts.

In this case, you have a shared remote repository on your heroku server (which you have called production: that's a "remote name", not a branch name). You also have your own local repository on your local machine (and your collaborators have their own local repositories on their machines, all separate from the single shared heroku repository). When some collaborator, let's call him Bob just for convenience, makes some change(s) and pushes, he gives his commit(s) to the shared repository—but you don't have them, you only have your commits: all the ones you shared earlier, plus some new one(s) you just made that are not yet shared.

So, now Bob has made some new commit(s) and pushed them successfully. Now his repo and the shared repo have mostly the same commits you have, plus his new one(s). You don't have those new ones, but you do have your own new one(s). That is, you have something like this:

A <- B <- C <- D <- F   <-- master

Here master is a branch label. and A through F (note that I skipped E; we'll see why in just a moment) are short for the 40-character SHA-1 commit IDs that you see if you run git log. I've drawn backward-pointing arrows between each commit because each commit records its parent ID (by 40-character SHA-1 number), so we can say that each commit "points to" its own parent.

Bob and heroku have this instead:

A <- B <- C <- D <- E   <-- master

where E is the new commit Bob pushed.

Git does not know or really care about Bob (and you don't really have to care either), all it knows at the time you do git push is that it calls up heroku on the Internet-phone, sends over your commit F, and then asks heroku's git to point the label master to commit F.

If the heroku-side git does that, the remote repository will wind up having the same commit chain that you have, where F points back to D, which points back to C, and so on. Commit E no longer has anyone pointing to it, so it gets lost. That's what the remote (heroku) git is telling your git, and your git is then telling you: there is at least one commit that would be lost if the remote made the change your git is asking it to make (or, with --force, your git would command it to make).

(Note that even if the heroku git loses this commit, Bob's git is not affected: he will still have his commit E. But this means you'd be forcing Bob to restore his work, perhaps by telling the heroku git to forget yours again and use his instead. That's no way to collaborate. :-) )


As for what to do about it, you have two (or three depending on how you count) choices:

  • run roughshod over Bob: wipe out his commit with a force push
  • collaborate: merge or rebase your work into / atop Bob's

(if you split the last into "merge with" as one option, and "rebase onto" as a second, you get three choices).

Both "collaborate" options require that you pick up Bob's commit. You could get it from Bob, but it's easier, at this point, to get it from heroku. To do that, you start with git fetch production: this obtains all the commits that heroku has that you don't—which in this case means commit E—and copies them to your own local repository, putting them on a "remote branch". (Side note: you may be able to run just git fetch, without the remote name, or git fetch --all; I'm using the remote name here to make things explicit, and because you used it in the git push command.)

Because you've named the heroku remote git production, the name of this "remote branch" label—which is actually local to your repository (in classic git-confuses-everyone style :-) )—will be production/master. In your repository, we can then draw a more-complete picture of the commit graph:

                   F   <-- master                 /A <- B <- C <- D                 \                   E   <-- production/master

You now have both labels pointing to the two commits that diverge from the common branch-ancestry ("DAGlet") of commits A through D.

It's at this point that you must decide whether to "merge" (make a new commit with two parents, to preserve the divergent history) or "rebase" (alter the history, copying your old commit F to a new one that's basically the same idea as F, but has as its parent, commit E instead of commit D). There's a lot of philosophical argument over which way is the right way, but for now let's just say "rebase is the right way", and illustrate the rebase, which you could do by running git rebase production/master.

To do the rebase, what git does is compare your original commit F to its parent D to see what you chagned. Then it checks out commit E, makes the same change you made before, and makes a new commit F' with the same message you wrote before as well:

                   F                 /A <- B <- C <- D                 \                   E - F'

I've taken the labels off here on purpose, because right after making that new commit, git does one last thing: it erases your production label (which pointed to F) and draws a new production pointing to the copy, F', which is otherwise the same as F. Now that you can't see the original, we can erase it, straighten out the downward kink in the graph, and draw this new graph:

A <- B <- C <- D <- E <- F'

There's no reason to call it F' any more either, so now we just call it F, and put the labels back on:

A <- B <- C <- D <- E <- F   <-- master, production/master

(both labels now point to F). It now looks as though you never wrote your original F; it looks instead like you waited for Bob to finish his work and then wrote your commit F based on Bob's latest. And, now you can push your commit F to the heroku remote, because F's parent commit is E, so if you get that git to make its master point to F, it won't lose any commits this time.


Those two steps—git fetch production, then git rebase production/master—can be done with a single git command, namely git pull --rebase production master. (Confusingly—user confusion is a pattern with git—this does not use the remote branch label production/master anywhere. When you use the two separate commands, though, you must use the remote branch label with the slash in it.) I often prefer the two separate commands, because that means you can see what happened before you start the rebase.

Once you've done the fetch (but neither a merge nor a rebase), to see what they have that you don't, or vice versa, give git log an extra argument:

$ git log master..origin/master

or:

$ git log origin/master..master

This double-dot syntax tells git "find commits starting with the label on the right and working back through the commit graph, but then don't show commits that are found by starting with the label on the left." Let's go back to the intermediate graph again:

                   F   <-- master                 /A <- B <- C <- D                 \                   E   <-- production/master

If we start at production/master, we get commit E. Working backwards from there we get D, then C, and on on. Starting from master, we get F, then D, then C, and so on. Hence master..production/master says "show E, but then don't show D or C or any of those earlier commits": they're all reachable by working back from F. So that shows us what we picked up from the remote git, without showing us what we already had before then. In particular, it shows the commits that our master is "behind" on, which is the ones that they're "ahead".

If you reverse the left and right sides of the two dots, production/master..master, that tells git to select the commits on master that aren't on production/master. This is where our master is "ahead", or theirs is "behind".

Summary: use git fetch and then the double dot syntax to see what you have that they don't, and vice versa. Use git rebase to copy what you have onto the end of what they have.


Has someone else in your group pushed in the mean time, so you are behind the Heroku repo because of that? Can you try to "git fetch origin --dry-run" to see what it would get? If you git fetch the origin/master" and pull in just the new changes onto origin/master, you can do a git log to list the difference between the tip of origin/master and your local tracking master branch to see the delta, something like "git log origin/master --not master"