Skip to main content
RunBook Academy

Git, CI/CD & GitOpsV · Branches, Refs and HEADBranches, Refs and HEAD

Branches are pointers — what a branch really is

Intermediate⏱ ~17 mingit

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

Not yet marked complete on this device.

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

  1. Use git branch -d for deletes; treat -D as a sharp tool. -d is the safe delete; -D is the force delete. In production, automated cleanup scripts should use -d and inspect the reflog first if there is any doubt.
  2. Never git branch -D on 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.
  3. git checkout -b is for new work; git branch is for scripting. When you are creating a branch and switching to it, use git checkout -b. When you are creating a branch to point at a specific commit (a hotfix, a release tag-a-like ref), use git branch <name> <commit>.
  4. Audit branches with git branch -a in CI. A nightly git branch -a enumerates 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 -D only 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

  1. Q1. What is the on-disk representation of a branch in Git?

  2. Q2. Running `git branch feature/iam-rotation` creates the branch and switches the working tree to it.

  3. Q3. What is the difference between `git branch -d <name>` and `git branch -D <name>`, and which is the safe delete?

  4. 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.