Reading — step 1 of 5
Read
Refs and HEAD
Here's the number that reframes git: a branch is 41 bytes. Forty hex characters of SHA plus a newline, in a plain text file. Everything that feels heavy about branches — creating, deleting, switching, "moving" — is writing 41 bytes to a file. This lesson is that file system.
Refs: names for hashes
Hashes are perfect identifiers and terrible names. A ref maps a human name to a commit hash, stored exactly where you'd hope:
.git/refs/heads/main ← contains: "a1b2c3…\n" (a branch)
.git/refs/heads/feature ← another branch
.git/refs/tags/v1.0 ← a tag
Try it in any repo: cat .git/refs/heads/main — there's your branch. Creating a branch is creating this file (git branch x = write current hash to refs/heads/x); deleting is deleting it. The 10,000 commits the branch "contains" are reachable through that one hash via last lesson's graph walk — the branch itself is just the entry point. (Real git also compresses old refs into a single .git/packed-refs file — same mapping, different storage; your implementation can ignore it, but now the file won't surprise you.)
The difference between branch and tag isn't storage — identical files! — it's who moves them: commit on a branch and git advances that branch's ref to the new commit; tags are never touched. A branch is a moving bookmark, a tag is a sticker.
HEAD: where you are
One question the refs can't answer: which branch am I on? That's HEAD — a single file at .git/HEAD, and its usual content is the twist of the lesson:
ref: refs/heads/main
HEAD doesn't (normally) hold a hash — it holds the name of a ref. A symbolic ref: a pointer to a pointer. That indirection is exactly how committing works:
- You commit. Git builds the commit object.
- Git reads HEAD → "refs/heads/main".
- Git writes the new commit's hash into refs/heads/main.
HEAD itself never changed — but "where you are" advanced, because HEAD points at a name and the name moved. Switching branches is rewriting one line in HEAD.
Detached HEAD, demystified
Check out a raw commit (git checkout a1b2c3) and HEAD contains a hash instead of a ref name. That's the whole phenomenon: the pointer-to-a-pointer temporarily holds a value directly. Git's scary warning just means: commits you make now advance no branch; they're reachable only through HEAD, and when HEAD moves away they're orphaned. Two-level indirection, level skipped, consequences explained. Every "git is confusing" story about detached HEAD is this paragraph.
Resolving a name
Your implementation needs one function used everywhere: given "main" or "HEAD" or a hash, produce the hash. The chain: if it's HEAD, read .git/HEAD — symbolic? follow the named ref; raw hash? done. If it's a name, try refs/heads/<name>, then refs/tags/<name>. If it looks like a hash, it is one. That resolver is what makes git log main, git log HEAD, and git log a1b2c3 the same command.
Your exercise: Implement Ref Operations
Create/read/update/delete refs, resolve names through HEAD's indirection, handle both symbolic and detached states. It's file I/O plus one recursive resolve — deliberately. When the test that checks "commit advances the current branch, not HEAD itself" passes, you understand the two-level design better than most daily git users, because you've implemented the level most people don't know exists.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…