skip to content

In Git, what is the difference between git branch -d and git branch -D?

level: juniorimportance: should knowfreq 58%

answer

  1. One of them asks a question first
  2. The question is about reachability
  3. Lowercase is polite, uppercase is not
  4. Neither one touches commit objects

basics

~20 s

git branch -d refuses to delete a branch whose commits are not already merged into its upstream or into HEAD; git branch -D is the forced form that deletes regardless. Both only remove the ref, never the commits.

solid answer

~40 s

`-d` is the safe delete: Git checks whether the branch tip is reachable from its configured upstream, or from `HEAD` when no upstream is set, and refuses with an error if commits would be left with no name pointing at them. `-D` is shorthand for `--delete --force` and skips that check entirely. Neither command deletes commit objects — they remove one ref under `refs/heads/`. After a forced delete the commits become unreachable but are still recoverable while a reflog entry survives, so `git reflog` plus `git branch <name> <hash>` puts the branch back. Two practical notes: you cannot delete the branch you are currently on, and the safety check is pure reachability, so a branch whose work was squashed or rebased into the target still looks unmerged and needs `-D`.

code

bash · 8 lines
bash
git switch main
git branch -d feature
# error: the branch 'feature' is not fully merged

git branch -D feature   # force: delete the ref anyway

git reflog              # find the old tip hash
git branch feature 9f2b6c1   # and it is back

go deeper

for a junior

Remember lowercase -d is the safe delete that can refuse, uppercase -D forces it, and neither removes commits from the repository.

for a middle

Explain the exact check: is the branch tip reachable from its upstream, or from HEAD when there is no upstream — and why squashed or rebased work fails that check despite having shipped.

for a senior

Treat the refusal as a diagnostic signal rather than an obstacle, and be able to walk someone back from a forced delete using the reflog hash to recreate the ref.

for a principal

Own what branch cleanup means under your integration model: on a squash- or rebase-based mainline the safe check always refuses, so the team needs an agreed verification step rather than a habit of forcing deletes.

## The two forms - `git branch -d <name>` — delete, but only if it is safe. - `git branch -D <name>` — force delete; equivalent to `git branch --delete --force`. The only difference is whether the merged-ness check runs. ## What "safe" means precisely `-d` succeeds when the branch tip commit is **reachable** from a reference point: - If the branch has a configured upstream, that upstream is the reference point. - If it has no upstream, `HEAD` is the reference point. "Reachable" means you can walk parent links from the reference commit and arrive at the branch tip. If you can, deleting the name loses nothing: every commit is still reachable through another ref. If you cannot, Git refuses and tells you the branch is not fully merged, because removing the name would leave those commits unnamed. This is a check about the graph, not about intent. Three common cases where it is technically right but feels wrong: 1. **Squash-merged work.** If the target branch received one new commit containing the combined changes, the original commits are not ancestors of it. `-d` refuses even though the content shipped. 2. **Rebased work.** Rebasing creates new commits with new hashes; the originals are unreachable, so `-d` refuses. 3. **Merged into a branch you are not on.** With no upstream configured and `HEAD` on some third branch, `-d` compares against the wrong reference point. Deleting from the branch you merged into, or passing the comparison explicitly via a `--merged` listing first, avoids the surprise. ## What deletion actually removes Deleting a branch removes the ref — the entry under `refs/heads/<name>` — and the branch's own reflog. It removes **no commit objects**. Those commits remain in the object database. As long as some reflog entry or other ref still reaches them, `git reflog` will show the hash and `git branch <name> <hash>` recreates the branch exactly. Only when a commit is unreachable from every ref and every surviving reflog entry does garbage collection eventually discard it, which is why "I force-deleted a branch, is my work gone?" is usually answerable with "no, find the hash". ## Restrictions You cannot delete the branch that `HEAD` currently points at — switch away first. In a repository using multiple worktrees, a branch checked out in another worktree is likewise protected. ## Deleting the remote-tracking copy `git branch -d -r origin/feature` deletes the local remote-tracking ref only. It does not touch anything on the server and it does not affect your local `feature` branch; the next fetch will recreate it if the branch still exists on the remote. Removing stale remote-tracking refs wholesale is `git fetch --prune`. Keep these mentally separate from deleting a local branch — they are three different objects with three different lifetimes. ## A workflow habit worth stating Use `-d` by default. Its refusal is information: it is telling you the commits under that name are not reachable from anywhere else, which is either a mistake (you have not merged yet) or an expected consequence of squashing or rebasing. Reaching straight for `-D` throws away that signal. When `-d` refuses and you have verified the work is elsewhere — for example the change is present in the target branch under a different hash — then `-D` is the correct answer, not a workaround. ## Related listing flags `git branch --merged` and `git branch --no-merged` answer the same reachability question in bulk, which is how you decide what is safe to delete before running anything destructive. `git branch --contains <commit>` inverts it: which branches can reach this commit. ## Recovering from a mistake Run `git reflog` to find the tip hash the branch had, then `git branch feature <hash>` to recreate it. The branch reflog is gone with the branch, but `HEAD`'s reflog records every checkout and commit you made while on it, so the tip hash is normally sitting right there.

  • Why does git branch -d refuse after the work was squash-merged into main?
    The check is reachability, not equivalence of content. A squash produces one new commit with a new hash, so the original commits are not ancestors of `main`. The change shipped, but those specific commits are unreferenced, so `-d` correctly reports them as unmerged and `-D` is the right call.
  • After git branch -D, is the work recoverable?
    Usually yes. Deletion removes the ref, not the commits. `git reflog` still shows the hashes you visited on that branch, and `git branch <name> <hash>` recreates it. Recovery only fails once the commits are unreachable and garbage collection has run.
  • What does git branch -d -r origin/feature do?
    It deletes only your local remote-tracking ref for that branch. Nothing changes on the server, and the ref reappears on the next fetch if the branch still exists there. Removing stale remote-tracking refs in bulk is `git fetch --prune`.

saying these in an interview costs you the question

  • Says -D deletes the commits and -d does not
  • Thinks deleting a local branch deletes it on the remote
  • Reaches for -D by reflex whenever -d complains
  • Believes a squash-merged branch will pass the -d check
  • Claims a force-deleted branch is unrecoverable

context