Recovering from a detached head

Posted on Thu 05 November 2015 in Digging through history

Like a French revolution, git provides many ways to detach your HEAD. But besides bad puns, what does that mean? And how do you recover from it?

Really quick introduction into git guts

Git really only stores 2 things: items of data, which it calls objects, and pointers to this data, which it calls refs. Every file, directory and commit is an object, every tag and branch is a ref.

But not all refs are equal. Most refs are a pointer from a name, say 'master', to an object, such as a commit. The exceptions to this rule are symrefs or symbolic refs. These are used very rarely, with one exception: the HEAD ref.

HEAD is a special ref in more ways than one. It by definition always points to the currently checked out commit. Usually not directly though, but as a symref: it points to a branch whose tip commit is currently checked out.

Let's use some git plumbing commands to show this:

$ git rev-parse --symbolic-full-name HEAD
refs/heads/master
$ git rev-parse refs/heads/master
7c3c37ba945276ca872217850ab8ceeb2e7249e5
$ git rev-parse HEAD
7c3c37ba945276ca872217850ab8ceeb2e7249e5

As you can see, HEAD actually points to the 'master' branch, and thus both HEAD and the master branch now point you to commit 7c3c37ba.

So how does head get detached?

If you check out anything that is not a branch (like a tag, or just commit sha1), HEAD will point directly to that commit. It no longer is a symbolic ref to a branch and has become detached. Robespierre would be proud of you.

Let's check out the second-to-last commit, HEAD^ is a nice shorthand to refer to it. For more of these shorthands, see the gitrevisions manpage.

$ 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 d3097d1... Enable travis tests

When you do this, git gives you a nice big warning that you now have a detached HEAD. It also gives you some nice advice as to what you can do to add new commits at this point. But if you were just looking and simply want to reattach your HEAD again, just check out a branch.

$ git checkout master

If you've added commits to your detached HEAD, git will warn you and tell you what to do to save those commits:

$ git checkout master
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  5c77598 foo

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> 5c77598

Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

More complicated situations

While checking out a non-branch is the only way to detach your HEAD, you don't always do this checkout yourself, so you may end up in a detached HEAD state without knowing it.

  • When bisecting, HEAD is always detached
  • During a rebase HEAD is detached
  • Submodules are almost always in a detached HEAD state

It's easy to forget you're in the middle of a bisect or rebase, and you may end up adding commits in a place where you don't want them. But don't worry, there's always a way out.

Recovering commits

The first thing to do to recover your commits is to point a branch to them to prevent git's garbage collection from removing them when you switch back to a branch.

When your current HEAD point to the most recent commit you want to keep, you can follow the advice above and create a new branch:

$ git checkout -b temp-branch
Switched to a new branch 'temp-branch'

But even when you've already moved away from your commits, for example by doing git checkout master, mistakenly thinking you had no commits to keep, you can still find them in the reflog:

$ git reflog
7c3c37b (HEAD -> master, tag: v2.5.1, origin/master, origin/HEAD) HEAD@{0}: checkout: moving from 5c7759821b9a52b63a6201488319abace9cfca09 to master
5c77598 HEAD@{1}: commit: Make the testsuite work with python 3
f351964 HEAD@{2}: commit: Python 3.4 compatibility
d3097d1 HEAD@{3}: checkout: moving from master to v2.1

According to that reflog, the commit I'm after is 5c77598, so let's attach a branch there.

$ git checkout -b temp-branch 5c77598
Switched to a new branch 'temp-branch'

If your commit cannot be found in the reflog, for instance because you have removed the reflogs, there is a reasonable chance the commit has been removed as well. But you can try recovering it with git fsck --lost-found. If the commit still exists, it will show up in .git/lost-found/commit.

Once you've pointed a branch to the commit(s) you want to keep, you can do the usual git things to review them (e.g. log -p, gitk), move the commits to their right place (w.g. merge, rebase, cherry-pick).

$ git rebase master
First, rewinding head to replay your work on top of it...
[detached HEAD f351964] Python 3.4 compatibility
Date: Wed Nov 4 18:45:19 2015 +0100
[detached HEAD 5c77598] Make the testsuite work with python 3
Date: Wed Nov 4 19:32:47 2015 +0100
$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git merge --ff-only temp
Updating 7c3c37b..5b18042
Fast-forward

And voilĂ , all your commits are now nicely part of your master branch.