Git Developer Guide
Remote branch deleted but still shows locally
Why the branch is still there
origin/fix-login is a remote-tracking ref: a local copy of where the remote's branch pointed the last time you fetched. Deleting the branch on GitHub, whether by clicking Delete branch after a merge or through the repository's Automatically delete head branches setting, changes nothing in your clone. A plain git fetch adds and updates refs but, by default, never removes one that the remote no longer has:
$ git fetch
From github.com:you/app
5ce1e4b..8954c19 main -> origin/main
$ git branch -r
origin/HEAD -> origin/main
origin/feat/search
origin/fix-login
origin/main
$ git ls-remote --heads origin
8954c19ef9a333c6eda43bf8fe7eee1fb9f2ad73 refs/heads/main
git ls-remote --heads origin asks the remote directly and is the truth: only main exists there. The two extra lines in git branch -r are stale. The outputs on this page come from a throwaway remote and two clones on Git 2.50; one clone played the part of GitHub.
Remove the stale remote-tracking refs
Preview what would go, then prune. Pruning only deletes origin/* refs; it never touches your local branches.
git remote prune origin --dry-run
git fetch --prune
$ git remote prune origin --dry-run
Pruning origin
URL: [email protected]:you/app.git
* [would prune] origin/feat/search
* [would prune] origin/fix-login
$ git fetch --prune
From github.com:you/app
- [deleted] (none) -> origin/feat/search
- [deleted] (none) -> origin/fix-login
To make every fetch and pull prune automatically, set it once:
git config --global fetch.prune true
git remote prune origin does the same as --prune without fetching anything new, and git pull --prune is the one-off form for pull. If you use several remotes, git fetch --all --prune covers them in one go.
Local branches whose upstream is gone
Pruning fixes origin/*, but any local branch that was tracking a deleted remote branch is now pointing at nothing. git branch -vv marks these with gone:
$ git branch -vv
docs-typo f647766 [origin/docs-typo: gone] fix typo
feat/search f60a0fa [origin/feat/search: gone] unpushed notes
fix-login b19ddbd [origin/fix-login: gone] fix login redirect
* main 9002afd [origin/main] Merge pull request #43 from you/docs-typo
Until you deal with them, git status on such a branch says Your branch is based on 'origin/fix-login', but the upstream is gone, and git pull fails with Your configuration specifies to merge with the ref 'refs/heads/fix-login' from the remote, but no such ref was fetched. One trap to know about: git push on a gone branch does not fail. It silently recreates the branch on the remote (* [new branch] fix-login -> fix-login), which is how deleted branches keep coming back to a team's repository.
Usually these branches were merged and the right move is to delete them. This lists every local branch whose upstream is gone and hands the list to git branch -d:
git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads \
| awk '$2 == "[gone]" {print $1}' \
| xargs -n 1 git branch -d
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'
error: the branch 'feat/search' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feat/search'
Deleted branch docs-typo (was f647766).
-d rather than -D on purpose. The branch that was merged with a merge commit (docs-typo) deletes cleanly. The other two are refused, for different reasons, and the refusal is the useful part. fix-login was squash-merged as PR #42, so its commit is on main as a different object and Git cannot see that it was merged. feat/search has a commit that was never pushed anywhere:
$ git log --oneline main..fix-login
b19ddbd fix login redirect
$ git log --oneline main..feat/search
f60a0fa unpushed notes
9003e6f search
Force-delete the first (git branch -D fix-login) once you have confirmed the PR merged. Keep the second, or push it somewhere, before it is deleted along with the rest. How to tell those two cases apart with git cherry is covered in error: The branch 'x' is not fully merged. If you want to keep a branch but stop Git complaining about its dead upstream, git branch --unset-upstream fix-login drops the tracking link without deleting anything.
Across every repository on the machine
fetch.prune is global, so once it is set, stale origin/* refs stop accumulating in any repository you fetch. The gone branches are per repository, though, and a machine with forty clones will have them in most. To find the ones with work on them from one terminal:
for d in ~/code/*/.git; do
r=${d%/.git}
git -C "$r" for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads \
| awk -v r="$r" '$2 == "[gone]" {print r": "$1}'
done
Then run the git log main..branch check in each repository it names before deleting. The companion loop for fetching every repo is in git pull or fetch every repo in a folder at once.
How GitMon shows it
GitMon reads each repository's .git/config and flags a branch whose configured upstream ref is not present locally with an Upstream missing badge. You see that state after a prune (the branch is gone) and also on a fresh machine before the first fetch, so the badge says to fetch or to check whether the remote branch was deleted rather than guessing. GitMon never fetches, prunes or deletes; it tells you which repositories need the commands above.