@michael-baker-us/git-branch-cleaner
v1.0.0
Published
Audit and safely clean up local git branches: merged, gone, stale, abandoned.
Maintainers
Readme
git-branch-cleaner (gbc)
Audit a repository's local branches — merged, gone (upstream deleted),
stale, abandoned — and clean them up safely. gbc previews everything,
deletes nothing without an explicit flag or confirmation, and archives instead
of destroying by default.
$ gbc status
BRANCH STATUS LAST COMMIT AHEAD/BEHIND UPSTREAM
* main protected today - -
feature/dark-mode active today 2/0 (never pushed)
feature/sepa gone today 1/0 (deleted)
fix/rounding merged today 0/1 (never pushed)
2 branches safe to clean (gone, merged). Run: gbc cleanSafety model (read this first)
Deleting branches is destructive, so gbc is built defensively:
- Nothing is deleted without an explicit action.
statusis read-only.cleanrequires interactive confirmation, or--yesfor scripts. - Archive-before-delete. Before running
git branch -D foo,gbccreates a lightweight tagarchive/foo/<yyyy-mm-dd>at the branch tip.gbc restore foorecreates the branch from the newest archive tag. Opt out with--no-archive. --dry-runmutates nothing. It prints exactly what would happen.- Protected branches are never touched.
main,master,develop, andrelease/*are protected by default, as is the currently checked-out branch. This holds even with--yes. - No remote branches are ever deleted. v1 only deletes local branches and
prunes local remote-tracking refs (
prune-remotes).
Install
npm install -g @michael-baker-us/git-branch-cleaner # provides the `gbc` binary
# or run from a clone:
npm install && npm run build && node dist/index.js statusRequires Node 20+ and git on the PATH.
Commands
| Command | What it does |
| --- | --- |
| gbc status | Read-only audit table (the default command). |
| gbc clean | Interactively delete safe candidates; archives first. |
| gbc restore <branch> | Recreate a branch from its newest archive tag. |
| gbc prune-remotes | Preview + prune stale remote-tracking refs. |
gbc clean
Flow: show candidates → checkbox multi-select (all pre-checked) → type-to-confirm
→ per branch: create archive tag, then git branch -D.
| Flag | Effect |
| --- | --- |
| --dry-run | Print planned actions, change nothing. |
| --yes | Skip prompts (scripting); deletes all candidates. |
| --no-archive | Delete without creating recovery tags. |
| --include-stale | Also offer stale branches. |
| --include-abandoned | Also offer abandoned branches. |
Candidates default to merged + gone. In a non-interactive session (pipe/CI),
clean refuses to prompt and asks you to pass --yes or --dry-run.
Exit codes: 0 success / nothing to do · 1 user aborted · 2 git error.
Classification
Branches are classified in strict precedence order:
- protected — matches a protected pattern, or is the current branch.
- gone — upstream was tracked but has been deleted (
[gone]). - merged — merged into the default branch (see below).
- abandoned — older than
staleDaysand behind default by > 50 commits and never pushed. - stale — tip older than
staleDays(default 30). - active — everything else.
How "merged" is detected (and its limits)
Ancestry merge:
git branch --merged <default>catches normal and--no-ffmerges.Squash-merge heuristic:
git branch --mergedmisses squash merges, sogbcalso treats a branch as merged whengit cherry <default> <branch>reports no+lines — i.e. every unique commit on the branch already has a patch-equivalent in the default branch.Caveats you should know:
- Branches with zero unique commits are excluded from the heuristic (they would otherwise look "merged" spuriously).
- The heuristic compares per-commit patch IDs. A branch of several commits
squashed into one combined commit on the default branch will not always
be detected, because the individual patch IDs don't match the combined one.
Such branches remain
stale/active— safe, just not auto-flagged.
Configuration
Repo-level .gbcrc.yaml (all fields optional), overridable by flags:
staleDays: 30
protectedPatterns:
- main
- master
- develop
- release/*
defaultRemote: origin
defaultBranch: main # omit to auto-detect from origin/HEAD, then main/masterGlobal flags: --stale-days <n>, --remote <name>, --default-branch <name>,
--protect <glob> (repeatable; appended to the protected list, never
replacing the defaults).
Development
npm install
npm test # vitest against real temp git repos
npm run lint # eslint
npm run typecheck # tsc --noEmit
npm run build # emit dist/Tests build real temporary git repositories via a fixture helper and run the
actual command logic against them. Time is injected through a single clock seam
so relative dates and staleness are deterministic. All git access goes through
one injectable runGit wrapper, and only machine-readable
(--porcelain / --format, NUL-separated) output is ever parsed.
Non-goals (v1)
- No remote branch deletion (local branches + local tracking-ref pruning only).
- No GitHub/GitLab API usage — "merged" is decided by git ancestry, not PR state.
- No GUI/TUI beyond interactive prompts.
License
MIT
