Skip to content

feat: add wt prune - #3

Merged
lorenzolfm merged 1 commit into
mainfrom
prune-finished-worktrees
Aug 29, 2026
Merged

lorenzolfm merged 1 commit into
mainfrom
prune-finished-worktrees

Conversation

@lorenzolfm

Copy link
Copy Markdown
Owner

A worktree outlives its branch. The merge lands, the branch goes away on origin, and the directory stays until you name it in wt delete. wt prune removes each worktree whose work is finished, and deletes its branch.

What counts as finished

Condition What it catches
The base branch contains the branch, and origin still has it. A merge commit whose branch is not deleted yet.
Origin no longer has the upstream branch. A squash merge or a rebase merge whose branch is deleted.

The second condition is the one that matters for a squash merge: such a merge writes a new commit, so the branch is never an ancestor of the base branch, and the merge is complete nonetheless.

The safety rule

Each condition needs proof that the branch reached origin. This is the rule that makes the command safe, and it is the design problem the feature turns on.

A new branch from wt add holds no commit of its own, so the base branch trivially contains it. The test for a merge alone therefore cannot tell wt add eng-1234 from finished work, and would remove a worktree you had just made.

The two conditions prove the push in two ways, because a branch that origin lost leaves no ref behind:

  • A merge needs the remote-tracking ref refs/remotes/origin/<branch>.
  • A loss needs the upstream branch that the config names — what git push -u and wt add <remote branch> write, and what git fetch --prune keeps after it removes the ref.

Reading the config and not @{upstream} is deliberate: @{upstream} fails once the ref is absent, which is exactly the condition the command looks for. And the remote-tracking ref is the proof of a merge, and not the config, because git clone --bare copies refs/heads. Every branch that origin had at the time of wt clone is a local branch with no upstream branch, and the config would miss all of them.

wt prune removes no branch that is only on your machine.

What it keeps

Condition Reason
A modified file or an untracked file. --force overrides.
You are in the worktree. --force does not override: a removal would leave your shell in a path that is absent.
It is on the base branch, or on no branch. There is no branch to test.

git branch -d refuses a branch that a squash merge left behind, so the command deletes it with -D and writes the reason. --dry-run reads the list and changes nothing, which shows that step before it runs. --no-fetch stays offline and uses the refs already present.

The command also removes the administrative files of a worktree whose directory you deleted by hand. It compares the list before with the list after, because git worktree prune -v writes its report to the error output, which this tool reserves for messages in its own form, and because git keeps a worktree that a lock holds.

Shape

verdict takes the facts and returns the decision, so the rules above are a table of tests and no test needs a repository. facts is the only part that reads git.

Checks

The four nix gates pass locally: wt, wt-clippy, wt-test, wt-fmt. 34 tests, 13 of them new.

Exercised against a real repository in the .bare layout: merged and still on origin, merged and deleted, squash-merged, a dirty candidate, an active branch, a never-pushed branch, an unstarted branch, a detached HEAD, a worktree deleted by hand, --dry-run, --force, --no-fetch, running from inside a candidate, and an unreachable origin.

  fetching   origin (--prune)
  kept       dirty-done  (origin/main contains it, it has a modified or untracked file. use --force)
  removed    merged-closed  (origin no longer has merged-closed)
  deleted    branch merged-closed
  removed    merged-open  (origin/main contains it)
  deleted    branch merged-open
  removed    squashed  (origin no longer has squashed)
  deleted    branch squashed (forced: origin no longer has it)
3 worktree(s) removed. 1 need(s) your attention

A worktree outlives its branch. The merge lands, the branch goes away on
origin, and the directory stays until you name it in `wt delete`. `wt prune`
removes each worktree whose work is finished, and deletes its branch.

Two conditions make a worktree finished. The base branch contains the
branch and origin still has it, which is a merge commit whose branch is not
deleted yet. Or origin no longer has the upstream branch, which is a squash
merge or a rebase merge whose branch is deleted. The second condition is the
one that matters for a squash merge: such a merge writes a new commit, so
the branch is not an ancestor of the base branch, and the merge is complete
nonetheless.

Each condition needs proof that the branch reached origin. This is the rule
that makes the command safe. A new branch from `wt add` holds no commit of
its own, so the base branch contains it, and the test for a merge alone
cannot tell `wt add eng-1234` from finished work. The two conditions prove
the push in two ways, because a branch that origin lost leaves no ref
behind: a merge needs the remote-tracking ref, and a loss needs the upstream
branch that the config names. The config is what `git push -u` and `wt add
<remote branch>` write, and `git fetch --prune` keeps it after it removes
the ref. Reading the config and not `@{upstream}` is deliberate:
`@{upstream}` fails once the ref is absent, which is the condition the
command looks for.

The remote-tracking ref is the proof of a merge, and not the config, because
`git clone --bare` copies refs/heads. Each branch that origin had at the
time of `wt clone` is therefore a local branch with no upstream branch, and
the config would miss every one of them.

The command keeps a worktree that has a modified file or an untracked file,
which --force overrides. It keeps the worktree that you are in, which
--force does not override, because a removal would leave your shell in a
path that is absent. It says nothing about a worktree on the base branch or
on no branch, because there is no branch to test.

`git branch -d` refuses a branch that a squash merge left behind, so the
command deletes it with -D and writes the reason. --dry-run reads the list
and changes nothing, which shows that step before it runs.

The command also removes the administrative files of a worktree whose
directory you deleted by hand. It compares the list before with the list
after: `git worktree prune -v` writes its report to the error output, which
this tool reserves for messages in its own form, and git keeps a worktree
that a lock holds.

`verdict` takes the facts and returns the decision, so the rules above are
a table of tests and no test needs a repository.
@lorenzolfm
lorenzolfm merged commit cf71b4d into main Aug 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant