Besides VonC's answer (and the upcoming change in Git 2.23), it's worth noting a few more items.
Because git checkout
does multiple different things, it's inherently confusing.
One of git checkout
's jobs is to populate the index and work-tree based on the target commit. It will do this whenever it is allowed and necessary.
Another is to change the branch name recorded in HEAD
, or set up HEAD
as a detached HEAD at the specified commit. It will do this whenever necessary (provided the first part allows the checkout operation).
For git checkout
, it will do the second operation based on the branch name or commit specifier argument you give it. That is, suppose we have some shell variable $var
set to some non-empty but sensible word: it might be set to master
, or maybe master^{commit}
or a23456f
or origin/develop
or something along these lines. In any case, we now run:
git checkout $var
What name or hash ID goes into HEAD
? Well, here's how git checkout
decides:
First, git checkout
tries to resolve the string we just gave it as a branch name. Suppose we gave it master
or develop
. Is that a valid, existing branch? If so, that's the name that should go into HEAD
. If the checkout succeeds, we will have switched branches, to that branch.
Otherwise, the string we just gave it isn't a branch name after all (even if it starts with one, as in master~1
for instance). Git will try—attempt—to resolve it into a commit hash ID, as if by git rev-parse
. For instance, a23456f
sure looks like an abbreviated hash ID. If it is one—if there's an object in Git's database with an ID starting with a23456f
—then Git makes sure that this ID names a commit, rather than some other object.1 If it's a commit hash ID, that's the hash ID that should go into HEAD
, as a detached HEAD. If the checkout succeeds, we will now be in detached HEAD mode, at the given commit.
If neither attempt works, git checkout
will next guess that maybe, $var
was meant to be a file name, and try to work that out.2 But we'll ignore this particular case here.
Many names that aren't branch names work fine here. For instance, origin/master
is extremely likely to be resolvable to a commit hash ID. If v2.1
is a valid tag, v2.1
can be resolved to a commit hash ID. In all of these cases—whenever the $var
result isn't a branch name already, but can be resolved into a commit hash ID—git checkout
will attempt to do a detached-HEAD checkout of that commit hash.
Once git checkout
has decided that you have asked to check out some particular commit, either as a branch name to stick into an attached HEAD, or as a commit hash ID to stick into a detached HEAD, then Git goes about determining whether this is allowed. This can get very complicated! See Checkout another branch when there are uncommitted changes on the current branch for detailed notes about whether and when it's allowed, and remember that --force
tells Git that it should do the checkout anyway, even if these rules wouldn't allow it.
The TL;DR, though, is that a raw hash ID is always a request to go into detached HEAD state. Whether it will result in a detached HEAD depends on that complicated "is the checkout allowed" test.
Note, too, that if you create a branch whose name could be a hash ID—such as cafedad
—things get a little weird sometimes. Any Git command that tries to use it as a branch name will succeed, because it is one. Any Git command that tries to use it as a short hash ID might succeed, because it might be a valid short hash ID!
Unless you create stupidly confusing branch names, this particular case is rarely a problem, because all well-written Git commands try branch-name before short-hash-ID. For illustration, I've created a deliberately stupid branch name using the first six letters of an existing hash that I found via git log
:
$ git branch f9089e 8dca754b1e874719a732bc9ab7b0e14b21b1bc10
$ git rev-parse f9089e
warning: refname 'f9089e' is ambiguous.
8dca754b1e874719a732bc9ab7b0e14b21b1bc10
$ git branch -d f9089e
Deleted branch f9089e (was 8dca754b1e).
Note the warning: f9089e
was treated as a branch name, as it parsed to 8dca754b1e874719a732bc9ab7b0e14b21b1bc10
. After deleting the stupid branch name, the short hash parses to the full hash again:
$ git rev-parse f9089e
f9089e8491fdf50d941f071552872e7cca0e2e04
If you made a branch name that accidentally works as a short hash—such as babe
, decade
, or cafedad
—you probably only type in the short name babe
or cafedad
when you mean the branch. If you mean the commit, you probably cut-and-paste the full hash ID with your mouse, or whatever.
The real danger here occurs when you create a branch and tag with the same name. Most Git commands tend to prefer the tag, but git checkout
prefers the branch. This is a very confusing situation. Fortunately, it's easy to fix: just rename one of the two entities, so that your branch and tag names don't collide.
(You can also mess with yourself by creating a branch name that is exactly the same as some existing full hash ID. This one is especially nasty as full hash IDs tend to take precedence over branch names, but again, git checkout
is an exception to this rule. So is git branch -d
, fortunately.)
1There are four types of objects in any Git repository: commits, trees, blobs, and annotated tags. Commit objects store commits. Tree and blob objects are mainly for Git's internal use, to store file names in a somewhat-directory-ish fashion and to store file data. Annotated tag objects are the trickiest: they store the hash ID of another object. Git can be directed to take such a tag and find the commit that the tag connects to. As a special complication, an annotated tag can instead ultimately lead to a tree or blob object, so some tags might not name commits after all—but typically, most tags end up naming a commit anyway.
If you use the git rev-parse
command, you can use that ^{commit}
suffix trick to tell Git: make sure the final object has type commit. If the immediate object has type annotated-tag, Git will "peel off" (follow to its destination) the tag to find its commit. If it doesn't find a commit—if it finds a tree or blob instead—git rev-parse
will spit out an error message and fail the parse. This is all designed to be exactly what's needed if you are writing your own fancy script to do something useful with commits.
(This "peeling" process repeats if needed, because the target of an annotated tag can be another annotated tag. The verb peel here is meant to remind one of peeling an onion: if you find another layer of onion, peel again. Eventually you'll find out what's at the center of the onion. :-) )
2Note that the expansion from $var
to whatever $var
was set-to is done by the shell (e.g., by bash), not by Git. That doesn't matter right here because of the constraints I placed on what can be in $var
, but in more complicated cases, it does.