Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Git Tip of the Week: Detached Heads

2011, git, gtotw

This week’s Git Tip of the Week is about detached heads. You can subscribe to the feed if you want to receive new instalments automatically.


Today’s topic is the subject of detached heads. They’re not as bad as they sound, and they don’t involve a guillotine in any way.

Heads

Since a git repository is a tree-of-commits, with each commit pointing to its ancestor(s), it is possible to directly address a single commit by means of its commit hash. Not only that, but it’s possible to record this hash in a variety of different systems; Twitter, E-mail, Bugzilla etc.

Both Git branches and Git tags are merely links to an item, by commit hash, in the repository. Creating a hundred tags (or branches) is tantamount to creating a hundred pointers, and is one of the reasons why Git is so blazingly fast.

Whilst tags are (generally) immutable, branches are not. Each time a commit is made on a branch, the pointer (reference) is updated to point to the newest commit. Thus, three commits on branch involves three modifications to the branch pointer (as well as the corresponding entries being added into the repository for the content).

These pointers are stored in the .git/refs subdirectories. Tags are stored in .git/refs/tags and branches are stored in .git/refs/heads. If you look at any of the files, you’ll find each tag corresponds to a single file, with a 40-character commit hash.

The point for this post is that branches are also known as heads. When you have a master branch, there’s a file refs/heads/master which is a pointer to where the current branch is at that point.

Detached Heads

So, if a head is synonymous with a branch, what does that make a detached head? Well, it’s simply a commit hash which isn’t pointed to by a tag or a branch. So, whenever you have checked out a non-referenced head, you end up with a detached head. Perhaps an example is called for:


$ git init example
Initialized empty Git repository in example/.git
(master) $ touch file
(master) $ git add file
(master) $ git commit -m "Initial"
[master (root-commit) 123be6a] Initial
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file
(master) $ touch other
(master) $ git add other
(master) $ git commit -m "Second"
[master 5a11d1c] Second
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 other
(master) $ git log --oneline
5a11d1c Second
123be6a Initial
(master) $ git checkout HEAD^
Note: checking out 'HEAD^'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at 123be6a... Initial
((123be6a...)) $ git log --oneline
123be6a Initial

What we’ve done here is to create a repository with two commits, then back out one version. (The HEAD^ checks out HEAD’s parent.) The master branch is still pointing to the second commit (5a11d1c) but we’re looking at effectively an unnamed commit.

The Git checkout contains a .git/HEAD, which normally points (indirectly) to a ref. In the case of ‘detached head’ mode, the .git/HEAD file contains the commit hash itself:


((123be6a...)) $ cat .git/HEAD
123be6a76168aca712aea16076e971c23835f8ca
((123be6a...)) git checkout master
Previous HEAD position was 123be6a... Initial
Switched to branch 'master'
(master) $ cat .git/HEAD
ref: refs/heads/master

Working with (or on) a detached head isn’t a problem. This occurs when you are dealing with bisects, or if you want to simply check out a specific version of a previous commit. There’s also nothing to stop you working on this unnamed branch, either; you can keep going and committing as long as you want.

Bear in mind, however, that finding a commit is dependent on what you do with that hash. Typically, you will create a new branch (say, it’s a bug fix for a previously released bit of code), or you end up tagging it with a hot fix identifier. There’s always the reflag which you can use to get back to your commit if you end up changing out of a detached head. Just remember that the periodic git gc will run, and will clean up commits that aren’t referenced (directly or indirectly) by a tag or a branch.


Come back next week for another instalment in the Git Tip of the Week series.