Senior 5 min · March 30, 2026

Git Delete Branch — Avoid Deleting Develop in Bulk Cleanup

A git push origin --delete script can delete develop, causing CI failures.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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 true once.
  • Production insight: A cleanup script without branch exclusions can delete protected branches — recoverable via reflog but causes real panic.
  • Biggest mistake: Using -D when -d fails without investigating unmerged commits.
Plain-English First

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.'

io/thecodeforge/git/DeleteBranch.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# io.thecodeforge — Git Branch Deletion

# ─────────────────────────────────────────────────────────────
# SAFE DELETE — refuses if branch has unmerged commits
# ─────────────────────────────────────────────────────────────
git branch -d feature/payment-retry
# Output: Deleted branch feature/payment-retry (was a3f9c2e).
# Or: error: The branch 'feature/payment-retry' is not fully merged.

# ─────────────────────────────────────────────────────────────
# FORCE DELETE — no safety check
# ─────────────────────────────────────────────────────────────
git branch -D feature/payment-retry
# Output: Deleted branch feature/payment-retry (was a3f9c2e).
# No check. Branch ref removed. Commits still in object store.

# ─────────────────────────────────────────────────────────────
# INSPECT BEFORE FORCE-DELETING
# ─────────────────────────────────────────────────────────────

# 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)

# See exactly what commits you'd lose with -D
git log feature/wip-refactor --not main --oneline
# Output:
# 9c3e8a2 WIP: experimental retry pattern
# 7b2d4f1 Spike: circuit breaker approach
# If these commits matter, merge or cherry-pick before deleting.

# ─────────────────────────────────────────────────────────────
# BULK DELETE: delete all local branches already merged into main
# ─────────────────────────────────────────────────────────────

# Review the list first
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
Output
Deleted branch feature/payment-retry (was a3f9c2e).
# --merged output example:
feature/add-retry-logic
feature/fix-null-check
* main
# --no-merged output example:
feature/wip-refactor
hotfix/urgent-null-ptr
The -d vs -D Decision Is About Intent, Not Urgency
  • -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 main before using -D
Production Insight
The -d vs -D confusion is the most common source of accidental data loss in Git.
Engineers see -d fail, assume it's a nuisance, and reach for -D without inspecting.
The branch is deleted. The commits are orphaned. If nobody else has the branch locally and 30 days pass, the commits are garbage-collected permanently.
The discipline: when -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.
Key Takeaway
-d is safe — it checks for unmerged commits.
-D is force — use only when you understand why -d failed.
When -d fails, inspect with git log <branch> --not main before reaching for -D.
The merge check failure is Git protecting you from data loss.

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.

io/thecodeforge/git/DeleteRemoteBranch.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# io.thecodeforge — Delete Remote Branch

# ─────────────────────────────────────────────────────────────
# DELETE THE REMOTE BRANCH
# ─────────────────────────────────────────────────────────────
git push origin --delete feature/payment-retry
# Output: To github.com:io/thecodeforge/payments-service.git
#  - [deleted]         feature/payment-retry

# Older syntax — same effect, less readable
git push origin :feature/payment-retry

# ─────────────────────────────────────────────────────────────
# CLEAN UP YOUR STALE REMOTE-TRACKING REFS
# ─────────────────────────────────────────────────────────────
git fetch --prune
# Removes any local refs to remote branches that no longer exist
# Output:
# From github.com:io/thecodeforge/payments-service.git
#  - [deleted]         (none)     -> origin/feature/payment-retry

# ─────────────────────────────────────────────────────────────
# DO BOTH IN ONE STEP
# ─────────────────────────────────────────────────────────────
git push origin --delete feature/payment-retry && git fetch --prune

# ─────────────────────────────────────────────────────────────
# CONFIGURE AUTO-PRUNE (do this once, globally)
# ─────────────────────────────────────────────────────────────
git config --global fetch.prune true
# Now every git fetch and git pull automatically prunes dead tracking refs.
# You never need to run git fetch --prune manually again.

# Verify auto-prune is configured
git config --global fetch.prune
# Output: true
Output
To github.com:io/thecodeforge/payments-service.git
- [deleted] feature/payment-retry
# After git fetch --prune:
From github.com:io/thecodeforge/payments-service.git
- [deleted] (none) -> origin/feature/payment-retry
Remote Branch and Local Tracking Ref Are Two Different Things
  • 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
