基于补丁的合并检测
通过比较补丁识别已合并代码,即使提交历史无法直接证明。
git-barberv0.3.0
Git 分支清理 CLI
找出改动已进入基准分支的本地分支。在终端中检查提交与差异,再确认要删除的分支。
Rust · Apache-2.0 · 最近更新
git barber --list
✓ feat/invoice-export squash
✓ fix/callback-retry merged
✓ chore/update-sdk rebase
○ experiment/new-ledger gone
git-barber
通过比较补丁识别已合并代码,即使提交历史无法直接证明。
TUI 展示提交和差异统计。仅上游分支消失不会默认选中该分支。
使用键盘选择器、只读列表或 JSON 输出。远程删除需单独启用,并在删除前检查远程分支是否已变化。

Git 分支清理 CLI
在仓库中运行 git barber --list,再打开 TUI 检查候选分支。
查看预览,保留未完成工作,只确认要删除的分支。
01 / 安装
brew install rekurt/tap/git-barber02 / README
识别通过 merge、squash 或 rebase 合并的分支,在终端中检查提交并确认删除。
配置说明、使用示例和 API 参考。
以下为仓库的原始文档,未作翻译;上方的示例与入门说明已本地化。
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.
Project website · All projects by rekurt

From 0.2.0 onwards, Homebrew (macOS and Linux):
brew install rekurt/tap/git-barber
Or a prebuilt binary, no Rust toolchain needed — grab the archive for your
platform from the
latest release and put
git-barber on your PATH. Archives also ship the completions and the man
page.
With an existing Rust setup, cargo binstall downloads the same prebuilt
binary instead of compiling:
cargo binstall git-barber
Or build from source:
cargo install git-barber
The binary is named git-barber, so git picks it up as a subcommand: run it
as git barber.
Homebrew installs completions and the man page for you. Otherwise generate them yourself — for zsh:
mkdir -p ~/.zfunc && git-barber --completions zsh > ~/.zfunc/_git-barber
then, in .zshrc before compinit runs:
fpath+=~/.zfunc
autoload -U compinit && compinit
bash, zsh, fish, powershell and elvish are supported. Completions
register against the git-barber executable — as git barber, git drives its
own completion machinery instead.
For automatic cleanup after a GitHub PR merge, see the GitHub Actions integration.
git barber # TUI: pick branches, confirm, done
git barber --list # dry-run listing (also the default outside a TTY)
git barber --json # machine-readable listing
git barber --yes # delete all verified candidates, no questions
git barber --yes --json # ...with a JSON report of what happened
| 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>.
j/k/arrows move · g/G top/bottom · space toggle · a all ·
n none · r arm remote deletion for the highlighted branch ·
p show/hide the preview panel ·
enter confirm · in the dialog y executes, n/q/esc cancel ·
q/Ctrl+C quit.
The preview panel shows the commits the highlighted branch carries that the
base does not, plus its diffstat — so you can see what a deletion would
actually cost before confirming it. It sits beside the list on terminals 100
columns or wider and underneath on narrower ones, loads lazily as the cursor
moves, and remembers what it has already fetched. Turn it off with
git config barber.preview false.
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.
| 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 of local branches; pre-arms the r toggle |
--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 |
--no-cache |
ignore and do not update the verdict cache; re-verify everything |
--completions <SHELL> |
print a shell completion script and exit; needs no repository |
Exit codes: 0 ok / nothing to do · 1 some deletions failed · 2 usage or
environment error.
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:
git config --add barber.protect 'release/*'
or per invocation with --protect. Globs support any number of *
wildcards (release/*, *-keep-*).
Detection is dominated by the patch-id probes, which are independent per
branch, so the scan runs them across several workers and shows a counter on
stderr while it works (a terminal only — piped output stays byte-for-byte
clean). Verdicts are then cached in .git/barber/cache.json, keyed on the
base tip, the fork point and the branch tip together: if any of the three
moved, the branch is probed again rather than trusted. Only entries the last
run actually used are written back, so the file stays the size of your branch
list.
Measured on a synthetic 200-branch repository (30 distinct fork points, every branch squash-merged), on an 18-core machine with git 2.50.1 — treat the ratios as the point, not the absolute seconds:
| workers | cold | warm cache |
|---|---|---|
| 1 | 11.1s | — |
| 4 | 4.6s | — |
| 8 | 3.5s | 1.3s |
git config barber.jobs 4 # default: CPU count, capped at 8
A repository you cannot write to simply pays full price every scan — the
cache is an optimisation and never an error. To force a full re-verification,
pass --no-cache or delete .git/barber.
The cache pays off most for repeated runs against an unchanged base. When the
base moves — after any git fetch that brings in new commits — every verdict
is recomputed, because what the base gained since the fork point is exactly
what squash and rebase detection compares against.
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, or upstream was gone.--include-gone.--remote (or the per-branch r toggle)
deletes with --force-with-lease against 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 candidates.
Press Enter and answer y to delete, or n to leave them intact. They
are never removed by --yes.git branch <name> <sha> / git push origin <sha>:refs/heads/<name>),
and the report survives the TUI (reprinted after exit).--no-cache, or deleting .git/barber,
forces a full re-verification.--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).
git branch --merged | xargs git branch -d — misses squash and rebase
merges, no confirmation step, no undo hints.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.
cargo test # unit + e2e (e2e builds real throwaway repos in tempdirs)
cargo clippy --all-targets -- -D warnings
cargo fmt --check
The codebase is deliberately small: scan.rs (detection), ops.rs
(deletion), tui/ (pure state machine + rendering), git.rs (the
subprocess seam, faked in tests), cache.rs (cross-run verdict cache),
parallel.rs (ordered parallel map), progress.rs (the scan counter) and
config.rs (git config barber.*). Start reading at src/main.rs.
MIT or Apache-2.0, at your option.
git-barber