Git Delete Branch — Avoid Deleting Develop in Bulk Cleanup
A git push origin --delete script can delete develop, causing CI failures.
- Git branch deletion removes the pointer, not the commits. Commits remain in the object store for 30 days.
git branch -d: safe delete — refuses if unmerged commits exist.git branch -D: force delete — no safety checks.git push origin --delete: removes the remote branch.git fetch --prune: cleans up stale remote-tracking refs on your machine.- Always delete both local and remote after merging a PR. Set
git config --global fetch.prune trueonce. - Production insight: A cleanup script without branch exclusions can delete protected branches — recoverable via reflog but causes real panic.
- Biggest mistake: Using
-Dwhen-dfails without investigating unmerged commits.
A Git branch is like a sticky note on a specific commit — deleting it just removes the sticky note, not the commits underneath. Git gives you two delete commands for good reason: the safe one checks you've merged everything first, and the nuclear one doesn't. Knowing which to reach for and when is the difference between a clean repo and a support ticket at 11pm.
Branch hygiene prevents the chaos that accumulates when merged branches are never cleaned up. A repo with 47 stale branches named 'fix', 'fix2', and 'test-thing' makes git branch output unreadable and causes junior developers to check out the wrong branch.
Deleting branches is a two-part operation: local (the ref on your machine) and remote (the ref on GitHub/GitLab). Confusing these two is the most common mistake — developers delete the remote branch and wonder why git branch -r still shows it, or delete the local branch and wonder why the PR is still open on GitHub.
Common misconceptions: that deleting a branch deletes the commits (it removes the pointer, commits remain for 30 days), that -d and -D are interchangeable (one checks for unmerged work, the other doesn't), and that remote branch deletion automatically cleans up local tracking refs (it doesn't — you need git fetch --prune).
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.'
- -d checks ancestry — refuses if commits aren't reachable from HEAD or upstream
- -D skips the check — removes the ref unconditionally
- Squash-merge and rebase break ancestry — -d will fail, -D is required
- Always inspect with
git log <branch> --not mainbefore using -D
-d vs -D confusion is the most common source of accidental data loss in Git.-d fail, assume it's a nuisance, and reach for -D without inspecting.-d fails, run git log <branch> --not main --oneline to see what you'd lose. If the output is empty (commits were squash-merged), -D is safe. If the output shows commits, merge or cherry-pick them first.-d is safe — it checks for unmerged commits.-D is force — use only when you understand why -d failed.-d fails, inspect with git log <branch> --not main before reaching for -D.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 will 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 catches people 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 is their local remote-tracking reference — a cached copy of what their Git client last saw on the remote. It does not update automatically. They need to run git fetch --prune or configure pruning globally.
- git push origin --delete: removes the branch from the remote server
- git fetch --prune: removes your local cached refs to deleted remote branches
- These are two separate operations — most developers forget the second one
- git config --global fetch.prune true eliminates the problem permanently
git branch -r and sees origin/feature/payment-retry — a branch deleted on the remote two weeks ago.git fetch --prune, but most developers do not know it exists.fetch.prune = true globally in your team's onboarding documentation and in your CI runner's git config. One command eliminates the problem permanently.git push origin --delete removes the remote branch.git fetch --prune cleans up your local tracking refs.git config --global fetch.prune true once to auto-prune on every fetch.git push origin :branch syntax works but --delete is clearer.Delete Local and Remote Branch in One Workflow
In practice you almost always want to delete both the local and remote branch together. Here is the sequence that takes 10 seconds and leaves the repo clean.
One common confusion: you cannot delete the branch you are currently on. Git will tell you error: Cannot delete branch 'feature/payment-retry' checked out at '/path'. 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.
- --delete has no built-in branch protection — it deletes anything
- Branch protection rules on GitHub/GitLab prevent deletion of protected branches
- Always grep -vE to exclude main, master, develop, release/* in bulk scripts
- GitHub has 'Automatically delete head branches' in repo settings — use it instead of scripts
-d (or -D for squash-merges), delete remote with push origin --delete, then fetch --prune.grep -vE.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).
- Records every HEAD movement: checkouts, commits, merges, resets
- Each entry has a hash — use it to recreate a deleted branch
- 30-day default window — configurable via gc.reflogExpire
- For remote recovery: check if a teammate has the branch locally, or use GitHub API
gc.reflogExpire = 90.days). The disk cost is negligible — reflog entries are tiny. The recovery value is enormous.git reflog shows every HEAD movement for 30 days (configurable).git branch <name> <hash>.gc.reflogExpire = 90.days for critical repos.Automating Branch Cleanup with GitHub Actions and Branch Protection
Manual branch deletion works for a single developer, but at team scale you need automation. The two main approaches are GitHub's built-in auto-delete setting and a custom CI cleanup script.
GitHub auto-delete head branches (Settings > General > 'Automatically delete head branches') is the safest option. When enabled, every merged PR's source branch is deleted automatically on the remote. No script, no grep, no risk of deleting protected branches. It only works for PRs merged via the GitHub merge button — not for merges done via command line or other tools.
For teams that need more control (e.g., deleting branches older than 30 days regardless of merge status), a scheduled GitHub Actions workflow can do the job. The key is to always include branch protection exclusions and a dry-run mode.
Branch protection rules on GitHub prevent deletion of critical branches regardless of how the delete command is issued — whether via git push --delete, the web UI, or an API call. Configure these for main, develop, release/*, and any long-lived feature branches. This is the last line of defence against accidental deletion.
A common mistake in automation: writing a script that deletes all branches that match a pattern (e.g., 'feature/*') without checking merge status. This can delete branches that are still in active development or that have open PRs. Always filter by merge status or last commit date.
git branch --merged which checks reachability correctly.grep -vE.Cleanup Script Deletes develop Branch: 30 Minutes of Panic, 30 Seconds of Recovery
git fetch showed - [deleted] (none) -> origin/develop. The develop branch no longer appeared in GitHub's branch list. PRs targeting develop showed 'unknown ref' errors. The CI pipeline for develop builds failed with 'branch not found'.git branch --merged main | xargs git push origin --delete to clean up merged branches.
2. The script did not exclude protected branches with grep -v.
3. develop had been merged into main that afternoon (a periodic sync). The script saw develop as 'merged' and deleted it.
4. The remote develop branch was gone. All remote-tracking refs on developer machines were now stale.
5. CI pipelines that built from develop failed because the branch reference no longer existed on the remote.git reflog | grep develop. Recreated the branch: git branch develop <hash> && git push origin develop.
2. All developers ran git fetch --prune to sync their local tracking refs.
3. Fixed the cleanup script: added grep -vE '^\|^\smain$|^\sdevelop$|^\smaster$|^\srelease/' to exclude protected branches.
4. Added branch protection rules on GitHub to prevent deletion of main, develop, and release/ branches.- Always exclude protected branches (main, develop, release/*) from bulk-delete scripts. Use grep -vE with explicit patterns.
- Branch protection rules on the remote prevent accidental deletion — configure them for main, develop, and release branches.
- Recovery is fast if someone has the branch locally. The reflog preserves the commit hash. But remote-only branches that nobody has locally are harder to recover.
- Test cleanup scripts on a fork or staging repo before running them on the production repository.
git log <branch> --not main --oneline.
3. If the commits were squash-merged: the tip isn't reachable via ancestry. Use git branch -D.
4. If the commits were NOT merged anywhere: merge or cherry-pick them first, then delete with -d.
5. If you're sure you want to abandon the work: use -D after confirming with git log.git fetch --prune to remove stale tracking refs.
3. Prevent this permanently: git config --global fetch.prune true.
4. Verify: git remote prune origin --dry-run shows what would be pruned without actually doing it.git checkout main or git switch main.
3. Then delete: git branch -d <branch>.
4. Prevention: always switch to main before running bulk-delete commands.git reflog to find the last commit on the deleted branch.
3. Recreate: git branch <branch-name> <commit-hash>.
4. If the branch was also deleted remotely: push the recreated branch: git push origin <branch-name>.
5. Window: 30 days by default (controlled by gc.reflogExpire). After that, commits are garbage-collected.Key takeaways
Common mistakes to avoid
6 patternsUsing -D when -d fails without asking why
git log <branch> --not main --oneline to inspect what would be lost. If the branch was squash-merged, -D is safe. If there are unmerged commits, merge or cherry-pick first.Deleting only the remote branch and leaving the local, or vice versa
git branch -d <branch> && git push origin --delete <branch> && git fetch --prune.Not setting fetch.prune true globally
git config --global fetch.prune true once. All future fetches will automatically prune dead refs.Running 'delete all merged' one-liner without first reviewing the list
grep -vE to exclude protected branches. Review the list before piping to xargs. Use GitHub auto-delete head branches instead of scripts where possible.Trying to delete the branch you're currently on
git checkout main && git branch -d <branch>.Deleting a branch after renaming it mid-development
git push origin --delete old-name. The new branch must be deleted separately after merge.Interview Questions on This Topic
What is the difference between `git branch -d` and `git branch -D`? When would you use each?
git branch -d is a safe delete that checks if the branch is fully merged before deletion. If the branch has commits not reachable from HEAD or its upstream, Git refuses. Use -d for normal post-merge cleanup.
git branch -D is a force delete that skips the merge check entirely. Use -D when you've squash-merged (the branch tip isn't reachable via ancestry), when you've rebased and no longer need the old branch, or when you deliberately want to abandon work. Never use -D without first inspecting what commits would be lost with git log <branch> --not main --oneline.Frequently Asked Questions
That's Git. Mark it forged?
5 min read · try the examples if you haven't