Git, CI/CD & GitOpsV · Branches, Refs and HEADBranches, Refs and HEAD
Branches are pointers — what a branch really is
What you'll learn
- Explain that a branch is a moving ref under refs/heads, not a directory of files
- Create, list, point, and delete branches with git branch and git update-ref
- Distinguish what git branch does (manipulates refs) from what git checkout does (switches the working tree)
- Recognise why branching is cheap and why the cost model differs from older version control systems
- Use the -d and -D flags to delete branches safely and only when the work is reachable
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A branch is a moving ref under refs/heads/. That is the whole
story. A branch is a single file whose contents are a 40-character
SHA-1, and “branching” is the operation of writing that file to
point at a new commit. There is no directory of branch files, no
lineage table, no metadata beyond the OID, and no overhead
beyond the cost of writing one small file. The cost model — a
branch per feature, a branch per hotfix, a branch per experiment
— falls out of this simplicity.
What a branch is, on disk
When you list the contents of .git/refs/heads/, you see one
file per branch. The file name is the branch name; the file
contents are the OID of the branch’s tip. The branch is that
file.
ls "$GIT_DIR/refs/heads"
# feature/iam-rotation
# main
cat "$GIT_DIR/refs/heads/main"
# 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e
When you commit on main, the file is rewritten to point at the
new commit. The previous tip is still reachable from the new
commit via the parent edge, but the branch has moved forward
by one commit. Branching is moving the pointer; merging is
creating a commit with two parents; deleting a branch is
removing the file.
flowchart LR
M1["main -> 8a3f9d2"] --> C1["commit 8a3f9d2"]
M2["feature/iam-rotation -> 9f3c1d7"] --> C2["commit 9f3c1d7"]
C1 --> P1["commit 6f4e5a6 (main ancestor)"]
C2 --> P1
In the diagram, main and feature/iam-rotation are two files
in .git/refs/heads/. Each points at a different commit. The
two commits share a common ancestor (6f4e5a6), but the
branches are not containers; they are pointers. The DAG is the
actual content; the branches are the names by which the team
refers to specific commits in the DAG.
git branch: what it does and what it does not
git branch is the command that manipulates branch refs. It
has four operations worth knowing:
# List all local branches
git branch
# Create a branch at the current HEAD
git branch feature/iam-rotation
# Create a branch at a specific commit
git branch hotfix/cert-renew "$COMMIT_OID"
# Delete a branch (refuse if the tip is not merged)
git branch -d feature/iam-rotation
# Force-delete a branch (discard the tip unconditionally)
git branch -D feature/iam-rotation
The crucial thing git branch does not do is switch the
working tree. Running git branch feature/iam-rotation creates
the ref and writes the file; it does not move HEAD to the new
branch, does not update the index, and does not touch the
working tree. The working tree and the index remain on whatever
branch HEAD was pointing at before the command.
To switch to the new branch, you need git checkout:
git branch feature/iam-rotation
git checkout feature/iam-rotation
# or, in newer Git, the combined command:
git checkout -b feature/iam-rotation
The -b flag tells git checkout to create the branch first
and then switch to it. This is the most common incantation for
starting a new line of work; git branch alone is the right
tool when you want to create a branch to point at a specific
commit (for example, a hotfix tag) without committing to
checking it out.
# Equivalent of git checkout -b: create and switch
git branch feature/iam-rotation
git checkout feature/iam-rotation
# Equivalent: create upstream-tracking remote branch locally
git checkout --track origin/feature/iam-rotation
Branch deletion is a ref deletion
git branch -d <name> removes the file under
.git/refs/heads/. The branch’s tip commit is unaffected; if
the commit is reachable from any other ref (for example, the
trunk after a merge, or a tag), it stays in the repository
forever. If the commit is not reachable — that is, the branch
tip had unique commits that were never merged — the deletions
becomes “orphaning” those commits, and they become candidates
for garbage collection.
# Safe delete: Git refuses if the branch tip is not reachable
git branch -d feature/iam-rotation
# error: The branch 'feature/iam-rotation' is not fully merged.
# If you are sure you want to delete it, run 'git branch -D feature/iam-rotation'.
# Force delete: discard the tip unconditionally
git branch -D feature/iam-rotation
The -d flag is the safe delete. It walks the commit graph
from the branch tip and asks: is this commit reachable from any
other ref? If yes, deletion is safe. If no, deletion is
refused. The -D flag bypasses the check and deletes the branch
anyway, orphaning the unreachable commits. In production, the
discipline is to use -d; the -D flag is for “I have already
verified this branch is not needed” and is the right tool when
you have inspected the graph with git log <branch> and
confirmed there is nothing there you want to keep.
Listing branches with -v and -a
git branch alone lists local branches. The -v flag adds the
tip commit and the subject line of the commit message:
git branch -v
# feature/iam-rotation 9f3c1d7 rotate iam keys
# * main 8a3f9d2 bump terraform module to v1.4.0
The asterisk marks the current branch — the branch HEAD points
at. The -a flag adds remote-tracking refs:
git branch -a
# feature/iam-rotation
# * main
# remotes/origin/main
# remotes/origin/feature/iam-rotation
The -a output includes refs under refs/remotes/, which are
the local mirror of the remote’s branches. They are not local
branches; you cannot git checkout origin/main and start
committing (Git will refuse and offer to create a new local
branch). Remote-tracking refs exist so that git status, git push, and git fetch can answer questions like “is my local
main ahead of origin’s main?” without a network round trip.
# A dry-run push: what would git push send to the remote?
git push --dry-run
# To git@github.com:org/infra.git
# 8a3f9d2..4d2c8e0 main -> main
The --dry-run flag is the read-only counterpart to a real push.
It reports the commits that would be sent and the branches that
would be updated, without actually doing anything. In production
this is the flag to use whenever a script is about to push — it
is the difference between a safe preview and a destructive
change.
The cost model: branches are cheap
Branching in Git is “create a file, write 40 characters” — a fraction of a millisecond on any reasonable filesystem. The limit on the number of branches in a Git repository is set by the filesystem (inode count, directory size limits) and by the team’s ability to manage them, not by Git itself.
flowchart LR
A["git branch feature/x"] --> B["write .git/refs/heads/feature/x"]
B --> C["40 chars to disk"]
C --> D["branch exists"]
Compare this to Subversion, where a branch is a directory copy in the central repository. The cost there is a copy of the trunk’s tree, which for an infrastructure repository is thousands of files. The cost model is what makes branching practices like one-branch-per-PR, one-branch-per-incident, one-branch-per-environment cheap in Git and prohibitive in SVN.
Production discipline
- Use
git branch -dfor deletes; treat-Das a sharp tool.-dis the safe delete;-Dis the force delete. In production, automated cleanup scripts should use-dand inspect the reflog first if there is any doubt. - Never
git branch -Don a branch you did not create. A branch tip from a colleague may have unique commits that have not yet been merged. The force delete orphans those commits and makes them candidates for garbage collection; the colleague’s work is lost. git checkout -bis for new work;git branchis for scripting. When you are creating a branch and switching to it, usegit checkout -b. When you are creating a branch to point at a specific commit (a hotfix, a release tag-a-like ref), usegit branch <name> <commit>.- Audit branches with
git branch -ain CI. A nightlygit branch -aenumerates every branch in the repository and every remote-tracking ref. A diff against yesterday’s list reveals unexpected branches and unexpected deletions.
Cross-course references
- Ansible for Production Sysadmins - Part XXXVIII (Review)
uses
git branch -Donly after explicitly verifying that every commit on the branch is reachable from the trunk. The same discipline applies to any infrastructure repository where branches are work-in-progress and orphans are review gaps. - GitOps with Argo CD - Part III (AppSources) discusses
branch-per-environment strategies (
main,staging,production). The cheap branch model is what makes this strategy viable; with SVN-era branching costs, the same strategy would be financially prohibitive. - Docker for Production Sysadmins - Part VII (Tagging) draws the analogy between branches and OCI image tags: both are mutable names pointing at a content-addressed object, and both are intended to be moved (branches) or fixed (tags) — the same distinction between a branch and a tag is the distinction between a moving and a fixed git ref.
Quiz
Knowledge check · 4 questions
Q1. What is the on-disk representation of a branch in Git?
Q2. Running `git branch feature/iam-rotation` creates the branch and switches the working tree to it.
Q3. What is the difference between `git branch -d <name>` and `git branch -D <name>`, and which is the safe delete?
Q4. Diagnose a branch cleanup script and recommend the correct deletion flag.
A team's nightly cleanup script attempts to delete every branch that has not been touched in 90 days. The script runs `git branch -D <branch>` for each candidate. After a run, a developer reports that a feature branch with 14 commits, which were never merged into main, has been deleted along with the reflog entries pointing at the orphaned commits. The team's policy is that unreviewed work must be preserved for 180 days.
Passing score: 75%. Answers are checked in this browser.