Skip to main content
RunBook Academy

Git, CI/CD & GitOpsVIII · BranchingBranching

The branch lifecycle — listing, sorting, filtering, and auditing branches

Intermediate⏱ ~17 mingit

What you'll learn

  • List local branches with git branch and read the asterisk that marks HEAD
  • Read remote-tracking refs with git branch -a and understand their separate identity
  • Sort and colour-control branch output with --sort and --no-color
  • Distinguish merged from no-merged branches with --merged and --no-merged and use them as the gate for safe deletion
  • Enumerate branches via the plumbing command git for-each-ref for scripts and audits

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.

Every Git repository accumulates branches. A feature branch is created to land a change; a hotfix branch is created to patch a release; a release branch is created to stabilise a build. None of them are deleted automatically. The list grows until someone runs a cleanup script — and the cleanup script is only safe if it can answer the question “which of these branches are merged into the trunk?”. That question is what git branch --merged and git branch --no-merged exist to answer.

The basic list: git branch

git branch with no arguments lists every local branch, one per line, with an asterisk marking the branch HEAD points at:

git branch
#   bugfix/cert-renew
# * feature/iam-rotation
#   hotfix/prod-503
#   main
#   release/v1.4.0

The asterisk sits in the leftmost column and aligns with the branch name regardless of length. * is HEAD. The other branches are local refs that HEAD is not on. Branch ordering is alphabetical; there is no “most recently used” column.

git branch -v adds the tip commit’s abbreviated OID and the subject line of the most recent commit message:

git branch -v
#   bugfix/cert-renew      4d2c8e0 renew letsencrypt certs
# * feature/iam-rotation   9f3c1d7 rotate iam keys
#   hotfix/prod-503        a1b2c3d fix 503 in ingress controller
#   main                   8a3f9d2 bump terraform module to v1.4.0
#   release/v1.4.0         6f4e5a6 cut v1.4.0-rc1

This is the listing format that production cleanup scripts and audit reports usually want. The OID column gives a stable identifier that survives renames; the subject line gives the human-readable context.

Listing remote-tracking refs: -a

git branch -a adds the refs under refs/remotes/, which are the local mirror of the remote’s branches. Remote-tracking refs are read-only from the perspective of a normal workflow: you do not commit on them; you fetch into them.

git branch -a
#   bugfix/cert-renew
# * feature/iam-rotation
#   hotfix/prod-503
#   main
#   release/v1.4.0
#   remotes/origin/HEAD -> origin/main
#   remotes/origin/bugfix/cert-renew
#   remotes/origin/feature/iam-rotation
#   remotes/origin/main
#   remotes/origin/release/v1.4.0

Two notes on the remote-tracking refs:

  • remotes/origin/HEAD is the remote’s default branch. It is stored as a symbolic ref (ref: refs/remotes/origin/main). Most clients never look at it directly, but it is what git clone --no-checkout and git remote set-head operate on.
  • A remote-tracking ref is not a local branch. You cannot run git switch origin/main and start committing — Git will refuse and offer to create a new local branch. The lesson on tracking (VIII-04) covers what remote-tracking refs are for.
flowchart LR
    L["refs/heads/\n(local branches)"] --> A["git branch"]
    R["refs/remotes/\n(remote-tracking mirror)"] --> B["git branch -a"]
    A --> C["single listing"]
    B --> D["local + remote"]

Sorting, colour, and the audit format

git branch output is colourised by default: the current branch is green, branches that have an upstream are blue, and merged branches that are candidates for cleanup may be dimmed depending on configuration. Scripts and audit reports should disable colour with --no-color:

git branch --no-color --list
#   bugfix/cert-renew
# * feature/iam-rotation
#   hotfix/prod-503
#   main
#   release/v1.4.0

The --list flag is implicit when no pattern is given, but explicit --list followed by a glob narrows the output:

git branch --list 'feature/*'
#   feature/iam-rotation
#   feature/network-policy-update

git branch --list 'release/*' --no-color
#   release/v1.4.0
#   release/v1.5.0

Sorting is alphabetical by default. --sort=<key> changes it. Two keys are useful for audits:

  • --sort=committerdate orders branches by the date of their tip commit. This surfaces “the branches nobody has touched in 90 days”, which is what a cleanup script usually wants.
  • --sort=-committerdate reverses it, surfacing the freshest branches first.
# Branches ordered by most recent tip commit, oldest first
git branch --no-color --sort=committerdate

