Should I worry if my head got detached? - The story of Git, part two

What does it mean when your head is detached? Will it hurt? In the case of git - it is not that bad, and no, it will not hurt you. Let's find out!

If your head, god forbid, got detached, you wouldn't have with what to worry about anything, right? Well, in Git, this is a lot more different thing, and it is not a problem, even though git "screams" at you with a bunch of warnings. I will show you what this means, how to fix it, and a bit more about it.

This is a second article in the series about Git, and with it we will cover head, branches, and tags. If you want to start from the beginning, checkout the first part of the story where we go through some of the git internals.

Unlike some warm-up videos, we will not start from the top-down (head-toes) approach, we will start with branches first, and then continue on explaining tags and in the end, last but not the least - the head.

What is a branch? To put it simply - it is a named reference to a certain commit. And that is just it. The data about branches is stored in .git/refs/heads directory. This directory contains the file with the branch name and the content of that file is the commit hash that it points to. It will look something similar to the below snippet:

$ ls .git/refs/heads/
master

$ cat .git/refs/heads/master 
e7906f8376b35321d19306260cdbf38f3b071ae8

$ git cat-file -p e7906f8376b35321d19306260cdbf38f3b071ae8
tree fb4e6dfcd4e11cdf2391b817d1e0a463e9dbe2dd
parent 6f4b9fdb4169e8f906f58670fd73a8801a5c0473
author Test <test@example.com> 1644596933 +0000
committer Test <test@example.com> 1644596933 +0000

my second commit

That is why creating branches in git is cheap. Cheap in a sense of resources that are used in order to create one - just a text file with a commit hash in it, not the snapshot of the whole working directory, for example. You can see the example below.

$ cat .git/refs/heads/master 
e7906f8376b35321d19306260cdbf38f3b071ae8

$ git branch
* master

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

$ cat .git/refs/heads/new-branch 
e7906f8376b35321d19306260cdbf38f3b071ae8

What happens when we add a commit to the new-branch? Let's see.

$ cat .git/refs/heads/new-branch 
e7906f8376b35321d19306260cdbf38f3b071ae8

$ echo "Hello from test!" >> test.md
 
$ cat test.md 
Hello from test!

$ git add test.md
$ git commit -m "commit on new-branch"
[new-branch 6dd7fdc] commit on new-branch
 1 file changed, 1 insertion(+)
 create mode 100644 test.md

$ cat .git/refs/heads/new-branch 
6dd7fdc78ebdf7a3d4447cc19709a1d1a8805155

From the output above we can see that the .git/refs/heads/new-branch file was updated with the new hash commit. That now means that the new-branch has diverged from main for one commit.

Deleting a branch is also cheap in git, however, if we would try to delete new-branch, git will raise a warning and not let us. We can always force it and don't care about the warning, but why will it not let us in the first place?

Because when deleting a branch new-branch, the commit with hash 6dd7fdc78ebdf7a3d4447cc19709a1d1a8805155 will become orphaned. Wait, what is orphaned commit now? To put it simple - it is a commit that has no reference. And what will happen to that orphaned commit if we would end up deleting the branch? Nothing, at first at least. It would remain orphan for around 30 days and then it will be picked up by git garbage collection and it will be deleted, and lost forever. Having orphaned commits is not a problem per-se, but they can be deleted at some point, and without reference they are not really easy to track, especially because of the hash value not being so readable. There is a way to track that, but we will write about it later.

Hopefully, from my rambling above, you've understood how branches work. So, let us know continue to tags and what they represent.

Similar to branches, tags also point to a specific commit, however, unlike branches - tags do not move! That means, when we switch to a new branch, we can commit to it, and the branch will move to that commit. That will happen with each commit. However, if we tag a commit with a certain value - e.g. v1.0.0, that means that the tag will not move from that commit. You can say that tags are somewhat like immutable branches. They are saved in the .git/refs/tags directory. Here is just an excerpt from the git log command and how tags are represented there.

$ git log
commit 27cbac466f04f6d7bde9e2ac61e1da6f03861f45 (HEAD -> new-branch)
Author: Test <test@example.com>
Date:   Mon Feb 14 11:03:23 2022 +0000

    Another commit to a branch

commit 6dd7fdc78ebdf7a3d4447cc19709a1d1a8805155 (tag: v1.0.0)
Author: Test <test@example.com>
Date:   Mon Feb 14 10:47:26 2022 +0000

    commit on new-branch

