Partlet

7.6 KiB ยท raw

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

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

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

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