HEAD
When we make commits to a Git repo, it tracks each of those commits in a log that you can visit. Let’s take a look at that now.
You can get the commit log by typing git log
.
Let’s say I’m in a repo with a single commit where I’ve just added a file with the commit message “Added”.
$ git log
produces:
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2 (HEAD -> main)
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
If I make another commit, we get a longer log:
commit 5e8cb52cb813a371a11f75050ac2d7b9e15e4751 (HEAD -> main)
Author: User Name <user@example.com>
Date: Thu Feb 1 12:36:13 2024 -0800
More output
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
Notice that the most recent commit entry is at the top of the output.
There are a few things to notice in the log:
Also, we have those huge hex16 numbers after the word commit
.
This is the commit ID or commit hash. This is universally unique number that you can use to identify a particular commit.
Normally you don’t need to know this, but it can be useful for going back in time or keeping track of commits in multi-developer projects.
We also see a bit at the top that says (HEAD -> main)
. What’s that about?
HEAD
ReferenceWe’ve seen that each commit has a unique and unwieldy identifier like this:
5a02fede3007edf55d18e2f9ee3e57979535e8f2
Luckily, there are a few ways to refer to commits with more-human symbolic names.
HEAD
is one of these references. It indicates which branch or commit you’re looking at right now in your project subdirectory17. Remember how we said you could go look at previous commits? The way you do that is by moving HEAD
to them.
We haven’t talked about branches yet, but the
HEAD
normally refers to a branch. By default, it’s themain
branch. But since we’re getting ahead of ourselves, I’m going to just keep saying thatHEAD
refers to a commit, even though it usually does it indirectly via a branch.So this is a bit of a lie, but I hope you forgive me.
Some terminology: the Git subdirectory you’re looking at right now and all the files within it is referred to as your working tree. The working tree is the files as they appear at the commit pointed to by HEAD
, plus any uncommitted changes you might have made.
So if you switch HEAD
to another commit, the files in your working tree will be updated to reflect that.
Importantly, the data in the files in the working tree might differ from the data in the files at the current commit pointed to by
HEAD
. This happens when you’ve modified a file in the working tree but haven’t yet committed it.
Okay then, how do we know which commit HEAD
is referring to? Well, it’s right there at the top of the log:
commit 5e8cb52cb813a371a11f75050ac2d7b9e15e4751 (HEAD -> main)
Author: User Name <user@example.com>
Date: Thu Feb 1 12:36:13 2024 -0800
More output
We see HEAD
right there on the first line, indicating that HEAD
is referring to commit with ID:
5e8cb52cb813a371a11f75050ac2d7b9e15e4751
Again, that’s a bit of a lie. The HEAD -> main
means that HEAD
is actually referring to the main
branch, and that main
is referring to the commit. HEAD
is therefore indirectly referring to the commit. More on that later.
HEAD
Here’s my full Git log:
commit 5e8cb52cb813a371a11f75050ac2d7b9e15e4751 (HEAD -> main)
Author: User Name <user@example.com>
Date: Thu Feb 1 12:36:13 2024 -0800
More output
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
If I look at the files, I’ll see the changes indicated by the “More output” commit. But let’s say I want to go back in time to the previous commit and see what the files looked like then. How would I do that?
Maybe there were some changes that existed back in an earlier commit that had since been removed, and you wanted to look at them, for example.
I can use the git switch
command to make that happen.
Before you switch branches, you should be all committed with
git status
telling you everything is clean. If you’re not, make a commit or stash your stuff before your switch.
Let’s check out the first commit, the one with ID 5a02fede3007edf55d18e2f9ee3e57979535e8f2
.
Now, I could say:
$ git switch --detach 5a02fede3007edf55d18e2f9ee3e57979535e8f2
and that would work, but the rule is that you must specific at least 4 unique digits of the ID, so I could have also done this:
$ git switch --detach 5a02
for the same result.
And that result is:
HEAD is now at 5a02fed
Let’s take a look around with git log
:
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2 (HEAD)
Author: User <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
That’s all! Just one commit?! Where’s the second commit I made? Is it gone forever?!
No. Everything is fine. [Soothing image of a kitten sleeping in the sun.]
When you have HEAD
at a certain commit, you’re looking at the world as it looked at that snapshot in time. Future commits haven’t “happened” yet from this perspective. They are still out there, but you’ll have to change back to them by name. (It’s like a flippin’ time machine!)
Also, do you see anything different about that first line that reads (HEAD)
? That’s right: no main
to be seen.
That’s because the main
branch is still looking at the latest commit, the one with the “More output” comment. So we don’t see it from this perspective.
Furthermore, this means that HEAD
is no longer attached to main
. We call this state detached head. And git switch
doesn’t let you do that unless you mean it, which is why we have that --detach
in there. (And reattaching is easy: just switch to the branch you want to attach to.)
Remember earlier when I said it was a bit of a lie to say that
HEAD
points to a commit? Well, detached head state is the case where it actually does. Detached head state is just what happens whenHEAD
is pointing to a commit instead of a branch. To reattach it, you have to change it to point to a branch again.
Let’s reattach HEAD
to the main
branch. There are two options:
git switch -
: this switches to wherever we were before this, which, in this case, was main
.git switch main
: this explicitly switches to main
.Let’s try:
$ git switch main
Previous HEAD position was 5a02fed Added Switched to branch 'main'
Notice there was no --detach
in that git switch
! We’re reattaching the head, not detaching it, so we don’t have to tell Git we know what we’re doing.
Don’t worry if you forget the
--detach
. Git will tell you if you need it.$ git switch 5a02 fatal: a branch is expected, got commit '5a02' hint: If you want to detach HEAD at the commit, try again with the --detach option.
And now if we git log
, we see all our changes again:
commit 5e8cb52cb813a371a11f75050ac2d7b9e15e4751 (HEAD -> main)
Author: User Name <user@example.com>
Date: Thu Feb 1 12:36:13 2024 -0800
More output
commit 5a02fede3007edf55d18e2f9ee3e57979535e8f2
Author: User Name <user@example.com>
Date: Thu Feb 1 09:24:52 2024 -0800
Added
and our working tree will be updated to show the files as they are in the main
commit.
And you see the HEAD -> main
? The arrow means HEAD
is reattached to main
. (If HEAD
were detached at the same commit as main
, you’d see HEAD, main
.)
git checkout
In ye olden days before git switch
existed, there was a command to do all that stuff called git checkout
. git checkout
did a lot of things, and it still does. Because it does so much, the maintainers of Git have been trying to break some of that functionality into git switch
and other commands.
There are still times when you need to use
checkout
, but if your version of Git supportsswitch
, this isn’t one of them. Useswitch
if you can and skip this section.
But let’s redo the previous section by using just git checkout
instead of git switch
. Let’s try:
$ git checkout 5a02
and it says:
Note: switching to '5a02fede3007edf55d18e2f9ee3e57979535e8f2'.
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 5a02fed Added
Well, that’s a lot of scary stuff, but it’s just Git telling us that we’re now in detached head state. Which of course we are since we just detached the head from branch main
by switching to a specific commit hash.
And we can get back to the main
branch with:
$ git checkout main
You can also switch back with the aforementioned git switch
variants, but we’re pretending those don’t exist for this section.
HEAD
There are a couple shortcuts to get to commits that are earlier than HEAD
, like, “I want to switch to the 3rd commit before this one.”
Here’s a pretty useless example that we’ll start with:
$ git switch --detach HEAD
This moves HEAD
to where HEAD
was. That is, it moves it nowhere. (Though it does have the effect of detaching it from the branch.)
But what if I wanted to move to the commit right before where HEAD
is now? You can do it with caret notation like this:
$ git switch --detach HEAD^
And that gets you to the previous commit.
It’s apparently a little-known Fun Fact™ that you can commonly enter
@
instead ofHEAD
with these commands. For example, the previous command could be written:$ git switch --detach HEAD^ $ git switch --detach @^ # Same thing
Be aware that some shells might require you to put the
@
in quotes to get it to work right. But that can potentially save three keystrokes.
What if you wanted to get to the third-previous commit? You can add more carets!
$ git switch --detach HEAD^^^
Or the 10th previous commit!
$ git switch --detach HEAD^^^^^^^^^^
Or the 100th previous commit!
$ git switch --detach HEAD^^^^^^^^^^^^^^^^^^^^forget this
Typing all these carets is wearing me out. Luckily there’s another shorthand we have at our disposal with tilde notation. The two following lines are equivalent:
$ git switch --detach HEAD^^^ $ git switch --detach HEAD~3
After the tilde, you can just give the number of commits back you want to go. So back to my example:
$ git switch --detach HEAD~100 # Much easier
All this said, personally I usually just look at the log and go to a specific commit instead of counting back.
“But that’s just, like, my opinion, man.”
—The Dude