open atlas
↑ Back to track
Git, from zero to senior GIT · 08 · 02

Refs and HEAD — how names point at commits

Refs are human names for commit hashes: a branch under refs/heads is a file holding one SHA, tags and remotes live alongside. HEAD is a symbolic ref pointing at the current branch (attached) or straight at a SHA (detached). Committing moves the branch ref and HEAD follows.

GIT Senior ◷ 17 min
Level
FoundationsJuniorMiddleSenior

The object store from the last lesson is addressed entirely by 40-character hashes. Nobody types 9f2c1a… to switch branches — you type git switch main. The thing that maps the word main to a hash is a ref, and it is almost insultingly simple: a small text file containing one commit SHA. Branches, tags, and HEAD are all just refs, which is why “branching is cheap” is literally true — a branch is one line in a file.

By the end of this lesson you will be able to explain what a branch ref, a tag ref, and HEAD actually are on disk, and tell an attached HEAD from a detached one.

Goal

After this lesson you can explain that a ref is a name holding a commit hash, locate branches/tags/remotes under .git/refs, describe HEAD as a symbolic ref, and distinguish attached from detached HEAD — including why committing updates the current branch ref.

1

A ref is a human-readable name that resolves to a commit hash. Refs live under .git/refs, organised by kind:

  • refs/heads/<branch> — local branches. The file refs/heads/main contains one line: the 40-char SHA of the branch tip.
  • refs/tags/<tag>tags. (A lightweight tag points straight at a commit; an annotated tag points at a tag object, which then points at the commit.)
  • refs/remotes/origin/<branch>remote-tracking refs, your local memory of where the remote’s branches were at last fetch.
cat .git/refs/heads/main      # -> 9f2c1a… (just a SHA)
git rev-parse main            # the porcelain way to resolve a ref to its hash
2

A branch is one mutable pointer, not a container of commits. Saying “this commit is on main” really means “the commit is reachable by following parent links back from whatever SHA refs/heads/main currently holds.” The branch owns no commits; it is a single editable pointer to a tip. That is why creating a branch is O(1) — git branch feature writes one 41-byte file — and why deleting a branch deletes no commits, only the name.

3

HEAD is a symbolic ref: a ref that points at another ref. .git/HEAD normally does not contain a SHA — it contains a pointer to a branch:

cat .git/HEAD                 # -> ref: refs/heads/main
git symbolic-ref HEAD         # -> refs/heads/main  (which branch HEAD tracks)
git rev-parse HEAD            # -> the SHA that chain ultimately resolves to

This indirection is the whole trick. “The current branch” is defined as “whatever branch HEAD points at.” Switching branches is just rewriting HEAD to name a different ref and updating your working tree to match.

4

Committing updates the branch ref, and HEAD comes along for free. When you commit while HEAD points at refs/heads/main:

  1. Git writes the new commit object (tree + parent = old tip + metadata).
  2. Git updates refs/heads/main to hold the new commit’s SHA.
  3. HEAD is untouched — it still says ref: refs/heads/main, which now resolves to the new commit.

So HEAD advancing is a consequence of the branch ref advancing, not a separate write. You can do step 2 by hand:

git update-ref refs/heads/main <new-sha>   # plumbing: move a branch tip directly
Edge cases

Detached HEAD. Check out a commit or tag directly — git checkout <sha>, git checkout v1.2, or git switch --detach — and HEAD stops being symbolic: .git/HEAD now holds a raw SHA instead of ref: …. You are on no branch. New commits are created and HEAD advances to each one, but no branch ref moves with you. Switch away and those commits become unreachable (recoverable only via the reflog) until garbage collection removes them. The fix is to name the work before leaving: git switch -c rescue turns your detached commits into a real branch.

5

Refs get packed for performance. A repo with thousands of tags would mean thousands of tiny files. git gc (or git pack-refs) consolidates them into a single .git/packed-refs file — one line per ref, <sha> <refname>. Git reads packed-refs and loose refs together; a loose .git/refs/heads/main always wins over the packed entry for the same name. This is purely a storage optimisation — git rev-parse main resolves identically either way.

Worked example

Watching the files move during a commit.

Start on main and inspect the three things that matter:

git symbolic-ref HEAD          # -> refs/heads/main   (HEAD is attached)
git rev-parse HEAD             # -> 9f2c1a…           (current tip)
cat .git/refs/heads/main       # -> 9f2c1a…           (same SHA)

Make a commit and look again:

echo change >> file.txt && git add -A && git commit -m "edit"
git symbolic-ref HEAD          # -> refs/heads/main   (UNCHANGED)
cat .git/refs/heads/main       # -> 4b8e7d…           (NEW tip)
git rev-parse HEAD             # -> 4b8e7d…           (follows the branch)

Only refs/heads/main was rewritten. HEAD still names the same branch; it resolves to the new commit purely because the branch it points at moved. Now detach and commit:

git checkout 9f2c1a…           # detach onto the old tip
cat .git/HEAD                  # -> 9f2c1a…           (raw SHA, not "ref: …")
echo x >> file.txt && git commit -am "wip"
cat .git/HEAD                  # -> a new SHA; refs/heads/main did NOT move

The commit exists but no branch points at it. git switch main now would strand it — git switch -c keep instead saves it as a branch.

Common mistake

A frequent confusion is treating HEAD as “the latest commit.” HEAD is not a commit and not the newest commit — it is a pointer, usually to the branch you have checked out. After git switch old-release, HEAD points at a months-old commit, and git rev-parse HEAD is that old SHA, not the tip of main. “Latest” only enters the picture because each branch ref happens to point at its own tip.

Check yourself
Quiz

You are on branch main and run git commit. Which ref does git rewrite to point at the new commit?

Recap

A ref is a human name that resolves to a commit hash; branches live under refs/heads, tags under refs/tags, remote-tracking refs under refs/remotes. A branch is just a one-line file holding a SHA, which is why creating and deleting branches is nearly free and touches no commits. HEAD is a symbolic ref — normally ref: refs/heads/<branch> — so “the current branch” is whatever HEAD names. Committing rewrites the branch ref; HEAD follows automatically. Check out a commit directly and HEAD becomes detached, holding a raw SHA with no branch to carry your new commits. Bulk refs are consolidated into packed-refs for speed. Next you will see how two of these branch pointers get reconciled — the real mechanics of merge and rebase.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.