Inventory Branches and Tags with for-each-ref
git for-each-ref turns raw refs into a readable report of branches, tags and their history. Without it, finding which branches are safe to delete, or which tag actually shipped last quarter, is pure guesswork. In about 15 minutes you will be able to list branch names with their current marker, order tags by creation date, and filter for merged or unmerged work. The examples use Git 2.43.0, installed here from the git-man package.
The route
Jump straight to the step you need, or tick off Done means at the end.
- Only inspects data: nothing here changes a ref, a commit or the working tree.
- No elevated privileges needed.
1. Confirm the repository and version
Change to the repository you want to inspect and check the Git version:
cd /path/to/repository
git --version
git status --short
The version check should print a Git version. An empty git status --short is not required, but it helps you spot unrelated changes before running commands you might later copy into a script. This guide is written against 2.43.0; later releases can add fields or options, so check the local manual when portability matters.
2. List refs with a useful format
A ref is a named pointer such as refs/heads/main or refs/tags/v2.0. for-each-ref reads refs, formats fields with %(fieldname), and prints one formatted record per ref. Supplying a ref prefix keeps the report focused. Print local branches and tags with a short object name and commit subject:
git for-each-ref \
--format='%(HEAD) %(refname:short) %(objectname:short) %(subject)' \
--sort=refname \
refs/heads refs/tags
Typical output looks like this, although your object IDs and names will differ:
feature d8f0e5f second commit
* main d8f0e5f second commit
v1.0 142d2cc first commit
v2.0 55825ca release v2.0
%(HEAD) expands to * for the checked-out branch and a space for everything else. refname:short strips the usual refs/heads/ or refs/tags/ prefix. Omit --format entirely and the default is the full object name, object type and ref name.
3. Select and order the records
Patterns are optional. A literal prefix such as refs/remotes/origin selects matching refs below that path, or use shell-style patterns for a broader match; exclusions follow the same rules. Show only remote-tracking branches, newest by committer date, capped at ten results:
git for-each-ref \
--count=10 \
--sort=-committerdate \
--format='%(committerdate:short) %(refname:short) %(subject)' \
refs/remotes
The minus sign makes a sort key descending. Multiple --sort options are allowed, and the last one given is the primary key. Date fields like committerdate can take a format suffix, and Git returns an empty value rather than failing when a field does not apply to a given object.
For release names, lexical order is often wrong: v10.0 sorts before v2.0 as plain text. Ask for version-aware sorting instead:
git for-each-ref \
--sort=version:refname \
--format='%(refname:short)' \
refs/tags
4. Answer history questions safely
Reachability filters are useful for release and cleanup reports. --merged lists refs whose tip is reachable from a commit, defaulting to HEAD; --no-merged does the opposite. --contains and --no-contains ask whether a ref contains a specified commit. List local branches already merged into the current branch:
git for-each-ref \
--merged=HEAD \
--format='%(refname:short)' \
--sort=refname \
refs/heads
Expect the current branch plus any branch whose tip is reachable from it. This command deletes nothing. Treat its output as a review list, not automatic permission to remove branches: one of them might hold a name or workflow you still need.
To find refs containing a known commit, capture the object name first, then filter with it:
commit=$(git rev-parse --verify HEAD~2)
git for-each-ref \
--contains="$commit" \
--format='%(refname:short)' \
--sort=refname \
refs/heads refs/tags
If the revision does not exist, git rev-parse stops the command before the report even runs, rather than letting you silently inspect the wrong commit. Combine several --contains filters and a ref only needs to satisfy one of them; it must satisfy none of the commits named by --no-contains.
5. Make output script-friendly
Formatting here is interpolation, not a shell language. A ref name or commit subject can contain characters that change how an unquoted shell line gets interpreted, so ask Git for shell quoting directly when generating assignments:
git for-each-ref --shell \
--format='ref=%(refname) subject=%(subject)' \
refs/heads
That output is suitable for the shell's string-literal syntax. Do not pipe arbitrary output into eval just because --shell exists. When a report will only be displayed or piped onward, use a delimiter such as %09 for a tab and let the receiving tool parse it in a safe, non-evaluating mode:
git for-each-ref \
--format='%(refname:short)%09%(objectname:short)' \
refs/heads | while IFS=' ' read -r branch commit; do
printf 'branch=%s commit=%s\n' "$branch" "$commit"
done
%09 is Git's hexadecimal escape for a tab, and the shell variables above are quoted when printed. Keep the format simple when another program consumes it. For human output, fields like %(upstream:short), %(upstream:trackshort), %(worktreepath), %(creatordate:short) and %(subject) usually beat raw plumbing details.
Common traps
- Unexpected tags or branches: without a pattern, every ref is eligible. Add
refs/heads,refs/tagsorrefs/remotesexplicitly. - Wrong ordering: the default sort is
refname. Use a date field for recency, and prefix it with-for newest first. - Blank fields: upstream, push, author and tag-specific fields can be empty when a ref has no corresponding data. Design the report to tolerate that.
- Ambiguous short names:
refname:shortaims to be non-ambiguous, but full names are safer when exporting data across repositories. - Disk-size conclusions:
objectsize:diskdescribes stored object data, but pack deltas and duplicate copies mean it cannot reliably tell you which ref is responsible for repository size.
Nothing in this guide alters refs, commits, the index or the working tree, so there is no undo step to perform. Stop before bolting on a separate cleanup command based on a report, and review each candidate with the project owner first.
For the complete field list and Git's release history, read the official git-for-each-ref documentation. On this machine, the installed manual is the authority for the Git 2.43.0 behaviour described above.
Done means
- Scope restricted: reports are limited to the ref namespace you intended.
- Fields formatted: you can print names, IDs, dates, subjects and current-branch markers.
- Ordering controlled: you can sort by refname, version or commit metadata and cap the count.
- Reachability understood: you can distinguish merged, unmerged, containing and non-containing refs.
- Output kept safe: generated values stay quoted, and you never evaluate untrusted output.