How Git Branching Works — Refs, HEAD, and the Cost of Branches

How Git Branching Works — Refs, HEAD, and the Cost of Branches

2026-03-24

In Subversion, creating a branch copies the entire directory tree. In Git, creating a branch writes 41 bytes to a file. That difference comes from Git's object model — branches are not copies of code, they are pointers to commits.

What Is a Ref?

A ref (reference) is a human-readable name that maps to a commit hash. It is a file containing 40 hexadecimal characters (a SHA-1 hash) plus a newline.

.git/refs/
  heads/
    main        → contains "a1b2c3d4e5f6..." (the commit hash)
    feature     → contains "f7e9a2b3c4d5..."
  tags/
    v1.0        → contains "8899aabb..."
  remotes/
    origin/
      main      → contains "a1b2c3d4e5f6..."

When you run git branch feature, Git creates the file .git/refs/heads/feature and writes the current commit hash into it. No files are copied. No trees are duplicated. One file, 41 bytes.

When you make a new commit on that branch, Git updates the ref to point to the new commit. The old commit still exists — nothing is overwritten.

What Is HEAD?

HEAD is a special ref that tells Git which branch you are currently on. It is stored in .git/HEAD and usually contains a symbolic reference to a branch:

ref: refs/heads/main

This means "HEAD points to main, and main points to commit a1b2c3." When you make a new commit, Git updates the ref that HEAD points to — main advances to the new commit.

git checkout feature changes HEAD to ref: refs/heads/feature. No files are moved between directories. Git reads the tree from the commit that feature points to and updates the working directory to match.

How Branch Pointers Move

Every commit advances the current branch pointer. Here is a three-commit history on main, then a branch created and extended:

branch pointers on a commit DAG

c1 c2 c3 c4 c5 main feature HEAD HEAD → feature → c5

main still points at c3. feature points at c5. HEAD points to feature. Each branch is just a pointer — the commits themselves are shared. c1, c2, and c3 belong to both branches.

Detached HEAD

Normally HEAD points to a branch name. But you can check out a specific commit directly:

git checkout a1b2c3

Now .git/HEAD contains a raw hash instead of a symbolic reference:

a1b2c3d4e5f6...

This is detached HEAD. You are not on any branch. If you make commits, they are not attached to any branch pointer. When you switch away, those commits become unreachable — no ref points to them. They will eventually be garbage collected.

This is not dangerous if you know what is happening. You can always create a branch from detached HEAD to save your work:

git checkout -b rescue-branch

Why Branching Is O(1)

In centralized VCS systems like Subversion, a branch is a copy of the directory tree. For a 10,000-file project, branching copies 10,000 files (or at minimum, creates cheap copies that still require metadata per file).

In Git, branching writes a 41-byte file. The cost is constant regardless of project size. This is because:

  1. Branches don't contain files. They contain a commit hash.
  2. Commits don't contain files. They contain a tree hash.
  3. Trees are shared. If two branches have the same files, they point to the same tree and blob objects.

This O(1) cost changes how you use branches. In SVN, branches were heavyweight and infrequent. In Git, creating a branch for every feature, bug fix, or experiment is free. The cost is in merging, not in branching.

Tags vs Branches

A tag is a ref that does not move. When you create a tag:

git tag v1.0

Git writes the current commit hash to .git/refs/tags/v1.0. Unlike a branch, making new commits does not advance the tag. It stays fixed at the commit where it was created.

An annotated tag is slightly different — it creates a tag object (a fourth object type) that contains the tagger, date, message, and a pointer to the commit. Annotated tags are used for releases. Lightweight tags are just refs.

Remote-Tracking Branches

When you clone a repository, Git creates refs under .git/refs/remotes/origin/ that mirror the remote's branches. These are remote-tracking branches — they record the last known state of the remote.

git fetch updates these refs to match the remote. git pull is git fetch + git merge origin/main. Your local main branch and origin/main are independent refs that happen to track the same history.

When remote-tracking branches and local branches diverge, you have a merge or rebase decision to make — which is the subject of the next two lessons.

Next Steps