Codementor Events

The Alternative Git Flow

Published May 11, 2023
The Alternative Git Flow

You are now in detatched HEAD state.

These words can strike fear into the hearts of developers and engineers everywhere. You know the story, one minute you are calmly working away on something, merging and rebasing changes from other branches. Having a great time, then all of a sudden you see these words. And you start to wonder “did I fat-finger!?”, “did my workstation glitch?”, “am I losing my head!?” (sorry I had to put the one in for the lolz).

However you ended up here, you might be thinking “what is this detatched HEAD state, and how do I get out of it?”.

The reason I know many people ask this question, is because a simple Google search of “git detatched head” will throw up dozens upon dozens of posts form various different sources, from engineers and developers asking this exact question. So I can confidently say, it’s a something which a lot of people panic about. But what if I told you, theres no need to be afraid of detached HEAD state? What if I told you, detatched HEAD state might actually be something beneficial to you? What if I told you that I work in detatched head state 99% of the time?

In this article, I aim to show the differences between working the “normal” way with git, and the…. unconventional way or using detatched HEAD state. And you never know, you might find yourself using detatched HEAD state more often, or at least not being so worried about it.

The Local Branches Flow

This is the most common way of using git. You have your repo (I’ll be using GitHub for these examples). You’ve cloned your repo locally onto your workstation so you have a copy of the main branch (or master). You create a new local branch and start doing some fun stuff.

user@MacBook-Pro Git % git clone git@github.com:user1/my-super-cool-project.git
Cloning into 'my-super-cool-project'...
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 7 (delta 0), reused 7 (delta 0), pack-reused 0
Receiving objects: 100% (7/7), done.
user@MacBook-Pro Git % cd my-super-cool-project
user@MacBook-Pro my-super-cool-project % git branch branch-a
user@MacBook-Pro my-super-cool-project % git checkout branch-a
Switched to branch 'branch-a'

From here, you start making your changes and when you are finished, you commit those changes to branch-a and push. Below I’ve added a new line into the notes.txt file, and I’m ready to commit that change.

user@MacBook-Pro my-super-cool-project % git diff
diff --git a/project/notes.txt b/project/notes.txt
index e69de29..e8e1ad1 100644
--- a/project/notes.txt
+++ b/project/notes.txt
@@ -0,0 +1 @@
+1. Clone repo, create a new branch, and checkout.

So I’m going to go ahead and add that file to my git changes, set my commit message and push. I’ve omitted some of the output below because if you are reading this, you are probably familiar with the git outputs. Note, that because this is the first time I am pushing this branch upstream, I need to configure the upstream origin, and git prompts me with the command to use. It is possible to set git to do this automatically using the global git config however I won’t talk about that here.

user@MacBook-Pro my-super-cool-project % g add project/notes.txt
user@MacBook-Pro my-super-cool-project % g commit -m "add my first note"
[branch-a 1cf751f] add my first note
 1 file changed, 1 insertion(+)
user@MacBook-Pro my-super-cool-project % g push
fatal: The current branch branch-a has no upstream branch.
To push the current branch and set the remote as upstream, use

 git push --set-upstream origin branch-a
...

user@MacBook-Pro my-super-cool-project % git push --set-upstream origin
 branch-a
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
...
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0

...

To github.com:user1/my-super-cool-project.git
 * [new branch] branch-a -> branch-a
branch 'branch-a' set up to track 'origin/branch-a’.

So we can see that after running the upstream origin command, we have a new remote branch created from our local branch-a great! We can repeat this process of course and create other branches etc. This is the “standard” git flow.

Detatched HEAD Flow

Using detatched HEAD requires a slightly different set of commands, which in some cases are longer, but of course you can set up aliases for these commands which you likely have already done for your normal git workflow. Lets go through the same process as above, but using the detatched HEAD method.

user@MacBook-Pro Git % git clone git@github.com:user1/my-super-cool-project.git
Cloning into 'my-super-cool-project'...

...

Receiving objects: 100% (11/11), done.
user@MacBook-Pro Git % cd my-super-cool-project
user@MacBook-Pro my-super-cool-project % git checkout origin/main
Note: switching to 'origin/main'.

You are in 'detached HEAD' state. You can look around, make experimental
changes...

...

HEAD is now at 21db179 initial commit
user@MacBook-Pro my-super-cool-project %

As you can see above, the command I used a command git checkout origin/main which moved me into detatched HEAD state. We can verify this by looking at the branches. You can see that I have a local main branch but the branch I am currently checked-out with is detatched at origin/main.

user@MacBook-Pro my-super-cool-project % g branch
* (HEAD detached at origin/main)
  main

Now, lets make a change and commit it, in this case I changed a parameter in the versions.tf file from True to False.

user@MacBook-Pro my-super-cool-project % git diff
diff --git a/terraform/versions.tf b/terraform/versions.tf
index 8abfb6e..07e07b0 100644
--- a/terraform/versions.tf
+++ b/terraform/versions.tf
@@ -23,7 +23,7 @@ provider "aws" {
       TerraformManaged = "true"
       Environment      = "dev"
       Contact          = "admin@awesomeorg.biz"
-      TrainingResource = "True"
+      TrainingResource = "False"
     }
   }
 }

