How to Work with Detached HEAD State and Navigate with git Commits
TLDR: You can use commits to move around in your project’s history, and moving to a specific commit will place you in a detached HEAD state. A detached HEAD state means that git’s pointer points towards a specific commit instead of a branch and requires a new branch to save any work done from the detached HEAD state.
A few weeks ago I was putting the finishing touches on some changes I had made for a ticket, and was getting ready to push the changes up to GitHub. I had been working on this issue for a while and had already pushed a dozen commits to my branch, but had not had my continuous integration tests running. None of my changes had been checked against my test suite. This was a huge problem.
Before pushing my latest commits, I decided to run my tests locally and found that certain files had +60% of their tests failing. This was bad, and because I had not been testing on each commit, I was not sure which of the dozen commits was the cause of my failing tests.
At this point I thought my only choices to get my tests working were to look through my current code, or to manually revert the code for each commit until I found my error. Either of these methods would have taken a long time. Luckily I found another way to fix my broken code.
Commits to the Rescue
Not wanting to commit to one of these two options, I decided to do a little digging around git. This mistake led me on a deeper dive into how git worked and some lesser known ways to move around in version control systems.
Using git is an amazing way to keep track of your progress and allow multiple developers to work on the same codebase at once. Version control is great for telling and retelling the story of how software is developed.
Git runs on the idea of commits, or copies of the project that you save at varying points. This string of commits lives on a branch, usually called master. If I wanted to see what had changed on master I could look at any of the commits to see the differences between my selected commit and those previous to it.
This singular line of history is great, but git gets interesting when you introduce the idea of multiple branches. To allow multiple users to work on different features for the same project, git introduced the idea of branches. Branches are copies of the project that run parallel to the master branch and allow developers to make changes to the project without immediately changing the main project.
If I wanted to work on a new project feature, and another developer wanted to work on a bug, we could each create separate branches and work on our task without effecting each other or the live project.
While the idea of switching between branches is well known, another way to move around a project is by checking out commits. Commits are the snapshots of our project and show what the project looked like at any given point in time. Thus they are the perfect tool to be able to see where something worked, or in my case stopped working.
To switch to a specific commit you can get the commit’s unique hash value found on Github (or from your terminal using “git log”). Once you have this hash, run the following command in your terminal and your workspace will now be set to the requested commit:
You are now looking at what the project looked like directly after this commit was made. Yes, you just time traveled. No, it’s not magic, just git. Now that I knew how to move to specific points of time in my project it was easier to run my tests on each commit to find exactly where my changes had broken my code. However, by checking out a commit I started getting messages about a detached HEAD.
What is a Detached HEAD?
If you’ve ever seen a message in your terminal about working with a detached HEAD state, know that you haven’t broken anything. Git uses HEAD as a pointer to keep track of where you are working in a project. HEAD usually points to the branch you are on, and the branch usually points to your most recent commit. As you add more commits the branch pointer will move to point at your most recent commit and because HEAD points to your branch, it will also point at this most recent commit.
When we checkout a specific commit on a branch we move our pointer, HEAD, from it’s normal position on the branch to the specific commit. This leads us to having a detached HEAD as we have separated our pointer from where it would usually be.
Is it broken? No, it is just not pointed to where it usually is. Going to specific commits and operating in a detached HEAD state can be great for development as you can run tests at different points in time, and make and discard experimental changes. If you would like to see some more of what you can do check out the reference article in my notes.
When you are done with your work in a detached HEAD state, you can either discard all changes by checking out a branch, or create a new branch to commit your changes. Because you working in a branch, instead of the end of a branch, it is not possible to add the commits directly to your current branch so you have to branch off your branch and merge this new branch to keep the changes.
Final Thoughts
While the new methods mentioned above are great for exploring and fixing broken code, all of my issues could have been prevented by getting my CI tests to run on every commit. This would have made it clear the exact moment my tests failed. However, everything is clearer looking back, and this was not too painful of an experience to learn more about the intricacies of git. Leave a comment if you’ve used git in any less conventional ways to fix problems in your codebase!
Notes