GitReferences (refs)

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

Bash
# 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

Text
.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 to
Branch Refs (refs/heads/)

Branch refs in detail

Bash
# 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
Note
When you make a commit, Git writes the new commit hash into `refs/heads/<current-branch>`. That single file write is what "advancing the branch" means internally.
Tag Refs (refs/tags/)

Lightweight vs annotated tag refs

Bash
# 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^{}
# a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b
Remote-Tracking Refs (refs/remotes/)
Remote-tracking refs are read-only snapshots of where a remote's branches were the last time you fetched from them. They are updated by `git fetch` but never by `git commit`.

Remote-tracking refs

Bash
# 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

Bash
# 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
Repositories with many branches and tags accumulate many small files in `/.git/refs/`. Git periodically compresses these into a single `/.git/packed-refs` file for performance. Once a ref is in `packed-refs`, the corresponding file in `refs/` may not exist. Git always checks `packed-refs` as a fallback.

Inspect packed-refs

Bash
cat .git/packed-refs

packed-refs file format

Text
# 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

Bash
# 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

Bash
# 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
A refspec is the pattern Git uses to map remote refs to local refs during fetch and push. It has the form `[+]<src>:<dst>` where `+` means force (allow non-fast-forward).

Refspecs in .git/config

Bash
cat .git/config

Default fetch refspec

Text
[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 branches

Custom refspecs for fetching

Bash
# 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

Bash
# 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

Text
main → a3f1c2d
develop → 9e2b0f1
feature/auth → 7c4d8a3
Tip
`git for-each-ref` is the scriptable, stable way to enumerate refs. Unlike `git branch` or `git tag`, its output format is fully configurable and guaranteed not to change between Git versions. Use it in scripts that process refs.