In Git, how do you list which branches are already merged into main before deleting them?
answer
- Two complementary listing flags on git branch
- Always name the reference point explicitly
- The comparison is ancestry, not content
- Squash and rebase defeat it
- A different command compares patches
basics
~20 sUse git branch --merged main to list branches whose tips main can reach, and --no-merged main for the rest. The test is pure reachability, so squashed or rebased work shows as unmerged; git cherry compares by patch instead.
solid answer
~40 s`git branch --merged main` lists local branches whose tip commit is an ancestor of `main` — deleting those names loses nothing, because every commit stays reachable through `main`. `git branch --no-merged main` gives the complement, which is the list worth reviewing rather than deleting. Add `-r` to ask the same question of remote-tracking refs, or `-a` for both. The critical caveat is that the test is graph reachability, not content equivalence: a branch that was squashed or rebased into `main` produced new commits with new hashes, so its originals are unreachable and it appears under `--no-merged` even though its changes shipped. For those, `git cherry -v main topic` compares patch identity and marks commits already present with a minus sign. Always run the listing before any bulk delete loop.
code
bash · 6 linesgit fetch --prune
git branch --merged origin/main # safe to delete
git branch --no-merged origin/main # review these
git branch -r --merged origin/main # remote-tracking refs too
git branch --merged origin/main | grep -v '^\*' | xargs -r git branch -dgo deeper
Know that git branch --merged main lists branches already folded into main and --no-merged lists the rest, and that you should look before deleting.
Explain that the flag tests ancestry — is the branch tip reachable from the named commit — and pass the reference point explicitly instead of relying on HEAD.
Demonstrate the audit end to end: fetch and prune first, compare against the remote-tracking ref, recognize that squashed and rebased branches read as unmerged, and fall back to git cherry -v or a main..topic log before deciding.
Own branch hygiene as policy: decide what the repository's integration model implies for cleanup, define who prunes and on what cadence, and make sure the mechanical check is never mistaken for a record of what shipped.
## The question these flags answer "Is it safe to delete this branch?" means "if I remove this name, does anything else still reach these commits?" `git branch --merged <commit>` answers it for every local branch at once: it prints the branches whose tip is an ancestor of the given commit, defaulting to `HEAD` when you pass nothing. Pass the reference point explicitly — `git branch --merged main`. `git branch --merged` with no argument compares against wherever you happen to be standing, which produces confidently wrong lists when you are on a feature branch. ## Scope flags - `git branch --merged main` — local branches. - `git branch -r --merged main` — remote-tracking refs (`origin/*`), useful for spotting branches the server still carries. - `git branch -a --merged main` — both. - `git branch --merged origin/main` — compare against the fetched server state rather than your possibly stale local `main`. Fetch first, or the answer describes yesterday's repository. The inverse question — which branches can reach a given commit — is `git branch --contains <commit>`, and `--no-contains` its complement. That is the flag for "which branches already have this fix?". ## The reachability caveat Both `--merged` and the `-d` safety check use ancestry, and ancestry is about hashes. Three histories that shipped identical content look completely different to it: - **A real merge.** The branch tip is an ancestor of `main`. It lists as merged. - **A squash.** One new commit on `main` carries the combined diff; the original commits are not its ancestors. It lists as **not** merged. - **A rebase.** Replaying created new commits with new hashes; the originals are unreachable. It lists as **not** merged. So on a project that squashes or rebases, `--no-merged` is not a list of unfinished work — it is mostly finished work under superseded hashes. Treating it as a delete-blocker there produces branch sprawl; treating it as a delete-list on a project that merges normally destroys unfinished work. Know which model the repository uses before you act on either list. ## Comparing by patch instead of ancestry `git cherry -v <upstream> <topic>` walks the topic branch's commits and marks each one: - `-` the commit's change is already present upstream (an equivalent patch exists), - `+` it is not. A branch whose commits are all `-` was squashed or rebased in and is genuinely safe to delete despite showing up under `--no-merged`. This comparison is by patch identity, so it survives rehashing but not substantial rewriting: a commit that was edited during a rebase may still show `+`. ## Auditing at scale Useful additions when the list is long: - `git branch --merged main --format='%(refname:short) %(committerdate:relative)'` — pair each name with age, so you delete stale merged branches first. - `git branch --sort=-committerdate -v` — most recently touched first. - `git log --oneline main..topic` — the commits `topic` has that `main` does not; an empty result is the merged case, and reading the actual commits is a far better decision input than a branch name. ## Safe deletion procedure 1. `git fetch --prune` so remote-tracking refs reflect the server. 2. `git branch --merged origin/main` and read the list. 3. Delete with `-d`, never `-D`, in the bulk pass — the safety check is a second opinion, and any branch it rejects deserves a look rather than a force. 4. For each rejection, decide with `git log --oneline origin/main..<branch>` or `git cherry -v origin/main <branch>` whether the work shipped under different hashes. Skipping step 1 is the classic error: comparing against a local `main` that is weeks behind marks live branches as unmerged and merged branches as unmergeable, and the whole audit is noise. ## What this does not tell you Reachability says nothing about whether a branch was reviewed, released, or deployed, and nothing about branches that exist only on the server and were never fetched. It is a mechanical safety check on the commit graph — a necessary input to the decision, not the decision.
- Why does a squash-merged branch still appear under --no-merged?The squash created a single new commit on the target with its own hash; the branch's original commits are not ancestors of it. `--merged` tests ancestry only, so the branch reads as unmerged even though its content shipped. `git cherry -v` compares patches and identifies it correctly.
- Why compare against origin/main rather than local main?Local `main` only reflects your last fetch and merge. Auditing against a stale ref reports branches as unmerged that were integrated days ago. Run `git fetch --prune` first and compare against `origin/main`, which is the server's state as of that fetch.
- How do you find which branches already contain a specific bugfix commit?`git branch --contains <hash>`, adding `-r` or `-a` to include remote-tracking refs. It inverts the reachability question: instead of "is this branch merged into X", it asks "which branch tips can reach this commit", which is exactly the backport-status question.
saying these in an interview costs you the question
- Runs git branch --merged without naming a reference point
- Treats --no-merged as proof the work is unfinished
- Bulk-deletes with -D to get past the safety check
- Compares against a local main that was never fetched
- Thinks --merged compares file contents rather than ancestry