Introduction
Objects are immutable and named by hash. References are the mutable, human-readable names that point at them. Every branch, tag, remote-tracking branch, and HEAD is a ref.
Where refs live
Refs are files (or entries in the packed-refs file) under .git/refs/:
.git/refs/heads/<name>: local branches..git/refs/tags/<name>: tags..git/refs/remotes/<remote>/<name>: remote-tracking branches..git/HEAD: the current ref.
Each non-symbolic ref is a one-line file containing a hash:
cat .git/refs/heads/main
# a1b2c3d4...
Listing refs
git for-each-ref
git for-each-ref refs/tags/
git for-each-ref --format='%(refname:short) %(objectname:short) %(subject)'
git show-ref
Symbolic refs
A symbolic ref points at another ref instead of a hash. HEAD is the canonical example:
cat .git/HEAD
# ref: refs/heads/main
git symbolic-ref HEAD
git symbolic-ref refs/remotes/origin/HEAD
Reading refs safely
Always go through plumbing rather than reading files:
git rev-parse HEAD
git rev-parse main
git rev-parse origin/main
git rev-parse v1.0.0^{commit}
Plumbing handles packed refs, symbolic refs, and ref lookups uniformly.
Updating refs
git update-ref refs/heads/feature/login <sha>
git update-ref -d refs/heads/feature/login # delete
git update-ref refs/heads/feature/login <new> <old> # CAS
The three-argument form is compare-and-set; it refuses if the current value differs.
Packed refs
For performance, Git can pack many refs into a single .git/packed-refs file. git pack-refs --all does this; future writes still go to loose files until the next pack.
Refspec syntax
Remotes use refspecs to map between local and remote refs:
+refs/heads/*:refs/remotes/origin/*
The leading + means "force update". This single line is what makes git fetch populate refs/remotes/origin/.
The reftable backend
Modern Git (2.45+) ships an alternative ref storage format called reftable, originally developed for JGit. Reftable stores millions of refs efficiently in a binary log-structured format and supports atomic transactions across many refs at once. Enable on a new repo:
git init --ref-format=reftable
git rev-parse --show-ref-format
For most users the classic loose+packed format is still fine. Reftable shines on servers and on huge monorepos with hundreds of thousands of refs (e.g., one ref per pull request).
Custom ref hierarchies
Refs do not have to live under the standard four namespaces. Tools that need their own names use custom hierarchies under refs/:
refs/notes/*:git notes.refs/stash:git stash.refs/replace/*:git replace.refs/bisect/*:git bisectstate.refs/pull/*/headandrefs/merge-requests/*/head: hosting providers.
git for-each-ref refs/notes/
git for-each-ref refs/replace/
You can create your own as long as the name is a valid ref (no double dots, no spaces, no leading dash).
Common mistakes
Editing ref files by hand and ending up with a trailing newline issue or a half-written file. Use git update-ref. Confusing refs/heads/main, main, and refs/main; git rev-parse --symbolic-full-name resolves any short name to its canonical form. Naming a branch and a tag identically; tools then prefer the tag, leading to subtle confusion. Finally, deleting .git/packed-refs to "clean up"; it deletes refs that were not also loose. Always go through Git.