git checkout <commit-hash> vs git checkout branch git checkout <commit-hash> vs git checkout branch git git

git checkout <commit-hash> vs git checkout branch


A git checkout <commit-hash>, Prepare to work on top of <commit>, by detaching HEAD at it (see "DETACHED HEAD" section), and updating the index and the files in the working tree.

While a git checkout <branch> does a switch: it prepares for working on <branch>, switch to it by updating the index and the files in the working tree, and by pointing HEAD at the branch.

This is confusing.

Mark Longair documented that confusion in "Why is the git command to switch branches named “git checkout”?"

He also wrote in May 2012: "The most confusing git terminology":

In CVS and Subversion “checkout” creates a new local copy of the source code that is linked to that repository.
The closest command in Git is “git clone”.
However, in git, “git checkout” is used for something completely distinct.
In fact, it has two largely distinct modes of operation:

  • To switch HEAD to point to a new branch or commit, in the usage git checkout <branch>. If <branch> is genuinely a local branch, this will switch to that branch (i.e. HEAD will point to the ref name) or if it otherwise resolves to a commit will detach HEAD and point it directly to the commit’s object name.
  • To replace a file or multiple files in the working copy and the index with their content from a particular commit or the index.
    This is seen in the usages: git checkout -- (update from the index) and git checkout <tree-ish> -- (where <tree-ish> is typically a commit).

In my ideal world, these two modes of operation would have different verbs, and neither of them would be “checkout.

Well...That is why Git 2.23 (Q3 2019) will split checkout into:

  • git restore which updates the working tree (and possibly the index)
  • git switch which can switch branches, or detach one if requested, in order for all new commits to be added to the tip of this branch.


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 f9089ewarning: refname 'f9089e' is ambiguous.8dca754b1e874719a732bc9ab7b0e14b21b1bc10$ git branch -d f9089eDeleted 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 f9089ef9089e8491fdf50d941f071552872e7cca0e2e04

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.


Here is a simple explanation:

HEAD is a file located at .git/HEAD which keeps Git informed about the branch that should be advanced in the advent of new commits.

When a branch, like main is checked out it contains:

ref: refs/heads/main

For each branch Git also keeps a file in the refs/heads directory, for instance the file refs/heads/main for the main branch will be there.

This file contains the hash of the last commit on that branch, meaning the tip of the branch.

These two files so far inform Git on which branch should be advanced and what is the last commit on that branch.

By running git checkout <branch name>, the HEAD file is updated to contain that branch's name.

Thus running git checkout 235a6d8, makes the HEAD point to a specific commit instead of pointing to a specific branch, meaning that Git HEAD is detached.

In order to attach the HEAD again simply run git checkout <branch name> which will bring things back to the normal behavior.

The detached head state is useful in order to inspect the state of a project at specific points in time, for testing and bug finding/fixing.

There is more to what you can do in a detached HEAD state and you can see more about that in the docs.

You may also find it interesting to check terms heads and HEAD in the gitglossary.