Metadata-Version: 2.4
Name: branchtidy
Version: 0.1.0
Summary: Find and delete merged & stale git branches, safely. Dry-run by default, never touches main/master/develop/current, handles local + remote. Zero dependencies.
Author: yyfjj
License: MIT
Project-URL: Homepage, https://github.com/jjdoor/branchtidy-py
Project-URL: Repository, https://github.com/jjdoor/branchtidy-py
Project-URL: Issues, https://github.com/jjdoor/branchtidy-py/issues
Keywords: git,branch,branches,cleanup,prune,stale,merged,cli,devtools,git-branch
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Version Control :: Git
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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.

```bash
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

```bash
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](https://github.com/jjdoor/branchtidy)). Both ports share one
selection-vector table, so they make byte-for-byte identical decisions.

## Usage

```bash
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

```bash
# 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 `--stale` → **delete** (`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
