stillanoob
stillanoob

Reputation: 1379

Git: Fetch a detached HEAD from remote

I checkout a particular commit (and hence, end up in a detached HEAD state) and then make a few commits on top of it. Let's say I did this in repository A. Let's further assume I have an another repository B which has A as one of its remotes. Now, in B, how do I fetch the detached HEAD of A without creating a new branch in A?

Upvotes: 3

Views: 1464

Answers (2)

torek
torek

Reputation: 487755

As ElpieKay answered in a comment, use git fetch remote HEAD, which saves the fetched commit's hash ID in the special FETCH_HEAD file. You can then use FETCH_HEAD as a reference until the next git fetch overwrites it.

Discussion

Both fetch and push operations work with names, but they are not symmetric.

They are symmetric when it comes to transferring commits. That is, whether you run git fetch remote [refspec...] or git push remote [refspec...], the sending and receiving Git systems have a conversation involving object hash IDs, where the sender advertises which hash IDs the sender would like to give to the receiver: I have <hash> for you, and the receiver sends back replies saying that the sender should send that, or—if the receiver already has that object—not send it. (It's a little more complicated than this, since a fetch-receiver kicks off the process with the first "wants", but close enough.)

When this is done, though, the push operation has the sender send over a series of recommended <refname, hash-ID> pairs: Please set your refs/heads/master to a123456... for instance. This means that if you are doing git push whlie you are on a detached HEAD in your repository, you still have to give the other Git a name for this commit:

git push origin HEAD:refs/heads/somebranch

for instance suffices to have your Git send the hash ID for your HEAD commit, but recommend that their Git set their refs/heads/somebranch to that hash ID when done. You cannot ask them to set their HEAD: if you try, they'll just create a branch named HEAD, i.e., refs/head/HEAD, if you're on a branch now, or reject your push request if not:

error: unable to push to unqualified destination: HEAD

On the other hand, when you run git fetch, you control which reference(s), if any, get updated on your end. Their Git simply sends a list of all their references (in protocol v0 anyway; v2 is fancier). Your Git picks over the list and, if they sent you new hash IDs for their refs/heads/master and refs/heads/branch, your Git will generally update your own refs/remotes/origin/master and refs/remotes/origin/branch. Your Git takes the list of their references, generates your side's "want" list of hash IDs, and delivers that to the sender to kick off the have/want hash ID conversation.

That is, that's what your Git does if you run git fetch origin, with no added refspec arguments, and assuming your configuration is normal (not the special configuration left behind for a --single-branch clone for instance). But if you do add refspec arguments, e.g.:

git fetch origin refs/heads/master:refs/weird/name

then your Git asks their Git to send only the commits you need to work with their master. That is, the have/want conversation starts with only the hash ID in their refs/heads/master (and even then, only if you don't already have it). When the have/want is done and the objects have arrived in your repository, your Git then creates or updates your refs/weird/name reference.

Remember, these refspecs have the general form src:*dst. The src part is the source reference—the name or hash ID that the sender uses to find the commit—and the dst part is the destination reference that the receiver should use to remember the hash ID in the end. You can omit one of the two by writing src or :dst, which has various special-case meanings depending on push vs fetch. Whether a raw hash ID works in the src part of this expression depends on two things:

  • if you are doing a push, it always works (as long as the object exists);
  • if you are doing a fetch, it works if and only if they allow it.

(So here, we already see that fetch and push are asymmetric.)

For git fetch, if you omit the :dst part of the refspec—e.g., git fetch origin refs/heads/master or git fetch origin master—your Git skips the create-or-update part, except for so-called opportunistic updates (creating or updating refs/remotes/origin/master, in this case). However, for each name that your git fetch obtained, your Git always writes that <name, hash-ID> pair to your FETCH_HEAD file:

$ git fetch origin HEAD master
From ...
 * branch                  HEAD       -> FETCH_HEAD
 * branch                  master     -> FETCH_HEAD
$ cat .git/FETCH_HEAD
f84b9b09d40408cf91bbc500d9f190a7866c3e0f        <url>
f84b9b09d40408cf91bbc500d9f190a7866c3e0f        branch 'master' of <url>

(Note that although git fetch got many branches and tags in the list of name/ID pairs from origin, we only asked for HEAD and master, so that's what git fetch wrote into .git/FETCH_HEAD.)

Conclusion

If you're sending commits, you must provide a name for the other Git. Usually the name is implied: you push your branch bran, so the name you want them to set is their branch bran. You can push any object: it is up to their Git, after receiving the object, to decide whether to accept the <name, hash-ID> pairing. Usually, you'll push a commit object, which will drag with it all the other objects required, and you'll have them set a branch name.

If you're receiving commits, though, you need not provide a name on your side. Their Git will send their names and objects, and your Git will use your .git/FETCH_HEAD file to remember the hash IDs you got from them. If you do provide names on your side, your Git will update those names, and if you don't, Git has some complicated default rules for fetching, to remember their branch names via refs/remotes/remote/ names.

While HEAD is not itself a branch name, it is a valid name. You may not be able to make them update their detached HEAD (via push), but you normally can have them send you the commit hash stored in their detached HEAD, which your Git will remember as "unnamed" in your .git/FETCH_HEAD.

Upvotes: 4

Helena Vargas
Helena Vargas

Reputation: 11

I think you can't just push a detached head. If you want to see those changes in B, you have to push the commits somewhere in A. Either into a new branch or into an existing one. Since you don't want to create a new branch in A, I'm assuming that you have pushed the commits into the original branch. Therefore, you can access them with a simple pull.

If you haven't pushed the commits anywhere, I would say that the best approach is to make the changes directly in repository B, if that is an option.

Upvotes: 0

Related Questions