commit e7906f8376b35321d19306260cdbf38f3b071ae8 (master)
Author: Test <test@example.com>
Date:   Fri Feb 11 16:28:53 2022 +0000

    my second commit

commit 6f4b9fdb4169e8f906f58670fd73a8801a5c0473
Author: Test <test@example.com>
Date:   Fri Feb 11 16:27:26 2022 +0000

    my first commit

You can see how with the new commit the branch moved, however, tag v1.0.0 stayed on the same commit. That's why tags are great for labeling a version for example. There are also some of the best practices for manipulating tags - you should never name a tag with the same name as your branch, and you should not move or delete a tag when you pushed the code to remote, it can mess up your repository.

And now let's talk about HEAD and how to lose it in git!

HEAD is also a pointer of some sorts - it points to a checked out branch, tag or commit. It is sort of like our compass. When we are on a branch, the .git/HEAD file contains the reference to that branch, for example - ref: refs/heads/new-branch. However, if we'd checked out a certain commit, and not a branch or tag, that file gets updated and it contains a hash of the commit we are on. And that is when we get the famous You are in 'detached HEAD' state. message, or maybe a scream from git, so you might act like - go back, go back, go back! Undo, undo, undo!

I remember my first time when I saw this, years ago, I thought I messed up everything! I was really stressed, because I linked detached head to something bad (sorry for being a living organism with a head). Then I quickly returned to the main branch, or maybe I've even cloned a repo in a new directory, as we have all done at some point and saw that nothing was messed up. Then, quick web search told me that this is nothing to worry about, and I'm now here to let you know that it really isn't something you should worry about.

The reason git "screams" at you is because anything you change and commit, it will end up orphaned. So git advises you to, if you end up adding some commits, move them to a different branch. The well-known warning looks somewhat like below snippet.

$ git checkout 6f4b9fdb4169e8f906f58670fd73a8801a5c0473
Note: switching to '6f4b9fdb4169e8f906f58670fd73a8801a5c0473'.

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 switching back to a branch.

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

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 6f4b9fd my first commit

So what should you do if you end up in detached HEAD state? First - don't panic, second - either return to the branch you've been or if you want to add changes, just read the git warning slowly, even though it might feel terrible, it really is not. Git will show you how to "fix" things. And that's it!

Before I finish, there is one last thing I want to mention, or to give an answer to (in case you were wondering) - what to do in case you've deleted a branch with some unmerged commits or if you've commited something while in detached HEAD state and you've lost track of those commits? reflog to the rescue!

You can look at reflog like a blue dot on a gps, it shows you where you are and how you got there. It is a log which updates every time the HEAD moves. The log is structured as a last in - first out. That means - entries are from the newest to the oldest. You can go to reflog whenever you think you've messed something up, want to recover orphaned commit (if not deleted), or recover a deleted branch. Every clone of the repository has it's own reflog so you can trace your steps from the moment you've cloned the repo, up until the present. Pretty cool, isn't it?! Here is the output of how the reflog looks.

$ git reflog
e7906f8 (HEAD -> master) HEAD@{0}: checkout: moving from 27cbac466f04f6d7bde9e2ac61e1da6f03861f45 to master
27cbac4 HEAD@{1}: checkout: moving from master to 27cbac4
e7906f8 (HEAD -> master) HEAD@{2}: checkout: moving from new-branch to master
27cbac4 HEAD@{3}: checkout: moving from 6f4b9fdb4169e8f906f58670fd73a8801a5c0473 to new-branch
6f4b9fd HEAD@{4}: checkout: moving from new-branch to 6f4b9fdb4169e8f906f58670fd73a8801a5c0473
27cbac4 HEAD@{5}: commit: Another commit to a branch
6dd7fdc (tag: v1.0.0) HEAD@{6}: commit: commit on new-branch
e7906f8 (HEAD -> master) HEAD@{7}: checkout: moving from master to new-branch
e7906f8 (HEAD -> master) HEAD@{8}: commit: my second commit
6f4b9fd HEAD@{9}: commit (initial): my first commit

As you can see, it shows my movements in the repository. Really useful option!

And we finally reached the end of the blog post. At last - some of you might think. Well, I thank you for sticking with me until the end of this post and I'm looking forward to see you in the next one!