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.
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.
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.