Emmanuel Corels
← Blog
Cloud & DevOps

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.

By Emmanuel Corels

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 ↗