Git Delete Local and Remote Branch: The Complete Guide
- git branch -d is safe β it checks for unmerged commits. git branch -D is force delete β use it after squash-merges or to abandon work, never blindly.
- git push origin --delete removes the remote branch. git fetch --prune cleans up your local tracking refs. You need both for a clean slate.
- Set git config --global fetch.prune true once and never think about stale remote-tracking refs again.
Every developer has stared at a repo with 47 branches named 'fix', 'fix2', 'test-thing', 'adi-temp', and 'FINAL_PLEASE'. Branch hygiene is one of those disciplines that seems trivial until it isn't β until a junior dev checks out the wrong branch, or a merge pipeline fails because it can't determine the base branch from the noise.
I spent three years as the sole platform engineer for a fintech startup where we ran 8-week feature sprints with 12 developers all branching off main simultaneously. We had a hard rule: the moment a PR merged, the branch got deleted β both locally and remotely β by the developer who opened it. Not by a cleanup cron job. Not by a script. By the person who created it, immediately. That discipline kept our branch list under 20 entries at all times and made git log --oneline actually readable.
This guide covers everything that matters in production: safe vs force delete, cleaning up stale remote-tracking refs, the difference between the remote branch and your local tracking reference, and the one-liner that nukes all branches you've already merged into main.
git branch -d vs git branch -D: The Critical Difference
Git gives you two local branch delete commands and the difference between them has saved and destroyed data in equal measure.
git branch -d <branch> is the safe delete. Git checks whether the branch tip is reachable from HEAD or from the branch it's supposed to be tracking upstream. If you have commits on that branch that aren't merged anywhere, Git refuses with: error: The branch 'feature/payment-retry' is not fully merged. That error message is Git doing you a favour.
git branch -D <branch> is the force delete β equivalent to --delete --force. It doesn't check anything. It just removes the ref. The commits still exist in the object store and are reachable via git reflog for 30 days by default, but the branch pointer is gone. This is the command you reach for when you've squash-merged a PR (the branch tip isn't reachable via normal ancestry) or when you genuinely want to abandon experimental work.
The mental model that's kept me out of trouble: -d is for branches you've properly merged via a merge commit. -D is for branches you've squash-merged, rebased, or are deliberately abandoning. Never use -D unless you can answer 'why does Git think this isn't merged?' β because sometimes the correct answer is 'I made a mistake and I'm about to lose work.'
# Safe delete β refuses if branch has unmerged commits git branch -d feature/payment-retry # Force delete β no safety check (use after squash-merge or to abandon work) git branch -D feature/payment-retry # Check what's merged into main before deleting git branch --merged main # Output: branches whose tips ARE reachable from main (safe to -d) git branch --no-merged main # Output: branches with commits NOT yet in main (need -D or review first) # Practical: delete all local branches already merged into main # (excluding main itself) git branch --merged main | grep -v '\* main' | grep -v main | xargs git branch -d
# --merged output example:
feature/add-retry-logic
feature/fix-null-check
* main
# --no-merged output example:
feature/wip-refactor
hotfix/urgent-null-ptr
Deleting Remote Branches with git push
Deleting a remote branch and deleting your local tracking reference for it are two separate operations. Most developers conflate them and then wonder why git branch -r still shows a deleted branch.
git push origin --delete <branch> sends a delete request to the remote. The remote branch on GitHub/GitLab/Bitbucket is gone. Other developers who fetch will get a notice that the branch has been deleted. This is the right command for post-merge cleanup.
The older syntax git push origin :<branch> (note the colon with no local branch name before it) does the same thing β it pushes 'nothing' to the remote branch reference, which deletes it. You'll see this in older scripts and blog posts. It works, but --delete is clearer and should be preferred in anything you write today.
One thing that caught me out early on: after running git push origin --delete feature/payment-retry, running git branch -r on a colleague's machine might still show origin/feature/payment-retry. That's their local remote-tracking reference β a cached copy of what their Git client last saw on the remote. It doesn't update automatically. They need to run git fetch --prune or configure pruning globally.
# Delete the remote branch (what lives on GitHub/GitLab/Bitbucket) git push origin --delete feature/payment-retry # Older syntax β same effect, less readable git push origin :feature/payment-retry # After remote delete: clean up YOUR stale remote-tracking refs git fetch --prune # This removes any local refs to remote branches that no longer exist # e.g. removes origin/feature/payment-retry from your local tracking list # Do both in one step (delete remote + prune all stale refs) git push origin --delete feature/payment-retry && git fetch --prune # Configure auto-prune so you never have stale refs again git config --global fetch.prune true # Now every git fetch and git pull automatically prunes dead tracking refs
- [deleted] feature/payment-retry
# After git fetch --prune:
From github.com:io/thecodeforge/payments-service.git
- [deleted] (none) -> origin/feature/payment-retry
Delete Local and Remote Branch in One Workflow
In practice you almost always want to delete both the local and remote branch together. Here's the sequence I've drilled into every team I've worked with β it takes 10 seconds and leaves the repo clean.
One common confusion: you can't delete the branch you're currently on. Git will tell you error: Cannot delete branch 'feature/payment-retry' checked out at '/home/adi/payments-service'. Switch to main first, then delete.
Another gotcha: if you renamed a branch mid-development (git branch -m old-name new-name), the old remote branch name might still exist. Deleting the new-named branch locally does nothing to the old remote ref. You need to explicitly delete the old remote branch name too.
# Full cleanup workflow β run after a PR is merged # Step 1: Switch away from the branch you want to delete git checkout main git pull origin main # Get latest main before cleanup # Step 2: Delete local branch git branch -d feature/payment-retry # Use -D if you squash-merged (the tip won't be reachable via ancestry) # Step 3: Delete the remote branch git push origin --delete feature/payment-retry # Step 4: Prune stale remote-tracking refs on your machine git fetch --prune # --- ONE-LINER for the muscle-memory inclined --- git checkout main && git pull && git branch -d feature/payment-retry && git push origin --delete feature/payment-retry # --- NUCLEAR: delete all local branches merged into main --- # Review the list first before running git branch --merged main | grep -vE '^\*|^\s*main$|^\s*master$|^\s*develop$' # If the list looks right, pipe to delete: git branch --merged main | grep -vE '^\*|^\s*main$|^\s*master$|^\s*develop$' | xargs -r git branch -d
Already up to date.
Deleted branch feature/payment-retry (was a3f9c2e).
To github.com:io/thecodeforge/payments-service.git
- [deleted] feature/payment-retry
--delete flag has no branch protection by itself β that's your remote's job. If you're running the 'delete all merged' one-liner in a script, always explicitly exclude main, master, develop, and any release branches with grep -vE. I've seen a staging pipeline delete its own develop branch with a naive cleanup script. The fix was 30 seconds with git reflog, but the panic was real.Recovering a Deleted Branch with git reflog
You deleted a branch and immediately knew it was a mistake. Here's the thing β Git almost certainly still has those commits. The branch ref is gone but the commit objects remain in the local object store for 30 days (controlled by gc.reflogExpire). git reflog is your time machine.
git reflog shows a log of every time HEAD moved β including checkouts, commits, merges, and resets. Each entry has a short hash. Find the commit that was your branch tip before you deleted it and create a new branch pointing there.
This only works if you haven't run git gc aggressively and the 30-day window hasn't passed. For remote branch recovery after git push --delete, you need someone else on the team who still has the branch locally, or a backup from your remote (GitHub keeps deleted branch commits accessible for a period but makes it hard to recover β better to ask a teammate first).
# You deleted feature/payment-retry by mistake. Recover it: # Step 1: Find the commit that was the branch tip git reflog # Look for the last checkout of the deleted branch or the last commit on it # Output: # a3f9c2e HEAD@{0}: checkout: moving from feature/payment-retry to main # 7b2d4f1 HEAD@{1}: commit: Add retry logic with exponential backoff # 9c3e8a2 HEAD@{2}: commit: Add PaymentRetryService skeleton # Step 2: Recreate the branch at the commit you want git branch feature/payment-retry a3f9c2e # Or use the reflog shorthand: git branch feature/payment-retry HEAD@{1} # Step 3: Verify you got the right commits git log feature/payment-retry --oneline -5 # If the branch was deleted remotely and you still have it locally: git push origin feature/payment-retry # Recreates the remote branch from your local copy
a3f9c2e HEAD@{0}: checkout: moving from feature/payment-retry to main
7b2d4f1 HEAD@{1}: commit: Add retry logic with exponential backoff
# Recovery:
Branch 'feature/payment-retry' set up to track remote branch 'feature/payment-retry' from 'origin'.
| Command | Deletes | Safety Check | When to Use |
|---|---|---|---|
| git branch -d | Local branch ref | Yes β checks if merged | Normal post-merge cleanup |
| git branch -D | Local branch ref | No β force delete | After squash-merge or abandon work |
| git push origin --delete | Remote branch | No (use branch protection) | After merging PR on remote |
| git fetch --prune | Stale remote-tracking refs | N/A | After any remote branch deletion |
| git config fetch.prune true | Auto-prunes on every fetch | N/A | Set once globally β always recommended |
π― Key Takeaways
- git branch -d is safe β it checks for unmerged commits. git branch -D is force delete β use it after squash-merges or to abandon work, never blindly.
- git push origin --delete removes the remote branch. git fetch --prune cleans up your local tracking refs. You need both for a clean slate.
- Set git config --global fetch.prune true once and never think about stale remote-tracking refs again.
- Deleted a branch by mistake? git reflog shows every HEAD movement for the last 30 days β find the commit hash and recreate the branch pointing to it.
- Never run bulk-delete scripts without reviewing the list first and always explicitly excluding main, master, develop, and release branches.
β Common Mistakes to Avoid
- βUsing -D when -d fails without asking why β the merge check failing is Git telling you there are commits on that branch not in any other branch. Always run git log <branch> --not main first to see what you'd lose.
- βDeleting only the remote branch and leaving the local β or vice versa β resulting in a repo state where git branch -a shows ghosts of deleted branches.
- βNot setting fetch.prune true globally, resulting in remote-tracking refs that outlast the actual remote branches by months, confusing git branch -r output.
- βRunning the 'delete all merged' one-liner without first reviewing the list β always pipe through grep -vE to exclude main, master, develop, and release/* branches.
- βTrying to delete the branch you're currently on β switch to main first, always.
Interview Questions on This Topic
- QA developer on your team force-deleted a branch that wasn't fully merged. How would you recover the work?
- QExplain the difference between a remote branch and a remote-tracking reference in Git.
- QYour git branch -r output shows 30 branches that were deleted on the remote months ago. How do you fix this and prevent it in future?
- QWhen would you use git branch -D instead of git branch -d, and what are the risks?
Frequently Asked Questions
What is the difference between git branch -d and git branch -D?
git branch -d is a safe delete that checks whether the branch has been fully merged before deleting it. If the branch has commits not reachable from HEAD or its upstream, Git refuses. git branch -D is a force delete that skips the check entirely. Use -d for normal post-merge cleanup and -D after squash-merges (where the tip isn't reachable via ancestry) or when deliberately abandoning work.
How do I delete both local and remote branch at the same time?
There's no single command for both, but the two-step sequence is quick: git branch -d feature/my-branch to delete locally, then git push origin --delete feature/my-branch to delete on the remote. Follow up with git fetch --prune to clean up stale remote-tracking refs on your machine.
Why does git branch -r still show a branch I deleted remotely?
git branch -r shows your local cache of remote branches, not the live state of the remote. Run git fetch --prune to sync the cache and remove references to branches that no longer exist on the remote. Set git config --global fetch.prune true to auto-prune on every fetch.
Can I recover a branch I accidentally deleted?
Yes, in most cases. Run git reflog to see a log of all HEAD movements including the last checkout or commit on the deleted branch. Find the commit hash that was the branch tip and run git branch <branch-name> <hash> to recreate it. This works for 30 days after deletion as long as you haven't run aggressive garbage collection.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.