# Oldest tips at the bottom; the top of the list is the freshest work
git branch --no-color --sort=-committerdate

--sort also takes - to reverse, and the key is any field that git for-each-ref knows (covered below). For branch listing, the most useful keys are committerdate and the default name-based sort.

Merged and no-merged: the reachability filter

The two flags that matter for production are --merged and --no-merged. They partition the local branch list by reachability from a given commit (HEAD by default):

  • --merged [<commit>] lists branches whose tip is reachable from <commit>. A branch is “merged” if its commits are ancestors of <commit>.
  • --no-merged [<commit>] lists branches whose tip is not reachable from <commit>. These are the branches whose work has not yet landed.
# Branches whose work is in main
git branch --no-color --merged main
#   bugfix/cert-renew
#   release/v1.4.0

# Branches whose work is NOT in main
git branch --no-color --no-merged main
# * feature/iam-rotation
#   hotfix/prod-503

The discipline this enables is:

# Show every branch that is fully merged into main
# (candidates for safe deletion)
git branch --no-color --merged main

# Show every branch that has unmerged work
# (candidates for review and follow-up)
git branch --no-color --no-merged main

A cleanup script that wants to delete “branches that are fully merged” should run git branch --merged main and pipe the result through xargs git branch -d. The -d flag will still refuse if a branch’s tip is unmerged for any reason (including a fixup commit that was forgotten), and the merge filter pre-narrows the list to the candidates that might be safe.

Plumbing: git for-each-ref

When a script needs the same information as git branch but in a parseable format, git for-each-ref is the underlying command. It walks the refs and prints one line per ref using a --format='%(field)' template:

git for-each-ref --format='%(refname:short)' refs/heads/
# bugfix/cert-renew
# feature/iam-rotation
# hotfix/prod-503
# main
# release/v1.4.0

The %(refname:short) field strips the refs/heads/ prefix and returns the branch name. Other useful fields:

  • %(objectname:short) — abbreviated OID of the tip commit
  • %(committerdate:short) — date of the tip commit
  • %(upstream:short) — name of the upstream branch, if any
  • %(HEAD)* if the ref is HEAD, empty otherwise
# All branches with their tip OID and committer date
git for-each-ref --format='%(refname:short) %(objectname:short) %(committerdate:short)' refs/heads/

# feature/iam-rotation 9f3c1d7 2026-08-15
# main                8a3f9d2 2026-08-21

This is the format that nightly audits, branch-protection verifications, and CI lint checks should use. git branch is a porcelain command for humans; git for-each-ref is the plumbing command for scripts.

Production discipline

  1. Use --no-color in scripts and audit reports. Colour codes embedded in logs are noise; disable them at the source.
  2. Filter by --merged before deletion, then enforce with -d. The two-step is the safe-deletion pattern; either step alone is a script waiting to fail.
  3. Audit with git for-each-ref. Nightly audits that emit branch counts, tip OIDs, and committer dates should use the plumbing command and a stable --format template.
  4. Never delete a branch listed by --no-merged main. The filter is the human-readable form of the same check that -d enforces; bypassing it is the first step toward orphaning unreviewed work.

Cross-course references

  • Ansible for Production Sysadmins - Part XXXVIII (Review) uses git for-each-ref to produce a per-branch inventory before a release cut. The same template format works for any infrastructure repository.
  • GitOps with Argo CD - Part IV (AppSources) describes branch-per-environment strategies where the merged-vs-not filter determines which environment branch is ready to be cut from.
  • Terraform for Production Sysadmins - Part XII (State) draws the analogy between --merged and Terraform’s reachability checks: both are graph queries that ask “is this thing still part of the history?”.

Quiz

Knowledge check · 4 questions

  1. Q1. A cleanup script wants to delete every local branch that is fully merged into main. Which pipeline is correct?

  2. Q2. `git branch -a` lists both local branches and remote-tracking refs, but remote-tracking refs cannot be checked out and committed on directly.

  3. Q3. What is the difference between `--merged main` and `--no-merged main`, and which one is the gate for safe branch deletion?

  4. Q4. Diagnose a cleanup script that is producing noisy refusals and recommend the right pipeline.

    A nightly cleanup script is meant to delete local branches whose tip is fully merged into main. The script runs `git branch --no-merged main | xargs git branch -d` and produces hundreds of `error: The branch '...' is not fully merged` messages in the job logs. The team's log monitor pages on every error line, and the on-call engineer is being paged every night.

Passing score: 75%. Answers are checked in this browser.