git-barber
git-barber trims your stale branches. It finds local branches that are
already merged — including GitHub-style squash and rebase merges that
git branch --merged can't see — shows them in a TUI with safe defaults, and
deletes the ones you confirm. Optionally it deletes their remote counterparts
too, with a lease so nobody's fresh commits are ever swept away.

Install
The binary is named git-barber, so git picks it up as a subcommand: run it
as git barber.
Usage
How branches are classified
| badge | meaning | confidence | default |
|---|---|---|---|
merged |
tip is an ancestor of the base branch | high | selected |
squash |
the branch, squashed into one diff, matches a patch already in base | high | selected |
rebase |
every branch commit's patch already exists in base individually | high | selected |
gone |
the tracked upstream was deleted; the branch itself may be unmerged | low | not selected |
Squash and rebase detection use git patch-id --stable: the whole-branch
diff (what a GitHub Squash and merge lands) and the per-commit patches
(what Rebase and merge lands) are compared against everything the base
gained since the fork point. Detection is pure reads — no objects are
written, so --list works even on read-only repositories.
The base branch is origin/HEAD when set, then origin/main,
origin/master, main, master; override with --base <ref>.
TUI keys
j/k/arrows move · g/G top/bottom · space toggle · a all ·
n none · r arm remote deletion for the highlighted branch ·
enter confirm · in the dialog y executes, n/q/esc cancel ·
q/Ctrl+C quit.
On the results screen j/k scroll; the full report (with undo commands)
is reprinted to the normal terminal after exit, so nothing is lost with the
alternate screen. Deletions run synchronously; the title shows the branch
in flight.
Flags
| flag | effect |
|---|---|
--base <REF> |
compare against this ref instead of the auto-detected base |
--list |
print candidates and exit; alias --dry-run; conflicts with --yes |
--json |
JSON output, for scripts |
--yes |
non-interactive deletion of merged + squash + rebase candidates |
--include-gone |
with --yes: also delete gone branches (requires --yes) |
--remote |
also delete remote counterparts for selected local branches; pre-arms their r toggle in the TUI |
--protect <P> |
extra protected names/globs, e.g. --protect 'release/*' |
--fetch |
run git fetch --prune first (non-fatal when offline) |
-C <PATH> |
operate on a repository at PATH |
Exit codes: 0 ok / nothing to do · 1 some deletions failed · 2 usage or
environment error.
Protected branches
main, master and develop are never touched, along with the current
branch, the base, its local twin, and branches checked out in any worktree.
Add your own patterns per repository:
or per invocation with --protect. Globs support any number of *
wildcards (release/*, *-keep-*).
Safety
- Compare-then-delete. Deletion verifies the branch tip still equals the sha you saw at scan time; a branch that advanced meanwhile (another terminal, an editor's git integration) is refused with a "rescan" hint.
- Merged branches use plain
git branch -d. When git refuses only because it judges merged-ness against HEAD rather than the base, git-barber re-verifies the ancestry itself and retries with-D. Every forced deletion is labeled with its justification:verified merged into base,patch-id verified, orupstream was gone. - Squash/rebase candidates are verified by patch-id and selected by default;
gone candidates never are — they need a manual check in the TUI or an
explicit
--include-gone. - Remote deletion is leased.
--remote(or the per-branchrtoggle) deletes with--force-with-leaseagainst your tracking ref: if anyone pushed after your last fetch, the deletion is rejected. The confirm dialog shows the actual remote ref being deleted, which can differ from the local branch name. - Remote-only cleanup is manual. Branches that are merged into the base
but have no local twin always appear as preselected
remotecandidates. PressEnterand answeryto delete, ornto leave them intact. They are never removed by--yes. - Every deletion prints an undo command
(
git branch <name> <sha>/git push origin <sha>:refs/heads/<name>), and the report survives the TUI (reprinted after exit). - Scanning never touches the network unless you pass
--fetch(remote deletion, when you ask for it, obviously pushes). Stale remote refs make detection more conservative and make leased remote deletion refuse — never the other way around.
Known limitations: a squash merge whose conflicts were resolved during the
merge produces a different patch and is not detected (the branch usually
surfaces as gone instead); shallow clones disable squash/rebase detection;
Ctrl+Z suspend is not handled; SIGKILL during the TUI can leave the
terminal in raw mode (reset fixes it).
Comparison
git branch --merged | xargs git branch -d— misses squash and rebase merges, no confirmation step, no undo hints.- git-trim — similar detection ideas, more configuration surface, no TUI, libgit2-based; last released in 2022.
git-barber shells out to your system git (like gh does), so hooks and
config behave the way your git does. Two deliberate exceptions: repo-selecting
environment variables (GIT_DIR & co) are cleared so -C always wins, and
interactive credential prompts are disabled for every git call except
--fetch — remote deletion needs a non-interactive credential helper
configured, and fails fast instead of hanging when there is none.
Development
The codebase is deliberately small: scan.rs (detection), ops.rs
(deletion), tui/ (pure state machine + rendering), git.rs (the
subprocess seam, faked in tests). Start reading at src/main.rs.
License
MIT or Apache-2.0, at your option.