Git from First PrinciplesPost 3 of 16 · 16 of 16 written
- How to use this guide
- 01Why version control exists
- 02Git's data model: a content-addressed object database
- 03References: branches, HEAD and tags are pointers
- 04The three areas: working tree, index, repository
- 05Daily commands, mapped onto the model
- 06Branching and integration: merge, rebase, cherry-pick
- 07Undoing things safely
- 08Remotes and collaboration
- 09Workflows and conventions
- 10Git for AI and ML work
- 11Git with AI coding agents
- 12Power tools
- 13Under the hood: build a commit by hand
- 14Debugging mindset and recovery playbook
- 15Practice path, fluency checklist and glossary
- 16Sources
3. References: branches, HEAD and tags are pointers
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 filePrefer 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 featurecreates 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/mainWhen 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
pushdoes not send them by default. Usegit push origin v1.2.0, orgit push --follow-tagsto 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..featurelist, and why is it useful before merging?