Production Insight
Stale remote-tracking refs are a silent source of confusion.
A developer runs git branch -r and sees origin/feature/payment-retry — a branch deleted on the remote two weeks ago.
They try to check it out and get unexpected behavior. They try to push to it and get errors.
The fix is git fetch --prune, but most developers do not know it exists.
The production solution: set fetch.prune = true globally in your team's onboarding documentation and in your CI runner's git config. One command eliminates the problem permanently.
Key Takeaway
git push origin --delete removes the remote branch.
git fetch --prune cleans up your local tracking refs.
These are two separate operations. Set git config --global fetch.prune true once to auto-prune on every fetch.
The older 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.

io/thecodeforge/git/DeleteFullWorkflow.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# io.thecodeforge — Full Branch Cleanup Workflow

# ─────────────────────────────────────────────────────────────
# FULL CLEANUP — 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 (with exclusions)
# ─────────────────────────────────────────────────────────────

# Review the list first before running
git branch --merged main | grep -vE '^\*|^\s*main$|^\s*master$|^\s*develop$|^\s*release/'
# If the list looks right, pipe to delete:
git branch --merged main | grep -vE '^\*|^\s*main$|^\s*master$|^\s*develop$|^\s*release/' | xargs -r git branch -d

# ─────────────────────────────────────────────────────────────
# HANDLE RENAMED BRANCHES
# ─────────────────────────────────────────────────────────────

# If you renamed old-name to new-name mid-development:
# The old remote branch 'old-name' still exists
git push origin --delete old-name  # Delete the old remote branch name
# The new-name remote branch (if pushed) stays — delete it separately after merge
Output
Switched to branch 'main'
Already up to date.
Deleted branch feature/payment-retry (was a3f9c2e).
To github.com:io/thecodeforge/payments-service.git
- [deleted] feature/payment-retry
Never Delete main, master, develop, or release Branches Remotely
  • --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
Production Insight
GitHub's 'Automatically delete head branches' setting (Settings > General) eliminates the need for manual cleanup scripts entirely.
When a PR is merged, GitHub deletes the branch automatically. This is the safest approach for most teams — it removes the human error of forgetting to delete, and it removes the risk of bulk-delete scripts hitting protected branches.
The trade-off: it only works for PR-based merges. Direct pushes to branches won't trigger auto-delete. For teams that use PRs exclusively, this setting should be enabled on every repository.
Key Takeaway
The full cleanup workflow: switch to main, delete local with -d (or -D for squash-merges), delete remote with push origin --delete, then fetch --prune.
For GitHub repos, enable 'Automatically delete head branches' to automate this.
Always exclude protected branches from bulk-delete scripts with 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).

io/thecodeforge/git/RecoverDeletedBranch.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# io.thecodeforge — Recover Deleted Branch

# ─────────────────────────────────────────────────────────────
# SCENARIO: You deleted feature/payment-retry by mistake
# ─────────────────────────────────────────────────────────────

# 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

# ─────────────────────────────────────────────────────────────
# SCENARIO: Branch was deleted remotely and you still have it locally
# ─────────────────────────────────────────────────────────────

# Push the local branch to recreate the remote branch
git push origin feature/payment-retry
# Recreates the remote branch from your local copy.

# ─────────────────────────────────────────────────────────────
# SCENARIO: Branch was deleted both locally and remotely
# But a teammate still has it locally
# ─────────────────────────────────────────────────────────────

# Ask the teammate to push it:
# On teammate's machine:
git push origin feature/payment-retry

# Or ask them for the commit hash, then recreate on your end:
git fetch origin
git branch feature/payment-retry origin/feature/payment-retry

# ─────────────────────────────────────────────────────────────
# SCENARIO: Nobody has the branch — check GitHub's API
# ─────────────────────────────────────────────────────────────

# GitHub keeps deleted branch refs accessible via the API for a limited time
# Use the GitHub API to list recent branch deletions:
curl -s -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/repos/your-org/your-repo/events" \
  | grep -A5 'DeleteEvent'
# Find the SHA of the last commit on the deleted branch
# Then recreate: git branch feature/payment-retry <sha> && git push origin feature/payment-retry

# ─────────────────────────────────────────────────────────────
# PREVENTION: Extend the reflog retention window
# ─────────────────────────────────────────────────────────────

git config --global gc.reflogExpire 90.days
git config --global gc.reflogExpireUnreachable 90.days
# Default is 30 days. Extend to 90 for more recovery window.
# Trade-off: more disk usage for reflog entries.
Output
# git reflog output (abbreviated):
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'.
Reflog Is Git's Audit Trail — 30 Days of Every HEAD Movement
  • 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
