Understanding HEAD and refs helps with reading git logs and using git commands.
HEAD is a ref. In git, a ref is a human readable reference to a thing.
You can have refs to git information stored in blobs, trees, and commits. A blob is an object representing a file. A tree is file hierarchy, or how files relate to each other. A commit is how you add code changes to a repository. You can also have refs to branches and remotes (e.g. where your code and commit history are stored). Each of these has a SHA or a unique identifier created with the SHA-1 hash algorithm.
When you use "main" instead of typing out a SHA in commands like
git checkout main, you are using a ref.
There is a directory of files,
.git/refs that holds your refs. You can create refs using
git update-ref <file path> <SHA>
where the file path is in
There are a lot of ref files created automatically. For instance, the main branch would have a
.git/refs/main file with the SHA in it. So while you are running commands like
git checkout main, git is referencing the
.git/refs/main file to figure out what SHA you mean.
There's a lightweight version that works just like other refs. The annotated tags are like a reference and a commit combined. They include information like a date, author, and message. They're all stored in
HEAD is a special ref that points to what you currently have checked out. Meanwhile, the head ref points to the tip of a branch (e.g. the last commit).
When you run
git checkout main, you're effectively running
git checkout main head. As a result, the
.git/refs/HEAD file is updated to contain
ref: refs/heads/main. The
.git/refs/heads/main file will contain the SHA of the last commit in the main branch.
You can checkout any ref or SHA. If I run
git checkout 123a4b, the HEAD ref in
.git/refs/HEAD will point at commit
123a4b. If this commit is not the head of the branch, the HEAD is detached.
While detaching your HEAD sounds scary, it's just another way to look at git information. Say you pushed commit A and then commit B. Then something went horribly wrong, but now you're not sure which commit is the culprit. You can checkout commit A and see your entire repo as it was before the changes in commit B.
Where it gets really interesting is committing new changes or making new branches while your HEAD is detached. There may be situations where you want to make a new branch based off of an older commit.
You're warned when your HEAD is detached because if you make any new changes, you're making them back in the commit history. If you commit those changes in the same branch and then try to push, git is going to reject them because a fast-forward merge isn't possible.
HEAD^ is shorthand for HEAD^1. HEAD^1 is shorthand for "the parent commit of HEAD". HEAD^2 is the shorthand for "the parent commit of the parent commit of HEAD".
If we ran
git checkout main, the HEAD is currently pointing at the head or last commit on main. If we ran
git checkout HEAD^ or
git checkout HEAD^1, we'd be checking out the 2nd to last commit in the main branch. Running
git checkout HEAD^2 would checkout the 3rd to last commit.
Every time we refer to a branch by its name instead of its SHA, we're using a ref git created automatically. We can also create refs.
A HEAD is a special ref git uses for the commit we have checked out. If a HEAD isn't pointing at a head, it's a detached HEAD.