Files
max-sixty__worktrunk/.gitattributes
Maximilian Roos 4c845597b2 fix(prune, remove, ci): count what a removal took, not what it selected (#3633)
Three follow-ups from the review of #3533, which landed shared-branch
retention across the removal surface. Each is small and independent;
they share a branch because they came out of the same pass.

## `wt step prune` counted kinds, not outcomes

A candidate's kind is what the scan selected, not what the removal took.
A branch a sibling worktree still has checked out is retained, so a
worktree candidate can take the worktree and leave the branch standing.
The summary counted the kind anyway:

```console
○ Worktree directory missing for feature; pruned
↳ branch checked out at ~/code/repo.feature
✓ Pruned 1 branch          ← the branch is right there
```

`prune_summary` now counts the planned outcome, so that reads `✓ Pruned
1 worktree` — the stale entry, which is all that went. Both
`--format=json` payloads gained `branch_deleted` for the same reason: a
consumer reading `{"branch": "feature", "kind": "branch_only"}` would
reasonably conclude the branch is gone.

The predicate has one home now. `RemoveResult::to_json` already computed
`branch_deleted` inline; it moved to `RemoveResult::deletes_branch()`
and both callers share it. That incidentally fixes a detached worktree
reporting `"branch_deleted": true` beside a null branch — it has no
branch to delete.

`wt step prune --dry-run` had the same defect and no per-item line to
contradict it, so it now predicts retention too. A `Prunable` item has
no plan until `try_remove` prunes its stale entry, so the dry run asks
`live_sibling_checkout` — the same predicate the plan would.

## `RemoveTarget::Branch` now means "a branch with no worktree"

All three callers resolve first and pass `Path` for anything with a
worktree, so the arm handling "the branch turned out to have a worktree"
was unreachable at selection time — codecov confirmed it never executed.
Deleting it would have been enough, but the arm was a live hazard rather
than dead weight: the picker builds a fresh `Repository` and re-lists
worktrees between selecting a row and removing it, so a `wt switch` in
another terminal can give a branch-only row a worktree mid-flight. The
old code would then have removed that worktree, from a row that said
"delete this branch" — the same defect class #3533 fixed.

```console
✗ Branch feature gained a worktree @ ~/code/repo.feature since it was selected;
  to remove that worktree, run wt remove ~/code/repo.feature
```

`test_prepare_removal_refuses_branch_that_gained_a_worktree` covers the
picker path. I checked it fails when the guard is neutralized rather
than passing trivially.

## `affected tests (windows, advisory)` was permanently red

git calls a file binary only when it finds a NUL byte in the first 8000,
and two of the committed loose objects under `tests/fixtures/*/_git/`
are zlib streams small enough to have none. Those diff as text, so `git
diff` writes raw deflate into its output, and `cargo affected` aborts
decoding it as a string before running a test.

Reproduced with `git show <commit> -- <object>` (invalid UTF-8, byte
`0x95` at position 5403) and confirmed fixed — the same command now
reports `Binary files … differ`. The rest of `_git/` stays text worth
reading.

## Testing

`prune_summary_counts_a_retained_branch_as_worktree_only` covers the
counting; `test_prune_retains_branch_checked_out_in_another_worktree`
gained an assertion on the summary line, and I verified it fails when
the guard is removed. The six `.snap` changes are all the same added
`branch_deleted` key.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:05:22 -07:00

26 lines
1.6 KiB
Plaintext

# Collapse auto-generated files in GitHub PR diffs. These are regenerated by
# `test_docs_are_in_sync` in tests/integration_tests/readme_sync.rs — edits
# belong in the primary source (src/cli/mod.rs for command pages,
# docs/content/*.md for everything else).
skills/worktrunk/reference/*.md linguist-generated=true
# Skill-only files and the top-level README symlink are primary sources — edited
# directly, not regenerated from docs/content/. Exempt them so real edits stay
# visible in PR diffs.
skills/worktrunk/reference/README.md linguist-generated=false
skills/worktrunk/reference/shell-integration.md linguist-generated=false
skills/worktrunk/reference/troubleshooting.md linguist-generated=false
docs/static/*.md linguist-generated=true
docs/static/llms.txt linguist-generated=true
docs/static/.well-known/agent-skills/index.json linguist-generated=true
# Shell templates are embedded into the binary at compile time (askama); a
# CRLF checkout would leak \r into the shell code Windows-built binaries emit.
templates/* text eol=lf
# A fixture's object store and index are git's own binary formats, but git only
# calls a file binary when it finds a NUL in the first 8000 bytes, and a small
# enough zlib object may have none. Those diff as text, so `git diff` writes raw
# deflate into its output and any tool that decodes that output as a string
# fails on it — which is how `cargo affected` came to abort before running a
# test. The rest of `_git/` (HEAD, config, refs) is text worth reading.
tests/fixtures/*/_git/objects/** binary
tests/fixtures/*/_git/index binary