Production Insight
The reflog recovery window is your safety net, but it's local-only.
If you deleted a remote branch and nobody on the team has it locally, the only recovery path is the GitHub API (which keeps deleted refs accessible for a limited time) or your CI system's artifacts.
The practical advice: extend the reflog window to 90 days for critical repositories (gc.reflogExpire = 90.days). The disk cost is negligible — reflog entries are tiny. The recovery value is enormous.
For remote branches, the safest approach is to never delete a branch until the PR is fully merged and verified in production.
Key Takeaway
git reflog shows every HEAD movement for 30 days (configurable).
Find the commit hash of the deleted branch tip and recreate with git branch <name> <hash>.
For remote recovery: check if a teammate has the branch locally, or use the GitHub API.
Extend the window with 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.

io/thecodeforge/git/.github/workflows/cleanup-stale-branches.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# io.thecodeforge — GitHub Actions workflow for stale branch cleanup
name: Cleanup stale branches

on:
  schedule:
    - cron: '0 6 * * 1'  # Every Monday at 6 AM UTC
  workflow_dispatch:  # Allow manual trigger

jobs:
  delete-stale-branches:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Need all branches for --merged check

      - name: Dry-run — list branches that would be deleted
        run: |
          echo "=== Branches merged into main (excluding protected) ==="
          git branch --merged origin/main | grep -vE '^\*|main|master|develop|release/|staging' || true

      - name: Delete stale merged branches locally and remotely
        run: |
          git branch --merged origin/main | grep -vE '^\*|main|master|develop|release/|staging' | while read branch; do
            echo "Deleting $branch"
            git branch -d "$branch"
            git push origin --delete "$branch" || echo "Failed to delete remote $branch (maybe already gone)"
          done

      - name: Prune local remote-tracking refs
        run: |
          git fetch --prune
Output
=== Branches merged into main (excluding protected) ===
feature/add-logging
fix/null-check
Deleting feature/add-logging
Deleted branch feature/add-logging (was abc123).
To github.com:org/repo.git
- [deleted] feature/add-logging
Deleting fix/null-check
Deleted branch fix/null-check (was def456).
To github.com:org/repo.git
- [deleted] fix/null-check
Always Include a Dry-Run Step in Automation
Never run a destructive script blindly. A dry-run step that prints what would be deleted before actually deleting gives you a chance to catch mistakes. In automation, pipe the branch list through a review step or require manual approval for actual deletion.
Production Insight
Automated branch cleanup can backfire hard if not carefully scoped.
A scheduled job that deletes 'all branches merged into main' will also delete branches that were merged via rebase or squash (their tip may not be reachable from main).
The fix: use git branch --merged which checks reachability correctly.
Another risk: branches that were merged but the remote ref still exists — deletion fails if the branch has open PRs or outstanding changes pushed after merge.
Always include error handling and a dry-run mode in any cleanup script.
Key Takeaway
GitHub auto-delete head branches is the safest automation — no script needed.
If using custom scripts, always exclude protected branches with grep -vE.
Always add a dry-run step before actual deletion.
Branch protection rules are your last line of defence — configure them on critical branches.
● Production incidentPOST-MORTEMseverity: high

Cleanup Script Deletes develop Branch: 30 Minutes of Panic, 30 Seconds of Recovery

Symptom
Multiple developers reported that 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'.
Assumption
The team initially assumed a GitHub outage or a permissions issue. They checked GitHub status, repository settings, and team access controls. Nobody suspected the cleanup script that had been running nightly for three months.
Root cause
1. A nightly CI job ran 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.
Fix
1. Recovery: found the last commit on develop from a developer's local reflog: 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.
Key lesson
  • 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.
