Git from First PrinciplesPost 3 of 16 · 16 of 16 written

3. References: branches, HEAD and tags are pointers

Git · 3 of 164 min readOct 2026

A branch is a name that holds one commit hash, and nothing more. Once you see this, branching, merging and recovery stop being mysterious.

The problem references solve

Hashes are unmemorable, and the newest commit keeps changing. You need human-readable, movable names such as “the latest commit of the data-loader work”. A ref is exactly that: a name mapped to a hash.

git rev-parse main        # prints the hash main points to
git show-ref              # lists every ref and its hash
cat .git/refs/heads/main  # often the same hash, stored as a tiny file

Prefer rev-parse and show-ref to reading files. Refs can also sit in a packed file, and Git 3.0 plans a new storage format called reftable, so the files may not be there.

Branches

  • git branch feature creates a ref pointing at the current commit. No files are copied, so it costs nothing.
  • When you commit, Git creates the new commit with the current one as its parent, then moves the branch ref to the new commit. That move is what “the branch advances” means.
  • Deleting a branch deletes only the label. The commits stay in the database until nothing reaches them and garbage collection runs.
  • A branch does not contain commits. It points to one; the commits “on” it are those reachable from it. One commit can be on many branches at once.

HEAD: where you are

HEAD is a special ref meaning “your current position”. Normally it is a symbolic ref: it names a branch, and the branch names a commit.

cat .git/HEAD
# ref: refs/heads/main

When you commit, Git moves whichever branch HEAD names. Switching branches means repointing HEAD at another branch and updating your files to match that branch’s snapshot.

Detached HEAD

If HEAD holds a commit hash directly instead of a branch name, you are detached. It happens with git switch --detach <hash>, when you check out a tag, and during rebase or bisect.

You can still commit while detached, but no branch moves to remember those commits. Switch away and they become unreachable, findable only through the reflog. The fix is one command before you leave: git switch -c rescue-branch. Think of detached HEAD as tourist mode, not an error.

Naming commits relative to refs

Most commands accept a revision expression wherever they want a commit. These are the ones worth memorizing.

Expression Meaning
HEAD~1 or HEAD~ The first parent of HEAD
HEAD~3 Three generations back along first parents
HEAD^2 The second parent of a merge commit
main@{2} Where main pointed two moves ago (from the reflog)
main@{yesterday} Where main pointed yesterday (from the reflog)
@{u} The upstream of the current branch (Part 8)
A..B Commits reachable from B but not from A: “what B has that A lacks”
A...B Commits reachable from either, but not both
HEAD:src/train.py That file’s content as it was in that commit
:/fix loader The newest commit whose message matches “fix loader”

git rev-parse <expression> resolves any of these to a hash. It is the fastest way to test your understanding. The full list is in git help revisions.

Tags

  • A lightweight tag is a ref under refs/tags/ pointing straight at a commit: a branch that never moves.
  • An annotated tag is a tag object (tagger, date, message, optional signature) plus a ref pointing to it. Use these for releases: git tag -a v1.2.0 -m "First public release".
  • Tags do not move when you commit, and push does not send them by default. Use git push origin v1.2.0, or git push --follow-tags to send annotated tags reachable from what you push.
  • In ML work, tag the exact commit that produced a model you ship, for example model-v3.

The reflog: your safety net

Every time a ref moves on your machine - commit, reset, rebase, switch - Git logs its old and new position. git reflog shows where HEAD has been; git reflog show main shows one branch’s moves.

The reflog is local, never pushed, and its entries expire: by default after 90 days, or 30 days for commits no longer reachable. This is why “lost” commits are almost never lost. They are unreachable from branches but still sit in the database, listed in the reflog (Parts 7 and 14).

Where each kind of ref lives

Ref Path Moves when
Local branch refs/heads/<name> You commit, merge, rebase or reset on it
Remote-tracking branch refs/remotes/origin/<name> You fetch (Part 8)
Tag refs/tags/<name> Never, unless you force it
HEAD .git/HEAD You switch, or commit (through its branch)
Stash refs/stash You stash or pop

Check yourself

  • What exactly is written to the repository when you run git branch experiment?
  • You make two commits in detached HEAD, then switch to main. Where are those commits, and how do you get them back?
  • What does git log main..feature list, and why is it useful before merging?