git cleanup
Preview and remove redundant Git branches and their clean linked worktrees.
Works in any repository without a hosting service. Requires Git
2.40+ and Python 3.11+. GitHub PR evidence optionally uses gh.
git cleanup # explain what can go and what stays
git cleanup --apply # refresh remote refs, back up, then delete
git cleanup --local # local branches only; no network
git cleanup --fetch # refresh remote refs before previewing
git cleanup --into feature/combined # also recognize squashed work in this branch
git cleanup --github # consult merged and open GitHub PRs
git cleanup --scan-history 200 # also search historical integration results
git cleanup --obsolete old-spike # explicitly abandon a local branch
git cleanup --obsolete origin/spike # explicitly abandon a remote branch
git cleanup --protect 'release/*' # retain additional branches
git cleanup --json # machine-readable plan
git cleanup --no-color # plain-text output
git cleanup --color=always # force color, including in a pipe
Add this directory to PATH, or symlink its executable into a directory already
on PATH. The executable uses python3 from PATH, which must be version 3.11
or newer. From this checkout you can also run ./git-cleanup --help.
Human-readable plans use color automatically on terminals: red for deletion,
green for retention, and highlighted headings and recovery information. Use
--color=auto|always|never to choose a mode; bare --color means always, and
--no-color means never. In automatic mode, piped or redirected output,
TERM=dumb, and a nonempty NO_COLOR environment variable disable color.
Explicit color options take precedence over the environment. JSON output
always remains free of color codes.
What counts as redundant
A branch is eligible when its tip is an ancestor of the integration baseline, or its complete net changes have the exact same file contents and modes at the baseline tip. The latter recognizes many squash merges and cherry-picks, including binary files, renames, and deletions. It deliberately declines source histories containing merges, repeated trees (including empty commits), wholly reverted paths, or more than 1,000 exclusive commits. Partial incorporation and later edits remain protected. Content matching proves the resulting files; it does not prove that commit messages or authorship were preserved.
Local branches incorporated by ancestry into another retained local branch can
also go. The receiving branch is kept for this run. --into REF additionally
permits content matching against an explicitly retained recipient. Merely
publishing a branch to its remote counterpart is not cleanup evidence.
By default content matching examines recipient tips only. --scan-history N
opts into checking the latest N commits reachable from each recipient, allowing
cleanup after landed files have changed again. This is a broader inference;
inspect the reported historical commit before applying it.
--github queries open PRs and up to 1,000 merged PRs in the selected GitHub
remote using gh. It aborts if the open-PR query reaches its 1,000-record limit.
A merged PR proves only its exact recorded head, and its merge commit must be
reachable from a retained recipient. Open PR branches are kept. Cross-repository
PRs are excluded. Query failures abort cleanup. Closed but unmerged PRs, deleted
upstreams, old timestamps, and branch names do not by themselves prove anything.
Use --obsolete to explicitly select abandoned branches; this bypasses proof,
but still preserves protected branches and unsafe worktrees.
Baseline and remote
--base REF overrides cleanup.base. Otherwise cleanup.mainBranch, the selected
remote's cached default branch, then main or master identify the integration
branch. Its selected-remote tracking ref is preferred over its local ref.
Without a recognizable integration branch, use --base or configure
cleanup.mainBranch. Empty option values, cleanup.base, and
cleanup.mainBranch settings are errors.
The selected remote is --remote NAME, otherwise origin, otherwise the only
configured remote. Multiple remotes without origin require a selection.
Only this remote's branches are considered. --local disables remote branch
cleanup and all network calls, using the local integration branch by default.
The default preview does not write files, fetch, or delete anything. Cached
remote refs may be stale. --fetch explicitly refreshes and prunes the selected
remote's tracking refs even during a preview; --apply does so automatically.
Remote deletion requires incorporation into a retained branch on that remote
(or explicit --obsolete), and a single matching fetch/push URL. Remote tips
and recipient tips are checked live before deletion. The resolved endpoint is
pinned for each operation, including when Git URL rewrites are configured.
Each push uses an explicit
expected-object lease, so a branch updated since inspection is not deleted.
Fetches and pushes explicitly exclude tags and recursive submodule operations,
even when enabled in your ordinary Git configuration. Command-scoped settings
such as git -c cleanup.protect=topic cleanup --apply are honored.
Safeguards and recovery
The current and primary worktrees, main, master, the integration/default
branch, --base, and --into refs are protected. Repeat --protect GLOB, or
configure repeated cleanup.protect values, for other long-lived branches.
Detached, locked, missing, dirty, untracked, ignored, or operation-in-progress
worktrees are retained. Ignored build output is retained too: move or remove
it yourself before cleanup. Stale worktree registrations are left for manual
inspection. Worktrees with assume-unchanged or skip-worktree index entries
(including sparse checkouts) are retained because these can hide edits from Git.
No worktree removal uses force.
Before deletion, every selected tip and its proof recipient get recovery refs
under refs/cleanup/<run>/. An execution journal is written under the common
Git directory at git-cleanup/<run>.json. These keep commits reachable through
normal garbage collection. Recover a branch with:
git for-each-ref refs/cleanup/
git branch recovered refs/cleanup/<run>/heads/old-branch
Remote tips are under refs/cleanup/<run>/remotes/<remote>/<branch>. A recovered
remote branch can be pushed normally. Worktree directories can be recreated
with git worktree add; their ignored/untracked data is never discarded.
Recovery refs are retained until you deliberately delete them. Branch-specific
Git configuration is left intact, avoiding a race with branch recreation.
Run cleanup while other Git operations and editors are idle. Tips, recipients, and worktree state are rechecked during apply; local ref deletion uses an expected-object transaction. Recipient proofs use one captured object, and transaction locks protect the final check for symbolic-ref changes. Worktree removal and remote operations cannot form one atomic transaction. An error stops execution, leaves recovery refs and the journal, and may leave a partially completed cleanup. Shallow repositories, partial clones (including promisor repositories), and legacy graft files are refused; replacement objects are ignored for proof. Partial clones must be replaced with complete clones so inspection never fetches missing objects.
Development
python3 -m unittest discover -s tests -v
Tests use disposable repositories and local bare remotes; they do not delete real repository branches or contact a hosted service.