Skip to main content

git-cleanup

PyPI version Python versions CI License: MIT

Interactively clean up git branches that are merged, done in your issue tracker, or stale.

git-cleanup fetches and prunes origin, gathers every local and remote branch (author, age, merged status, ahead/behind counts vs upstream, and linked issue status), then opens a full-screen TUI with three tabs. Branches is one table of all branches where each row carries an action you control. Worktrees lists every git worktree with its branch, its count of uncommitted changes, and whether it is broken or locked. Stashes lists every stash with its message, origin branch, age, and file count, alongside a live diff pane so you can read a stash before deciding its fate.

Branches

  • delete — removes the branch locally and on origin (whatever exists). Your branches that are merged or whose issue is done come pre-marked.
  • delete-local — removes only your local branch and leaves origin/<branch> in place. For treating local branches as your active workspace: clear one off your machine because the remote still has it. Nobody else is affected. Press d a second time to get here. Branches that exist on only one side skip this action — delete is already single-sided.
  • archive — creates a tag archive/<branch> at the tip (pushed for remote branches), then deletes the branch. Restore any time with git checkout -b <branch> archive/<branch>.
  • keep — the default; nothing happens.

Worktrees

  • remove — runs git worktree remove, with --force when the worktree has uncommitted changes. A worktree whose directory is already gone is cleared with git worktree prune instead. Worktrees whose branch is merged or issue-done come pre-marked, as do broken entries — but a worktree with uncommitted changes never does.
  • keep — the default; nothing happens.

Removing a worktree never deletes its branch — do that on the Branches tab. Worktree removals run before branch deletions, so marking both in one session works: git branch -d refuses a branch that is checked out anywhere, and the review screen tells you which deletions are waiting on which removal.

Stashes

  • drop — discards the stash without restoring it. Recoverable: git keeps the commit reachable until the next gc, and the review screen prints the git stash store <sha> you would need.
  • pop — restores the stash into your working tree, then removes it from the list.
  • apply — restores it but keeps it in the list.
  • keep — the default; nothing happens.

Stashes are never pre-marked — a stash is uncommitted work by definition, there is no "merged" signal to key off, and an old stash is exactly the one you forgot about but may still want. Only one pop or apply per run is allowed: a restore is the only action here that writes to your working tree, and a failed one leaves it dirty, which would make every restore after it fail too. Drops are unlimited.

Press Enter to review everything grouped (with a prominent warning for anything deleted on origin or removed with --force), confirm, and it executes. Quit with q and nothing changes.

Keys

Key Action
↑/↓, PgUp/PgDn Move
[ / ] Previous / next tab
b / w / t Jump straight to Branches / Worktrees / Stashes
space Cycle keep → delete → delete-local → archive (Worktrees: toggle keep ⇄ remove; Stashes: cycle keep → drop → pop → apply)
d Mark delete; press again to toggle between delete and delete-local (Worktrees: mark remove; Stashes: mark drop)
a / k Mark archive / keep (Stashes: a marks apply)
p Mark pop (Stashes only)
o Open the branch's compare page on origin (vs the default branch)
/ Live filter (same syntax as --filter)
s Live sort (same syntax as --sort)
r Reset filter & sort to defaults
Ctrl+D / Ctrl+U Scroll the stash diff pane
Enter Review and confirm
q / Esc Quit without changes

The footer always shows the keys for the tab you are on, and advertises [/] for tab switching; b/w/t work too but stay out of the footer to keep it to one line. /, s, r, and o are branch-only — worktree and stash lists are short enough not to need filtering or sorting, and stashes must never be reordered, because their stash@{N} numbering is positional.

On the Stashes tab the diff pane sits to the right of the table at 100 columns or wider, and moves below it on narrower terminals so the table always has room for every column.

Filter and sort changes are remembered per repository, so your view comes back the next time you run git-cleanup there. r resets (and forgets) them.