Production debug guideSystematic recovery paths for accidental deletions, stale refs, and cleanup failures.5 entries
Symptom · 01
'error: The branch X is not fully merged' when trying to delete
Fix
1. Git is protecting you — the branch has commits not reachable from HEAD or its upstream. 2. Inspect what you would lose: 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.
Symptom · 02
git branch -r shows branches that were deleted on the remote weeks ago
Fix
1. Your local remote-tracking refs are stale. The remote branch is gone but your local cache hasn't synced. 2. Run 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.
Symptom · 03
'error: Cannot delete branch X checked out at /path' when deleting current branch
Fix
1. You cannot delete the branch you're currently on. 2. Switch to another branch: 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.
Symptom · 04
Remote branch deleted but PR on GitHub still shows as open
Fix
1. The PR is tied to the branch name, not the branch ref. Deleting the branch doesn't close the PR automatically in all cases. 2. Close the PR manually on GitHub/GitLab. 3. Or configure your platform to auto-close PRs when the branch is deleted (GitHub: Settings > General > 'Automatically delete head branches').
Symptom · 05
Accidentally force-deleted a branch with unmerged commits
Fix
1. Don't panic. The commits still exist in the object store. 2. Run 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.
★ Git Branch Deletion Triage Cheat SheetFast recovery for accidental deletions, stale refs, and cleanup failures.
Accidentally deleted a branch with unmerged commits
Immediate action
Recover from reflog — commits still exist for 30 days.
Commands
git reflog | grep <branch-name> (find the last commit on the deleted branch)
git branch <branch-name> <commit-hash> (recreate the branch)
Fix now
If also deleted remotely: git push origin <branch-name> to restore.
git branch -r shows branches deleted on remote weeks ago+
Immediate action
Prune stale remote-tracking refs.
Commands
git remote prune origin --dry-run (preview what would be pruned)
git fetch --prune (remove stale tracking refs)
Fix now
Set permanently: git config --global fetch.prune true
'error: Cannot delete branch X checked out at /path'+
Immediate action
Switch to a different branch first.
Commands
git checkout main (or git switch main)
git branch -d <branch-name>
Fix now
Always switch to main before running bulk-delete commands.
Bulk-delete script removed a protected branch (develop, release/*)+
Immediate action
Recreate from a teammate's local copy or reflog.
Commands
git reflog | grep develop (find last commit hash)
git branch develop <hash> && git push origin develop
Fix now
Add grep -vE to the script to exclude protected branches. Add branch protection rules on the remote.
'error: The branch X is not fully merged' — unsure if safe to force-delete+
Immediate action
Inspect what commits would be lost before force-deleting.
Commands
git log <branch> --not main --oneline (list unmerged commits)
git diff main <branch> (see the actual changes)
Fix now
If squash-merged: safe to -D. If genuinely unmerged: merge or cherry-pick first.
Branch Deletion Commands
CommandDeletesSafety CheckWhen to Use
git branch -dLocal branch refYes — checks if mergedNormal post-merge cleanup
git branch -DLocal branch refNo — force deleteAfter squash-merge or abandon work
git push origin --deleteRemote branchNo (use branch protection)After merging PR on remote
git fetch --pruneStale remote-tracking refsN/AAfter any remote branch deletion
git config fetch.prune trueAuto-prunes on every fetchN/ASet once globally — always recommended

Key takeaways

1
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.
2
git push origin --delete removes the remote branch. git fetch --prune cleans up your local tracking refs. You need both for a clean slate.
3
Set git config --global fetch.prune true once and never think about stale remote-tracking refs again.
4
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.
5
Never run bulk-delete scripts without reviewing the list first and always explicitly excluding main, master, develop, and release branches.
6
GitHub's 'Automatically delete head branches' setting eliminates manual cleanup for PR-based workflows.

Common mistakes to avoid

6 patterns
×

Using -D when -d fails without asking why

Symptom
Branch deleted, but commits are orphaned and potentially lost after 30 days.
Fix
When -d fails, run 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

Symptom
git branch -a shows ghosts of deleted branches; PRs may remain open or appear stale.
Fix
Always delete both local and remote branch in the same workflow: git branch -d <branch> && git push origin --delete <branch> && git fetch --prune.
×

Not setting fetch.prune true globally

Symptom
git branch -r shows stale remote-tracking refs that were deleted on the remote weeks ago.
Fix
Run 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

Symptom
Protected branches like develop or release may be deleted, breaking CI and causing panic.
Fix
Always pipe through 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

Symptom
Git returns: 'error: Cannot delete branch X checked out at /path'.
Fix
Switch to main or another branch first: git checkout main && git branch -d <branch>.
×

Deleting a branch after renaming it mid-development

Symptom
The old remote branch name still exists and continues to appear in git branch -r.
Fix
After renaming locally, delete the old remote branch with git push origin --delete old-name. The new branch must be deleted separately after merge.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between `git branch -d` and `git branch -D`? When...
Q02SENIOR
After deleting a remote branch with `git push origin --delete feature/fo...
Q03SENIOR
A developer accidentally force-deleted a feature branch with unmerged co...
Q01 of 03JUNIOR

What is the difference between `git branch -d` and `git branch -D`? When would you use each?

ANSWER
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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between git branch -d and git branch -D?
02
How do I delete both local and remote branch at the same time?
03
Why does git branch -r still show a branch I deleted remotely?
04
Can I recover a branch I accidentally deleted?
05
How do I delete all merged branches at once?
🔥

That's Git. Mark it forged?

5 min read · try the examples if you haven't

Previous
Git Checkout -b: Creating and Switching Branches
13 / 19 · Git
Next
Git Squash Commits: Combine Multiple Commits into One