Skip to main content
fix redundant definition of detached HEAD state (I moved it and forgot to delete the later one)
Source Link
torek
  • 502.5k
  • 71
  • 780
  • 899
  1. There is always—well, almost always—a current commit, which you can use the word HEAD to find.
  2. There is not always a current branch, but if there is, it's a branch name, i.e., a reference whose full name has the form refs/heads/name. The same word—HEAD, in all capital letters, finds that branch name. If there isn't a current branch name, Git calls this a detached HEAD.
  3. A remote-tracking name, such as origin/master, is not a branch name. Its full form starts with refs/remotes/ rather than refs/heads/.
  4. If you tell git checkout to check out a commit, but identify it by something other than a branch name, Git will—if the checkout succeeds, that is—produce what Git calls athe detached HEAD state described in point 2. (You can also produce this same state with a branch name, using git checkout --detach.)

The git symbolic-ref HEAD command answers only the first question; git rev-parse HEAD mostly answers the second, but can be told to answer the first too / instead.

  1. There is always—well, almost always—a current commit, which you can use the word HEAD to find.
  2. There is not always a current branch, but if there is, it's a branch name, i.e., a reference whose full name has the form refs/heads/name. The same word—HEAD, in all capital letters, finds that branch name. If there isn't a current branch name, Git calls this a detached HEAD.
  3. A remote-tracking name, such as origin/master, is not a branch name. Its full form starts with refs/remotes/ rather than refs/heads/.
  4. If you tell git checkout to check out a commit, but identify it by something other than a branch name, Git will—if the checkout succeeds, that is—produce what Git calls a detached HEAD state. (You can also produce this same state with a branch name, using git checkout --detach.)

The git symbolic-ref HEAD command answers only the first question; git rev-parse HEAD mostly answers the second, but can be told to answer the first too.

  1. There is always—well, almost always—a current commit, which you can use the word HEAD to find.
  2. There is not always a current branch, but if there is, it's a branch name, i.e., a reference whose full name has the form refs/heads/name. The same word—HEAD, in all capital letters, finds that branch name. If there isn't a current branch name, Git calls this a detached HEAD.
  3. A remote-tracking name, such as origin/master, is not a branch name. Its full form starts with refs/remotes/ rather than refs/heads/.
  4. If you tell git checkout to check out a commit, but identify it by something other than a branch name, Git will—if the checkout succeeds, that is—produce the detached HEAD state described in point 2. (You can also produce this same state with a branch name, using git checkout --detach.)

The git symbolic-ref HEAD command answers only the first question; git rev-parse HEAD mostly answers the second, but can be told to answer the first too / instead.

Source Link
torek
  • 502.5k
  • 71
  • 780
  • 899

The premise of your question is wrong:

A simple git checkout <argument> is impossible because it would not check out remote branch the way git checkout origin/<argument> would.

It's important to realize several interlocking things here about Git:

  1. There is always—well, almost always—a current commit, which you can use the word HEAD to find.
  2. There is not always a current branch, but if there is, it's a branch name, i.e., a reference whose full name has the form refs/heads/name. The same word—HEAD, in all capital letters, finds that branch name. If there isn't a current branch name, Git calls this a detached HEAD.
  3. A remote-tracking name, such as origin/master, is not a branch name. Its full form starts with refs/remotes/ rather than refs/heads/.
  4. If you tell git checkout to check out a commit, but identify it by something other than a branch name, Git will—if the checkout succeeds, that is—produce what Git calls a detached HEAD state. (You can also produce this same state with a branch name, using git checkout --detach.)

The consequence of point 4 above is that git checkout origin/name results in a detached HEAD, the same way that git checkout hash-ID would.

This means your script can just use git checkout <argument>, as it will do the same thing—produce a detached HEAD—if the argument is a hash ID or if it is a remote-tracking name like origin/develop.

Note, however, that if we change this statement to read:

A simple git checkout <argument> is unsuitable because it would not first create, then check out, a local branch based on an existing remote-tracking name, the way git checkout <argument minus the leading origin/ part> would.

we get a true statement: git checkout develop will create a new (local) branch named develop using the name origin/develop (provided, of course, that local develop does not exist yet). However, there's no obvious issue with just allowing <argument> here and having the user provide develop as the name:

#! /bin/sh
git fetch && git checkout "$@"

for instance.

Side notes

There is an interesting consequence of points 1 and 2 here, which is that asking what's the value of HEAD at the moment is really asking one of two different questions:

  • Is HEAD attached to a branch? If so, which branch?
  • What is the hash ID of the current commit?

The git symbolic-ref HEAD command answers only the first question; git rev-parse HEAD mostly answers the second, but can be told to answer the first too.


In point 1 above, the almost is there for a particular reason. Imagine you have just created a new, totally-empty repository. There are no commits in this repository, so which commit is the current commit?

This situation is problematic for Git. You're on a branch, namely master, that doesn't exist. Git calls this an orphan branch or a branch yet to be created (depending on which part of Git is doing the calling). The way Git handles this is to store the branch's name into .git/HEAD, without actually creating the branch itself in the reference database. When you make a new commit, that creates the branch itself, and now the problem is resolved: you're on the branch, which identifies the one new commit just made, which is the current commit, so HEAD names both the current commit and the current branch.

(Git can re-create this slightly distressed situation on demand, using git checkout --orphan, which writes a new branch's name into HEAD without actually creating the new branch.)