A delete-local branch still exists on origin, so the next scan lists it again as a remote-only row. If you treat local branches as your active workspace, filter to local (press /, type local) to see only branches you actually have checked out — that filter is remembered per repo, so the branches you have cleared off your machine stay out of view.

Jira is the built-in issue tracker for now; the provider layer is designed so GitHub Issues, Linear, etc. can be added later. The scan pipeline (git_cleanup.core.scan_repo

  • planner.recommend_actions) is UI-free and importable, so CI jobs can generate branch reports from the same data.

Install

Requires Python 3.14+ and git.

$ uv tool install git-cleanup   # install the CLI on your PATH
$ uvx git-cleanup               # or run it without installing

You can also install it with pip install git-cleanup if you prefer.

Usage

$ uvx git-cleanup            # in any git repo with an origin remote
$ uvx git-cleanup --dry-run  # preview everything, change nothing

Flags

Flag Effect
--dry-run Full run with zero mutations — prints [dry-run] would delete ... instead
--no-fetch Skip the initial git fetch --prune origin
--all Pre-mark other authors' cleanup-eligible branches for deletion too
--sort COLS Sort columns, comma-separated, - prefix for descending — e.g. --sort=-age,status,author. Columns: branch, local, remote, worktree, sync, author, age, merged, issue, status
--filter TERMS Only show branches matching all terms — e.g. --filter 'mine,age>6m,status!=done'. A bare word matches any text column (--filter brent). Flags: mine, merged, local, remote, gone, worktree (prefix ! to negate); age>N/age<N/age>=N/age<=N in days or with d/m/y suffix; substring matches branch=X, author=X, issue=X, status=X (!= excludes). `
--config PATH Use an alternate config file
--version Print the version

Interactive runs default to your last-used filter and sort in that repository; an explicit --sort/--filter wins for that session without overwriting the saved view. Non-interactive runs (pipes, CI) use only explicit flags.

worktree is a boolean filter flag, so the bare word worktree now means "has a worktree" rather than a substring search across the text columns, and !worktree means "has no worktree". To search for the literal string, use branch=worktree.

Branches are matched to issues by extracting an issue key (e.g. ABC-123) from the branch name, case-insensitively. Branches without a key just show no issue info.

Safety

  • The current branch, the default branch, and protected branches can never be marked for deletion or archiving.
  • Nothing happens until you review the grouped summary and confirm it; quitting the TUI changes nothing.
  • Remote deletions are called out in their own red-bordered warning on the review screen, and local deletions that would lose unpushed commits are flagged.
  • delete-local rows never appear in that warning (nothing leaves origin); they are listed with the origin/<branch> they are keeping.
  • Non-interactive runs (pipes, CI) never mutate anything — they print the tables and exit.
  • If Jira is unreachable or unconfigured, the tool degrades to git-only info (merged status still works).

For worktrees specifically:

  • The main worktree, the worktree you are currently in, and locked worktrees can never be marked — git cannot remove them, and the tab says so when you try.
  • A worktree with uncommitted changes can be marked by hand, but it is never pre-marked, it is flagged in its own red-bordered panel on the review screen, and removing it passes --force, which discards that work irrecoverably.
  • git worktree prune is repo-wide, so clearing one broken entry clears them all. The dry-run note and the summary report git's own list rather than the count you marked.
  • Worktree removals execute before any branch deletion, and removing a worktree never deletes its branch.
  • If listing worktrees fails for an unrelated reason, the run degrades to branches-only with a warning on stderr.

For stashes specifically:

  • Nothing is ever pre-marked, and only one pop/apply is allowed per run.
  • A stash@{N} selector is a reflog position, not an id: dropping stash@{1} renumbers {2} to {1}. Marked stashes are therefore executed in descending index order, so the ones not yet touched never move, and each is re-checked against the commit it pointed at during the scan — a stash that changed underneath you (say, popped in another terminal) is skipped with a warning rather than acted on.
  • Dropping is recoverable until git's next gc: the review screen prints the git stash store <sha> that would bring one back.
  • git allows restoring a stash onto a different branch than it was made on with no warning of its own, so the tab colors the mismatch and the review screen spells it out.
  • A failed pop always leaves the stash in the list. If it conflicted, the conflict markers are in your files and the stash is still there to retry or drop by hand.
  • A restore lands in whichever worktree you ran git-cleanup from, since refs/stash is repo-global but the restore writes to the current working tree.
  • If listing stashes fails, the run degrades to no Stashes tab with a warning on stderr.

Configuration

~/.config/git-cleanup/config.toml (respects $XDG_CONFIG_HOME):

[tracker]
provider = "jira"          # or "none" to disable issue lookups

[jira]
url = "https://yourcompany.atlassian.net"
email = "you@yourcompany.com"
api_token = "..."          # create one at https://id.atlassian.com/manage-profile/security/api-tokens

[cleanup]
protected_branches = ["main", "master", "develop"]
done_statuses = []         # extra status names to treat as done, e.g. ["Won't Do"]
archive_age_days = 90      # minimum age for the archive prompt

Any setting can be overridden per repository with a [repos."<path>"] table in the same file — the key is the repository's root path (~ expands), and its values merge key-by-key over the global sections:

[repos."~/Code/some-repo".cleanup]
protected_branches = ["main", "staging"]
archive_age_days = 30

[repos."~/Code/other-repo".tracker]
provider = "none"

Environment variables JIRA_URL, JIRA_EMAIL, and JIRA_API_TOKEN override the config file, including repo overrides.

The Worktrees tab adds no configuration of its own: archive_age_days also drives the stale highlight on its Age column, and since that is already per-repo overridable, a monorepo with long-lived worktrees just raises it. Note that protected_branches does not prevent removing a worktree — removing a checkout never touches a ref — but a protected branch's worktree is never pre-marked, only manually markable.

Worktree listing uses git worktree list --porcelain -z (git 2.36+), falling back to the newline form on older git.

Filter/sort views chosen in the TUI are saved per repository in $XDG_STATE_HOME/git-cleanup/state.json (default ~/.local/state/git-cleanup/state.json).

Development

$ uv sync
$ uv run pytest
$ uv run git-cleanup --dry-run

Tests run against real temporary git repositories and a mocked Jira API — no network or credentials needed.

With just installed, just format, just lint, and just test wrap the common tasks, and just pre_commit runs all three. Releases are cut with just version_bump <major|minor|patch> followed by pushing the tag — see CLAUDE.md for the full release process.

License

MIT. Provided as is, without warranty of any kind — see the LICENSE file for the full text.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

git_cleanup-0.2.0.tar.gz (44.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

git_cleanup-0.2.0-py3-none-any.whl (51.3 kB view details)

Uploaded Python 3

File details

Details for the file git_cleanup-0.2.0.tar.gz.

File metadata

  • Download URL: git_cleanup-0.2.0.tar.gz
  • Upload date:
  • Size: 44.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for git_cleanup-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c52721272effcf91d0387d190d3c57205dee0daf0ba3ff6ce898357647788f82
MD5 7e1c947cce10635e56a6cbf770f57ecd
BLAKE2b-256 6a8c18abab121d0b67fcf1e1a740f9e8d9de29e5cb1ef5fee9ca6b815c559bbe

See more details on using hashes here.

Provenance

The following attestation bundles were made for git_cleanup-0.2.0.tar.gz:

Publisher: release.yml on epicserve/git-cleanup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file git_cleanup-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: git_cleanup-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 51.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for git_cleanup-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b8409bbfc230c6fbc1a28defd7f6aff8da352d9b145d6639b78e66c304f29894
MD5 a4eee233d00d6977f4a40409e5945ab3
BLAKE2b-256 e36572ca3bc9ebe72a67cd61d0a74de7878a7d2aa04c6b665eb85dc8c7d23e4a

See more details on using hashes here.

Provenance

The following attestation bundles were made for git_cleanup-0.2.0-py3-none-any.whl:

Publisher: release.yml on epicserve/git-cleanup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.0

2 files

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page