1. Introduction

The Git versioning system has many features that enable precise selection of commit tree parts. One such feature is Git tags. In essence, although it can be a separate object, a tag is just an alternative name for a given commit.

In this tutorial, we explore ways to use tags as indicators of the repository parts we want to work on. First, we look at a common way to display the commit history. After that, we turn to a brief overview of the Git commit tree. Next, we refresh our knowledge about a special state of the commit structure. Finally, we explain the connection between that and Git tags.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. Unless otherwise specified, it should work in most POSIX-compliant environments.

2. Git Log and a Dog

Before we continue, let’s introduce a concise way to show the commit history of a repository.

Specifically, we use the log subcommand of git with several switches, colloquially known as a dog

  • –all: include all refs (references) and HEAD as commits
  • –decorate (–decorate=short): show refs and tags next to commit
  • –oneline (–pretty=oneline –abbrev-commit): minimalistic output with a shortened commit identifier
  • –graph: include a text-based graphical view on the left

By using this method of commit listing, we get a short but organized overview of the current branch or repository history.

3. Git Commit Structure

The Git HEAD ref (reference) is a pointer to the latest commit.

Let’s see an example:

$ git log --all --decorate --oneline --graph
* 58404cc (HEAD -> master) late modifications
* 84c3441 major modifications
* 1fb0c0b minor modifications
* 2850700 init commit

Here, we have 58404cc as the last commit of the default master branch, which is currently checked out. Thus, in this case, HEAD is -> pointing to the 58404cc commit, but we also see the connection between it and the history of commits that follow.

Let’s visualize this:

                                    | HEAD  |
(initial)                           |master |
+-------+   +-------+   +-------+   +-------+
|2850700|<--|1fb0c0b|<--|84c3441|<--|58404cc|
+-------+   +-------+   +-------+   +-------+

Now, we can create and check out a new branch from the initial commit:

$ git branch branch1 2850700
$ git checkout branch1

After several changes and commits, we end up with a new structure:

$ git log --all --decorate --oneline --graph
* 191f89b (HEAD -> branch1) branch1 restructuring
* 7b7561d init commit branch1
| * 58404cc (master) late modifications
| * 84c3441 major modifications
| * 1fb0c0b minor modifications
|/
* 2850700 init commit

Visually, we can now see the new branch and the HEAD relocation:

(initial)                           |master |
+-------+   +-------+   +-------+   +-------+
|2850700|<--|1fb0c0b|<--|84c3441|<--|58404cc|
+-------+   +-------+   +-------+   +-------+
    ^
    |       +-------+   +-------+
    +-------|7b7561d|<--|191f89b|
            +-------+   +-------+
                        |branch1|
                        | HEAD  |

It’s very important to note that HEAD is over the last commit of the current checked-out branch, where we can also see the branch ref (name).

For clarity, let’s check out master and see the tree again:

$ git log --all --decorate --oneline --graph
* 191f89b (branch1) branch1 restructuring
* 7b7561d init commit branch1
| * 58404cc (HEAD -> master) late modifications
| * 84c3441 major modifications
| * 1fb0c0b minor modifications
|/
* 2850700 init commit

At this point, we have a fairly standard commit tree with a main and secondary branch.

4. Detached HEAD State

So far, we understood that HEAD points to the most recent commit of the current checked-out branch. So, what happens if we don’t check out a branch?

To demonstrate, let’s check out a specific commit:

$ git checkout 7b7561d
Note: switching to '7b7561d'.

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 7b7561d init commit branch1

Thus, we can see a warning, telling us that the HEAD is detached and briefly what that means. Effectively, if we don’t make a new branch from the commit, we may lose any further commits from this state when switching back to a branch.

Let’s get a better view:

(initial)                           |master |
+-------+   +-------+   +-------+   +-------+
|2850700|<--|1fb0c0b|<--|84c3441|<--|58404cc|
+-------+   +-------+   +-------+   +-------+
    ^
    |       +-------+   +-------+
    +-------|7b7561d|<--|191f89b|
            +-------+   +-------+
             [HEAD]     |branch1|

Naturally, we can detach the HEAD at any commit that’s not a branch and create a branch from any commit that HEAD currently points to. By creating a branch at any point in a detached HEAD state, we retain the changes since the detachment.

5. Git Tag Checkout

Since tags are another way to identify a specific commit, checking out via a tag is effectively the same as doing so with a commit ID:

$ git tag br1tag 191f89b
$ git checkout br1tag
Note: switching to 'br1tag'.

You are in 'detached HEAD' state.[...]

Thus, after tagging a commit from the branch1 branch and then performing a checkout with it, the result is the usual detached HEAD message.

Importantly, even if we perform a checkout of the latest commit for a given branch, the HEAD is considered detached:

(initial)                           |master |
+-------+   +-------+   +-------+   +-------+
|2850700|<--|1fb0c0b|<--|84c3441|<--|58404cc|
+-------+   +-------+   +-------+   +-------+
    ^
    |       +-------+   +-------+
    +-------|7b7561d|<--|191f89b|
            +-------+   +-------+
                        |branch1|
                         [HEAD]

Let’s compare this with a regular branch checkout:

$ git checkout branch1
Previous HEAD position was 191f89b init commit branch1
Switched to branch 'branch1'

As expected, we don’t see a warning and the result is a regular HEAD:

(initial)                           |master |
+-------+   +-------+   +-------+   +-------+
|2850700|<--|1fb0c0b|<--|84c3441|<--|58404cc|
+-------+   +-------+   +-------+   +-------+
    ^
    |       +-------+   +-------+
    +-------|7b7561d|<--|191f89b|
            +-------+   +-------+
                        |branch1|
                        | HEAD  |

In other words, the HEAD state depends on the way we perform checkouts.

6. Summary

In this article, we talked about Git HEAD detachment during commit ID, tag, and branch checkouts.

In conclusion, the automatic Git HEAD state is mainly dependent on the way we perform a checkout and the objects or metadata involved.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.