user@MacBook-Pro my-super-cool-project % g add terraform/versions.tf
user@MacBook-Pro my-super-cool-project % g commit -m "change tag value"

[detached HEAD e5f0f95] change tag value
 1 file changed, 1 insertion(+), 1 deletion(-)
user@MacBook-Pro my-super-cool-project % git push origin HEAD:refs/head
s/branch-b
Enumerating objects: 7, done.

 ...

remote:      https://github.com/user1/my-super-cool-project/pull/new/branch-b
remote:
To github.com:user1/my-super-cool-project.git
 * [new branch]      HEAD -> branch-b

As you can see above, my push command is different from that when working with local branches. My push command was git push origin HEAD:refs/heads/branch-b. What you are actually doing here is telling git exactly where to push your changes on the remote side. There is actually a structure to the locations of remote origins, and you can configure this, but by default this is where all branches are created. Interestingly, if you make a typo like HEAD:refs/headss/branch-b, your changes just dissapear because that location doesn’t exist. So you will need to push again with the correct syntax. This is one reason why it is handy to set up command aliases. You will notice that, there was no need to set an upstream origin, and my new remote branch has been imediately created as a result of my push command, which we can verify by checking the repo in GitHub

github1.webp

So what next?

OK, so now you’ve seen the flow for making changes in detached HEAD state. SO what’s the point of doing it this way? Well one big advantage is that you don’t need to maintain local branches. Here’s what that looks like:

  • user 1 creates local branch-a and makes changes
  • user 1 commits changes and pushes to remote branch-a
  • user2 pulls branch-a from the remote and makes some changes, commits and pushes back to the remote branch-a
  • user1 checks out branch-a and does a git pull to obtain the changes made by user2 and then carries on working.

But what happens if user1 forgets that other people might be making changes to this branch? Or maybe they arent expecting anybody else to be working on this branch at all. So user1 carries on working on their local branch, commits and tries to push to the remote branch-a without realising the another user has already pushed changes of their own. Now, user1 finds themselves needing to re-pull branch-a locally and start merging changes, or potentially resolving conflicts.

Detatched HEAD state can help!

When using detatched HEAD state, you don’t need to worry about other people making changes to remote branches so much. This is because you are working directly from a remote branch at a certain commit where your HEAD is pointed at. So, instead of pulling changes from a remote branch, what you do instead is simply update your HEAD pointer so that it is pointing at the latest commit on the remote branch which looks like this:

git remote update -p < this command will update your head to point at the latest commit on the remote branch of which you are currently checked-out. Lets see that in action:

github2.webp

I am using scrteenshots here because the code block formatting on medium doesn’t suficciently highlight the difference between the local and remote HEAD lines. You can see here that my local HEAD is pointing at my latest commit on branch-b where I changed the tag value, and the remote HEAD is pointing at the initial commit on the main branch, as is my local main branch. Now lets merge both our recent changes into main from both branches and update our pointers:

github3.webp

You can see both branch-a and branch-b were merged into main and my local HEAD is now in-line with the remote HEAD or origin/HEAD. Notice that my local main branch is still at the initial-commit commit, because I haven’t done my local branch maintenance and checked out main, and done a git pull. But since we don’t care about local branches anymore, we don’t need to worry about it so we can delete it.

git branch -d main

Ahh — thats better, now we only need to look at where our local HEAD and the remote HEAD are at (blue and red respectively).

github4.webp

Rebasing

One of the big time savers of using detatched HEAD state, is when you want to pull the latest changes from another branch into the work you are currently doing. In the local branch workflow it looks like this:

git checkout some-other-branch

git pull

git checkout my-current-branch

git rebase some-other-branch

Now lets see what it looks like when working in detached head state:

git remote update -p

git rebase origin/some-other-branch

Boom! 50% less commands every time! That’s because you are already working on branch origin/my-branch and you’ve just told git to update itself about where the remote HEAD ponters are at for every remote branch (not just the one you have checked out). So when you do this you can then simply tell git to rebase your branch off of any remote branch, from the latest commit, glorious!

To sum up

So, in my case I work in detatched HEAD state for the following reasons:

  • I can quickly create new remote branches with git push origin HEAD:refs/heads/new-branch-name.
  • I never have to worry about pulling the latest changes into my branch because I can simply git remote update -p and git rebase origin/other-branch.
  • I never end up in a situation where I have dozens of local branches on my workstation, causing confusion and data blindness. I have enough confusion and data blindness of my own brains making, without adding to it needlessly.
  • Related to the last point, I never have to do any local branch management, like deleting old branches when they have been merged, or creating new branches before I can start making changes. Everything is taken care of for me, because I am always working with my HEAD pointing at the remote state of the repository. If a branch is deleted on the remote, as soon as I next update my pointers, that branch will dissapear from my workstation’s list of branches without me needing to do anything.

Hopefully this gives some insight into what detatched HEAD state is, and how to effectively use it instead or being avoid it. I personally really prefer using git this way and I would encourage anyone to at least give it a try for a week and see how you find it. You never know, you might find yourself detatching your head more often!

Discover and read more posts from Matthew Love
get started