Skip to main content

Find and delete merged & stale git branches, safely. Dry-run by default, never touches main/master/develop/current, handles local + remote. Zero dependencies.

Project description

branchtidy

Delete merged & stale git branches — safely. branchtidy finds the local (and optionally remote) branches that are already merged, or haven't seen a commit in N days, previews them, and deletes them in one batch. It is dry-run by default, refuses to touch main / master / develop / your current branch, and won't nuke unmerged work unless you explicitly ask.

Zero dependencies (pure Python stdlib). Zero config. No daemon, no account.

pipx run branchtidy
branchtidy  local branches  ·  default main  ·  stale > 90d

  BRANCH            LAST COMMIT   MERGED   ACTION
  feature/login     12d ago       yes      delete (merged)
  feature/old-poc   210d ago      no       delete (stale 210d)
  main              2d ago        no       keep (protected)
  feature/wip       3d ago        no       keep (active)

Dry run. 2 branch(es) WOULD be deleted. Re-run with --delete to apply.

Nothing was deleted. That's the point — you read the table, then decide.

Why another branch cleaner?

Everyone reinvents this as a throwaway git branch --merged | grep -v ... | xargs one-liner, and those one-liners are exactly how people delete branches they wanted. branchtidy's whole pitch is safety + zero config:

  • Dry-run is the default. No flags → it only prints what it would do.
  • Real deletion is gated twice: --delete and an interactive confirm (skip the prompt only with --yes).
  • Protected branches are never candidates: main, master, develop, the current HEAD, plus anything you pass to --protect.
  • Merged vs unmerged is respected. Merged branches use the safe git branch -d. Unmerged branches are only deletable with an explicit --force (which maps to git branch -D).
  • Remote deletion is double-gated: it requires --remote --delete and its own confirmation, and uses git push <remote> --delete.

When in doubt, branchtidy keeps the branch.

Install

pipx run branchtidy        # no install, run on demand
pip install branchtidy     # or install the `branchtidy` command

There's an identical Node build too: npx branchtidy / npm i -g branchtidy (see branchtidy). Both ports share one selection-vector table, so they make byte-for-byte identical decisions.

Usage

branchtidy [options]            # dry-run preview (default — deletes nothing)
branchtidy --delete             # actually delete, after a confirm
Option Description
--delete Perform deletion. Without it, branchtidy only previews.
--yes Skip the interactive confirm (use with --delete, e.g. in scripts).
--stale <dur> Staleness threshold. Default 90d. Accepts 30d, 2w, 12h, 45m, 30s, or a bare number (days).
--merged-only Only delete merged branches; never delete on age alone.
--remote [name] Operate on remote-tracking branches (default remote: origin).
--protect <a,b> Extra branch names to never delete (comma-separated).
--force Allow deleting unmerged branches (maps to git branch -D).
--json Machine-readable output; never prompts, never deletes (preview only).
--no-color Disable ANSI colors.
-h, --help Show help.
-v, --version Print version.

Exit codes: 0 success/clean, 1 one or more deletions failed, 2 usage or environment error (e.g. not a git repo).

Examples

# what WOULD be cleaned up, right now?
branchtidy

# stricter window, only merged branches, do it (with a confirm)
branchtidy --stale 30d --merged-only --delete

# clean up gone-stale remote branches on origin (double-gated + confirm)
branchtidy --remote origin --delete

# delete unmerged stale branches too — you have to ask for it
branchtidy --stale 180d --delete --force

# protect a couple of long-lived branches by name
branchtidy --protect release/v1,staging --delete

# pipe the plan somewhere
branchtidy --json | jq '.toDelete'

How it decides

For each branch branchtidy looks at: is it the current HEAD? is it protected? is it merged into the default branch? how old is its last commit? Then:

  1. current branch → keep (current)
  2. protected (default set or --protect) → keep (protected)
  3. merged → delete (merged)
  4. otherwise, if older than --staledelete (stale <N>d)
  5. otherwise → keep (active)

In --merged-only mode, step 4 is skipped entirely — age never causes a deletion.

The default branch is resolved from origin/HEAD when available, otherwise it falls back to main, then master.

Design notes

  • One pure function at the core. select_branches(branches, policy, now_ms) has no git, no fs, no clock — it's a pure data→data transform that returns {toDelete, toKeep} with a reason on every branch. The CLI is a thin git wrapper around it. That's what makes the Node and Python ports verifiably identical: they run the same vector table.
  • Time is integer math. Ages are computed from committerdate:unix against a single captured now in milliseconds — no datetime parity to worry about between languages.
  • Safe by construction. Protected and current branches are filtered out before any staleness logic runs, the staleness test is a strict > (a branch exactly at the threshold is kept), and deletion always passes through the safe git branch -d unless you opt into -D with --force.

License

MIT

Project details


Download files

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

Source Distribution

branchtidy-0.1.0.tar.gz (14.5 kB view details)

Uploaded Source

Built Distribution

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

branchtidy-0.1.0-py3-none-any.whl (11.8 kB view details)

Uploaded Python 3

File details

Details for the file branchtidy-0.1.0.tar.gz.

File metadata

  • Download URL: branchtidy-0.1.0.tar.gz
  • Upload date:
  • Size: 14.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.7

File hashes

Hashes for branchtidy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 50c24a9d0193594d001379f533dbd922213b706916d6ccb3028dd2f85d3a6a76
MD5 40906ad1182df9b0d44252c551f40747
BLAKE2b-256 a3fd3a38c3d74d44e02a2f1c6a540f596524af0748e427be8388d019c835dd6f

See more details on using hashes here.

File details

Details for the file branchtidy-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: branchtidy-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 11.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.7

File hashes

Hashes for branchtidy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a026ba2f6bc46216047ace817e781d63658171de9150cec4944c5ef6cbb02ed2
MD5 f0415f09dadd724479f7aec2965c3f0e
BLAKE2b-256 48fd3f56c0edcfa2a3d08f110c1a38e1fb32365159276a07faf6087bd4bad830

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page