Git Developer Guide
error: The branch 'x' is not fully merged
What git branch -d is checking
git branch -d only deletes a branch whose tip commit is reachable from a reference point. If the branch has an upstream and that upstream still exists locally, the reference point is the upstream (origin/fix-login). Otherwise it is HEAD, the branch you have checked out. If the tip is not an ancestor of that reference point, Git refuses:
$ git branch -d fix-login
error: the branch 'fix-login' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D fix-login'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
Git 2.50 prints it in lower case with the hints; older versions print error: The branch 'fix-login' is not fully merged. followed by If you are sure you want to delete it, run 'git branch -D fix-login'. Same check either way. Note that “merged” is a question about the commit graph, not about file contents. A branch whose changes are all on main can still fail the check if the commits that carried them are different objects, which is exactly what squash and rebase merges produce. The outputs below come from a throwaway repository on Git 2.50.
When the branch is merged into its upstream but not into what you have checked out, -d deletes it and warns instead:
$ git branch -d feature
warning: deleting branch 'feature' that has been merged to
'refs/remotes/origin/feature', but not yet merged to HEAD
Deleted branch feature (was d8a8c18).
That warning is why a squash-merged branch sometimes deletes cleanly and sometimes does not: as long as origin/feature is still around, the branch is trivially merged into it. Once the pull request's head branch is deleted on GitHub and you run git fetch --prune, the upstream is gone, the check falls back to HEAD, and the error appears. See remote branch deleted but still shows locally for that side of it.
See what -d sees
Three commands show the same thing from different angles. Run them from main, or name main explicitly as below so it does not matter what is checked out.
git branch --no-merged main
git log --oneline main..fix-login
git cherry -v main fix-login
git branch --merged main lists branches whose tip is an ancestor of main; --no-merged lists the rest, which are the ones -d will refuse. git log main..fix-login lists the commits on the branch that main does not have, by identity. git cherry is the useful one: it compares each of those commits by patch (the diff it introduces) instead of by hash. A - means an equivalent patch is already on main; a + means it is not. Here is a branch that was squash-merged as PR #42, after the remote branch was pruned:
$ git branch --merged main
* main
$ git log --oneline main..fix-login
9adf785 fix login redirect
$ git cherry -v main fix-login
- 9adf78557e50f4b5ba36441348a2fed843b96039 fix login redirect
$ git log --oneline -1 main
f8ad202 Fix login redirect (#42)
log says the commit is missing, cherry says its patch is present. That is the signature of a squash or rebase merge, and the branch is safe to force-delete.
Squash-merged and rebase-merged pull requests
GitHub's Squash and merge writes one new commit on main containing the branch's combined diff; Rebase and merge rewrites each commit with a new parent and so a new hash. GitLab and Bitbucket have the same options. In both cases the original commits never become ancestors of main, so git branch -d will refuse forever, however many times you pull. The same happens if you cherry-picked the commits onto main yourself:
$ git cherry -v main feat2
- 97012487ecf7fad51cbcfa0e1efb761de89a0cf3 add d
- d3b759a70069029a35e3e46ebe6f594db34568aa add e
$ git branch -d feat2
error: the branch 'feat2' is not fully merged
cherry handles rebase merges and single-commit squashes. It cannot see through a squash of several commits, because the one combined patch on main matches none of the individual ones; every line comes back +. For those, check the pull request instead. With the GitHub CLI, gh pr list --state merged --head feat3 shows whether a PR from that branch was merged, and git log --oneline main | grep '#43' finds the squash commit by PR number.
When git branch -D is safe
-D is just --delete --force: it skips the merged check and nothing else. It is safe when every commit in git log main..branch is accounted for, either because its patch is on main (a - from cherry, or a merged PR) or because you genuinely do not want it. It is not safe when that list contains work you have not seen land anywhere; a branch with one merged commit and one later commit you forgot about fails the check for a real reason.
git branch -D fix-login
If you get it wrong, the commits are not gone yet. The deletion message prints the tip (Deleted branch fix-login (was 9adf785)), and git branch fix-login 9adf785 recreates the branch at it. If you lost the message, git reflog still lists the commit. Unreachable commits survive until git gc prunes them, which by default is at least two weeks after they became unreachable.
git branch -d takes several names and deletes the ones that pass, so it is fine to hand it a list and then deal with the refusals one at a time. Two things it never does: delete the branch you have checked out (cannot delete branch 'x' used by worktree at ..., so switch to main first) or touch the remote. For a bulk cleanup of everything already merged, see find and delete stale local Git branches.
How GitMon shows it
GitMon lists stale local branches across every repository on your Mac. It marks a branch merged only when its tip is provably an ancestor of the repository's default branch, which is the same test git branch -d makes with main checked out, so a squash-merged branch shows up as untouched for N days rather than as merged. GitMon is read-only; open the repository from its row and use the commands above to decide what to delete.