Git, CI/CD & GitOpsVIII · BranchingBranching
The branch lifecycle — listing, sorting, filtering, and auditing branches
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
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/HEADis 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 whatgit clone --no-checkoutandgit remote set-headoperate on.- A remote-tracking ref is not a local branch. You cannot run
git switch origin/mainand 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=committerdateorders 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=-committerdatereverses 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
- Use
--no-colorin scripts and audit reports. Colour codes embedded in logs are noise; disable them at the source. - Filter by
--mergedbefore deletion, then enforce with-d. The two-step is the safe-deletion pattern; either step alone is a script waiting to fail. - 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--formattemplate. - Never delete a branch listed by
--no-merged main. The filter is the human-readable form of the same check that-denforces; bypassing it is the first step toward orphaning unreviewed work.
Cross-course references
- Ansible for Production Sysadmins - Part XXXVIII (Review)
uses
git for-each-refto 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
--mergedand Terraform’s reachability checks: both are graph queries that ask “is this thing still part of the history?”.
Quiz
Knowledge check · 4 questions
Q1. A cleanup script wants to delete every local branch that is fully merged into main. Which pipeline is correct?
Q2. `git branch -a` lists both local branches and remote-tracking refs, but remote-tracking refs cannot be checked out and committed on directly.
Q3. What is the difference between `--merged main` and `--no-merged main`, and which one is the gate for safe branch deletion?
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.