Martin Nally Martin Nally - 3 months ago 17
Git Question

Git fetch a single commit

I would like to fetch a single commit (with history, of course) of a remote repository into a local one. Some Stack Overflow responses suggest that the following should work, but it does not:

md foo
cd foo
git init
git fetch https://github.com/ld4apps/lda-serverlib.git f1e32e18a90e10c150221af55c69aeafaa42c57a


This produces the following error:


error: no such remote ref f1e32e18a90e10c150221af55c69aeafaa42c57a


Surprisingly (to me) the following does (sort of) work:

git fetch https://github.com/ld4apps/lda-serverlib.git :f1e32e18a90e10c150221af55c69aeafaa42c57a


This creates a new local branch called
f1e32e18a90e10c150221af55c69aeafaa42c57a
(which I did not want), but at least the new branch has the contents I was trying to fetch. I might have expected that the following command would do the same thing, but it gets the same error as the first example:

git fetch https://github.com/ld4apps/lda-serverlib.git f1e32e18a90e10c150221af55c69aeafaa42c57a:f1e32e18a90e10c150221af55c69aeafaa42c57a


I tried this on Windows and Linux with a couple of different git versions and saw the same behavior.

Can anyone explain what is going on here? What am I not understanding about Git,
fetch
, and refspecs? What I really want is a single
git fetch
command to which I can provide either the id of a remote branch or tag or a commit id and it will fetch the corresponding commit.

Answer

Update: since Git 2.5 (mid-2015 or so), servers now may expose raw hashes. This is a server-side configuration item. You must have sufficient control on the server to set it, and you should consider potential security issues before you do set it. See this answer to Retrieve specific commit from a remote Git repository for details. (Original answer, which applies to pre-2.5 or if the configuration knob is not set, below.)


The git fetch command delivers references (names, not raw commit-IDs) to the remote, more or less. (More specifically, use git ls-remote remotename to see what the remote is willing to give you in terms of names. This produces a list of SHA-1s on the left with names on the right, and the only thing your fetch can ask for is the names-on-the-right. At which point you'll get the ID-on-the-left if the name on the remote still points to that ID, so it depends on how actively that remote gets updated.)

It is possible, in various ways, to deliver raw commit-IDs to a remote and ask that remote what is visible starting from that point, and sometimes working backwards through history as well, but not via git fetch. (You can use git archive but the remote can decide whether or not to allow you to access via raw commit-IDs; or with remotes that have web server access, including to specific commits, you can often just view the top-level contents of a commit, and use that to "drill down", as they say, to the various pieces. But that is a very slow way to do it.)

If you'd like to use git fetch to get some particular commit, probably the easiest way to do that is to have someone with access to the remote attach a name—most likely a tag—to that commit ID. Then you can have your git fetch bring over that refspec, and put it under any other refspec you like. For instance, suppose you can ssh directly to whatever hosts origin:

$ ssh our.origin.host 'cd /repos/repo.git; git tag temporary f1e32e1'
[enter password, etc; observe tag created]
$ git fetch origin refs/tags/temporary:refs/heads/newbranch
[observe fetch happen; now you have local branch 'newbranch']
$ ssh our.origin.host 'cd /repos/repo.git; git tag -d temporary'

Note that the name need not be a branch, it need only be a reference you can pull over with git fetch and see with git ls-remote. You then use a name that will match that on the left-hand-side of your refspec when fetching. The name created in your repo is controlled by the right-hand-side of the refspec (refs/heads/newbranch in the example above).

This is also the answer to your last paragraph question: you can only name things that have names on the remote (this is partly intended to avoid "leaking" unnamed commits that remain in a repository before garbage-collection, so it's considered a feature rather than a bug). These names go on the LHS of the refspec. Your own names go on the right.

Your name on the right is assumed to be a branch or tag name (based on what the name on the left matches, though you can explicitly spell out refs/heads/ or refs/tags/ to override it), so even though f1e32e1... is a valid SHA-1, it's treated as a branch name here—the missing name on the left translates to HEAD, as missing names almost always do—and git fetch creates a branch whose name is disturbingly SHA-1-ish. (Incidentally I once created a branch name that looked like an SHA-1, and later confused myself. I forget exactly what the name was, something like de-bead without the hyphen. I renamed it to the hyphenated version just to make it clear I didn't mean a raw commit ID! :-) )