Git References (Refs)
A ref (reference) is a human-readable name that points to a Git object — almost always a commit. Branches, tags, and remotes are all just refs. Without refs, every Git operation would require you to type 40-character SHA hashes. Refs are the layer that makes Git usable as a human being.
What Refs Are
Every ref is stored as a small text file inside /.git/refs/. The filename is the ref name. The file content is a 40-character SHA-1 hash. That is it. A branch is just a file containing one hash.
Read a branch ref directly from disk
# See what main branch points to cat .git/refs/heads/main # a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b # Exactly the same as: git rev-parse main # a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b
The refs/ Directory Structure
.git/refs/ directory layout
.git/refs/
├── heads/ ← local branches
│ ├── main ← file: SHA of latest commit on main
│ ├── develop
│ └── feature/
│ └── auth ← nested directories for / in branch names
├── tags/ ← tags
│ ├── v1.0.0 ← file: SHA of tag object (or commit for lightweight)
│ └── v2.0.0
└── remotes/ ← remote-tracking branches
└── origin/
├── main ← file: SHA of origin's main as of last fetch
├── develop
└── HEAD ← which branch origin defaults toBranch Refs (refs/heads/)
Branch refs in detail
# List all local branches (reads from refs/heads/) ls .git/refs/heads/ # develop feature main # Read main branch ref cat .git/refs/heads/main # a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b # When you commit, this file is updated: git commit -m "New commit" cat .git/refs/heads/main # b4e2f3d94e5f6b7c8d9e0f1a2b3c4d5e6f7a8b9c ← updated to new commit
Tag Refs (refs/tags/)
Lightweight vs annotated tag refs
# Lightweight tag: file contains the commit hash directly
cat .git/refs/tags/v1.0.0-light
# a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b ← direct commit hash
# Annotated tag: file contains the TAG OBJECT hash
cat .git/refs/tags/v1.0.0
# 5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a ← tag object hash
# Dereference annotated tag to get the commit it points to
git rev-parse v1.0.0^{}
# a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8bRemote-Tracking Refs (refs/remotes/)
Remote-tracking refs
# See what origin/main pointed to after your last fetch cat .git/refs/remotes/origin/main # 9e2b0f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a # Git fetch updates these files git fetch origin cat .git/refs/remotes/origin/main # a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b ← updated # List all remote-tracking refs ls .git/refs/remotes/origin/ # HEAD main develop feature/auth
Special Refs
Ref Name | Location | Meaning |
|---|---|---|
HEAD | .git/HEAD | The current branch or commit (symbolic or direct) |
ORIG_HEAD | .git/ORIG_HEAD | Where HEAD was before a merge, rebase, or reset |
MERGE_HEAD | .git/MERGE_HEAD | The other commit being merged (during a merge) |
CHERRY_PICK_HEAD | .git/CHERRY_PICK_HEAD | The commit being cherry-picked |
REBASE_HEAD | .git/REBASE_HEAD | The commit being applied during rebase |
FETCH_HEAD | .git/FETCH_HEAD | The branch fetched during the last git fetch |
Reading special refs
# See current HEAD cat .git/HEAD # ref: refs/heads/main (normal - symbolic ref to main branch) # OR # a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b (detached HEAD) # After a merge that was paused by conflicts: cat .git/MERGE_HEAD # 9e2b0f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a # After a reset or merge, ORIG_HEAD remembers the old position cat .git/ORIG_HEAD # a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b # Undo a merge using ORIG_HEAD: git reset --hard ORIG_HEAD
Packed Refs
Inspect packed-refs
cat .git/packed-refs
packed-refs file format
# pack-refs with: peeled fully-peeled sorted a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b refs/heads/main 9e2b0f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a refs/heads/develop 5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a refs/tags/v1.0.0 ^a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b ← dereferenced annotated tag (commit hash)
Force repacking of refs
# Pack all loose refs into packed-refs git pack-refs --all # After this, loose files under refs/ are removed ls .git/refs/heads/ # may be empty
The git update-ref Plumbing Command
git update-ref is the plumbing command that safely writes ref files. It is safer than writing them directly because it handles locking, atomic writes, and validates that the old value matches what you expect (CAS — compare-and-swap).
Using git update-ref to manipulate refs
# Point main branch to a different commit git update-ref refs/heads/main a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b # Only update if the current value matches (safe CAS operation) git update-ref refs/heads/main NEW_HASH EXPECTED_OLD_HASH # Delete a ref git update-ref -d refs/heads/old-branch # Create a new branch (just create a new ref file) git update-ref refs/heads/new-feature a3f1c2d
Refspecs
Refspecs in .git/config
cat .git/config
Default fetch refspec
[remote "origin"]
url = git@github.com:org/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
# ^ ^
# src (remote) dst (local remote-tracking)
# + means: allow forced updates (non-fast-forward)
# * wildcard: fetch ALL remote branchesCustom refspecs for fetching
# Fetch a specific remote branch directly into a local branch git fetch origin refs/heads/feature/auth:refs/heads/local-auth # Fetch all pull-request refs from GitHub git fetch origin '+refs/pull/*/head:refs/remotes/origin/pr/*' # Push local main to remote's production branch git push origin refs/heads/main:refs/heads/production
Listing All Refs
Various ways to list refs
# List all refs (branches, tags, remotes) git show-ref # List only branches git for-each-ref refs/heads/ # List with custom format git for-each-ref --format='%(refname:short) → %(objectname:short)' refs/heads/ # List refs pointing to commits (exclude tag objects) git for-each-ref --format='%(refname) %(objecttype)' | grep commit
git for-each-ref output
main → a3f1c2d develop → 9e2b0f1 feature/auth → 7c4d8a3