Retire a Git branch locally and remotely without losing unmerged work
Identify the remote and merge target, preserve the tip, prove commit reachability, delete the server ref, prune stale tracking refs, and recover if the decision was wrong.
A merged feature branch appears in three places: a local branch, a branch on the hosting server, and a local remote-tracking reference such as origin/feature/invoice-export. Those are related references, not one object. Deleting the wrong one can either do nothing permanent or remove the collaboration point before its commits are safely reachable.
Synchronize your view before deciding
git status --short --branch
git remote -v
git fetch --all --prune --tags
Work from a clean or understood working tree. git fetch --prune updates remote-tracking references and removes ones the server has already deleted; it does not delete your local development branches. Confirm that origin is the intended repository rather than assuming every clone uses that name.
Resolve the exact three references
branch='feature/invoice-export'
git show-ref --verify -- "refs/heads/$branch"
git show-ref --verify -- "refs/remotes/origin/$branch"
git ls-remote --exit-code --heads origin "refs/heads/$branch"
The first command checks the local branch. The second checks your cached view of the remote branch. git ls-remote asks the server. Deleting origin/feature/invoice-export with git branch -r -d removes only the cached remote-tracking ref; the next fetch recreates it while the server branch still exists.
Prove where the branch-only commits are
git log --oneline --decorate --graph origin/main.."$branch"
git rev-list --left-right --count origin/main..."$branch"
An empty first result means the local tip is reachable from origin/main. It does not prove the intended pull request was reviewed, deployed, or merged into the correct release branch. Check the hosting platform’s merge record, required checks and environment state. Squash and rebase merges intentionally create different commit IDs, so raw reachability may report branch-only commits even when their changes were incorporated.
Compare patch content after a squash merge
merge_base=$(git merge-base origin/main "$branch")
git diff --stat "$merge_base".."$branch"
git range-diff "$merge_base"..origin/main "$merge_base".."$branch"
Patch equivalence needs human review; range-diff is evidence, not an automated authorization to delete. Generated files, conflict resolution and follow-up commits can make apparently similar histories materially different.
Create a recoverable marker when risk warrants it
tip=$(git rev-parse "$branch")
printf 'branch=%s tip=%s\n' "$branch" "$tip"
git tag -a "archive/invoice-export-2026-07-27" "$tip" -m 'Temporary recovery marker before branch retirement'
A local tag keeps the commit reachable in this clone. Push an archival tag only if team policy permits persistent server-side archives; otherwise record the full object ID in the approved change ticket and retain the clone until the recovery window closes. A SHA written down is useful only while some reachable repository or object store still retains that object.
Leave the branch and check linked worktrees
git switch main
git pull --ff-only
git worktree list --porcelain
git branch --show-current
Git refuses to delete the branch currently checked out, and a branch checked out in another linked worktree also needs attention. Do not remove a colleague’s worktree directory or use force flags to get past evidence you have not understood.
Delete the server branch explicitly
git push origin --delete feature/invoice-export
This updates refs/heads/feature/invoice-export on the remote. It can fail because the branch is protected, deletion is forbidden, authentication lacks permission, or the ref changed. Those are controls, not obstacles to bypass. Re-fetch and re-evaluate if another contributor pushed since your review.
git ls-remote --heads origin refs/heads/feature/invoice-export
No output with a successful command means the server no longer advertises that branch. Hosting systems may still retain pull-request references or audit records according to their own policies.
Delete the local branch with the safe form
git branch -d feature/invoice-export
Official Git documentation says -d deletes only when the branch is fully merged into its configured upstream, or into HEAD when no upstream exists. Read any refusal. Do not convert it mechanically to uppercase -D; forced deletion explicitly ignores merged status.
git branch -vv
git config --get branch.feature/invoice-export.remote || true
git config --get branch.feature/invoice-export.merge || true
Use force only after preserving and reviewing the tip
git branch backup/invoice-export feature/invoice-export
git branch -D feature/invoice-export
This is appropriate when the branch was intentionally abandoned, replaced by a squash, or points at known disposable experiments. The backup branch makes the action locally reversible. Do not use -D in a bulk cleanup script without a policy for unmerged commits.
Clean remote-tracking references on other clones
git fetch origin --prune
git branch -r --list 'origin/feature/invoice-export'
git remote prune origin --dry-run
Other developers need to fetch with pruning before their origin/... view disappears. Their local branch remains. If they push it again, the server branch can be recreated unless policy blocks it, so communicate retirement and close or update related automation.
Audit consumers before deletion
- Open pull requests and review links.
- CI jobs, deployment rules or preview environments keyed to the branch name.
- Release tooling that fetches the exact ref.
- Documentation badges and external integrations.
- Teammates whose unpushed work is based on the branch.
Deleting a Git ref does not necessarily delete a deployed preview, package, container image or cloud environment. Retire those through their own lifecycle controls.
Recover a branch deleted by mistake
git switch -c feature/invoice-export archive/invoice-export-2026-07-27
git push -u origin feature/invoice-export
If no marker exists, search local reflogs while the object is retained:
git reflog --all --date=iso
git fsck --no-reflogs --unreachable 2>/dev/null
Inspect every candidate before recreating a ref. Reflog expiry and garbage collection mean this is not a durable backup strategy. If the branch name was protected or reused, coordinate rather than pushing immediately.
Verify the final state from both perspectives
git show-ref --verify --quiet refs/heads/feature/invoice-export; echo "local=$?"
git ls-remote --exit-code --heads origin refs/heads/feature/invoice-export; echo "remote=$?"
git branch -r --list 'origin/feature/invoice-export'
Record the retired branch, preserved tip, merge or abandonment evidence, remote response, cleanup owner and recovery expiry. That turns a casual deletion into a reviewable lifecycle event.
A local branch, a remote branch and a remote-tracking reference are three separate refs. Delete each only after proving what it points to and where the commits remain reachable.
Sources: the solved Stack Overflow question (question and accepted answer by Matthew Rankin, CC BY-SA), and Git’s official git-branch, git-push, and git-fetch documentation.
Primary source: Review the official reference ↗