Skip to content

Add an opt-in Clean action to the Worktrees inventory#8

Merged
MrArcher23 merged 1 commit into
MrArcher23:mainfrom
Jorgelig-Labs:feat/worktree-clean
Jun 4, 2026
Merged

Add an opt-in Clean action to the Worktrees inventory#8
MrArcher23 merged 1 commit into
MrArcher23:mainfrom
Jorgelig-Labs:feat/worktree-clean

Conversation

@Jorgelig

@Jorgelig Jorgelig commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

The Worktrees inventory is read-only today: a row can only copy a git worktree remove command for you to run in a terminal. This adds the action that was deliberately deferred — a Clean button (Tauri-only) that runs it for you, so disk hygiene stays in the app.

It is the only action in arrow that mutates disk, so it is built conservatively:

  • Backend (src-tauri/src/worktrees.rs): remove_worktree / prune_worktreesCleanupResult, argv-direct (no shell), without --force — git itself refuses a locked or dirty/untracked worktree, and the refusal is surfaced verbatim. Two Tauri commands emit report-changed on a real run so the UI re-scans. The parser lib (src/lib.rs) stays git-free.
  • UI (WorktreesModal.svelte): Clean/Prunedry-run → confirmation → run → re-scan, offered only on rows git would actually clean (proven merged + clean, or prunable) — never the main, dirty, locked, or active worktree. In the browser dev build it degrades to copy cmd (the mutating commands are not exposed over HTTP).
  • Transport (api.ts, types.ts): dual-mode; throws in the browser.
  • Docs: README / CLAUDE / ROADMAP updated so they no longer claim the inventory is purely read-only; the deferred-Clean backlog item is marked done.

Honesty

The button never promises a removal git would refuse: no --force, no branch -d/-D, never rm -rf. A squash/rebase-merged branch (shown as "can't confirm") gets no Clean button.

Testing

  • New: remove_deletes_a_real_worktree_without_force — a real temp git repo + linked worktree, dry-run leaves it, real remove deletes it.
  • cargo test green (parser + backend), cargo clippy clean, cargo fmt applied, frontend builds.

Branches off the current main, so it applies cleanly on top of the existing Worktrees inventory.

The Worktrees modal was read-only: it could only copy a `git worktree
remove` command for the user to run. Add a `Clean` button (Tauri-only)
that runs it, behind a dry-run + confirmation, so disk hygiene happens in
one place instead of bouncing to a terminal.

- Backend (src-tauri/src/worktrees.rs): remove_worktree/prune_worktrees ->
  CleanupResult, argv-direct, WITHOUT --force (git itself refuses a locked
  or dirty worktree), surfacing git's message verbatim on a refusal. Two
  Tauri commands emit report-changed on a real run so the UI re-scans.
- UI (WorktreesModal.svelte): a Clean/Prune button -> dry-run -> confirm ->
  run -> re-scan, offered ONLY on rows git would actually clean (proven
  merged + clean, or prunable), never the main/dirty/locked/active worktree.
  In the browser dev build it degrades to 'copy cmd' (not exposed over HTTP).
- Transport (api.ts, types.ts): dual-mode, throws in the browser.
- Docs: README/CLAUDE/ROADMAP no longer claim the inventory is purely
  read-only; the deferred-Clean backlog item is now done.
- Test: remove_deletes_a_real_worktree_without_force (a real temp repo +
  worktree, removed without --force).
@Jorgelig Jorgelig force-pushed the feat/worktree-clean branch from 4758c00 to 615a91f Compare June 4, 2026 19:18
@MrArcher23

Copy link
Copy Markdown
Owner

Thanks for this, Jorge — really solid and conservative work. I read through it and the safety model is exactly right: no --force, no git branch -d/-D, no rm -rf anywhere in the production path, and you lean on git itself to refuse a dirty/untracked/locked/main worktree (with the refusal surfaced verbatim). Removal stays reversible (the branch ref survives), and Clean is only offered on proven-merged-and-clean or prunable rows. The hermetic temp-repo test is a nice touch, and it test-merges cleanly onto current main (after #9 landed) with no conflicts.

One thing I'd like to fix before merging — not a data-loss issue, a robustness one:

The mutating path drops the safeguards the read path has. run_git_capture calls git via a bare .output() with no timeout and without Stdio::null() on stdin, whereas the read-only run_git kills git after GIT_TIMEOUT (5s) and nulls stdin. A wedged git — a worktree on a stalled network FS, lock contention, or an unexpected credential/hook prompt reading stdin — would block the invoke indefinitely and leave the confirm bar stuck on "Running…" with no escape. This is the exact failure mode worktrees.rs went out of its way to prevent for the read path. Could you reuse the same bounded-wait/kill-on-deadline pattern and set stdin(Stdio::null()) on the mutating call too?

Two optional follow-ups (fine to fold in or skip):

  • canClean in WorktreesModal.svelte is inTauri && (prunable || (isMerged && !dirty && !locked)) — it doesn't check isActive, even though the description says Clean is never offered on an active worktree. classify() already computes isActive, so either add && !isActive(...) to canClean or soften the wording. Low risk (git still refuses a dirty tree); just aligning behavior with the claim.
  • Prune is offered per-row, but git worktree prune operates repo-wide, so confirming it on one phantom row prunes every phantom entry for that repo. Harmless (and the -v dry-run shows exactly what goes), but the per-row framing can read as "just this one."

Everything else looks great — once the timeout/stdin fix is in, happy to merge. 🙌

@MrArcher23 MrArcher23 closed this in c091492 Jun 4, 2026
@MrArcher23 MrArcher23 merged commit 615a91f into MrArcher23:main Jun 4, 2026
3 checks passed
@MrArcher23

Copy link
Copy Markdown
Owner

Merged — thanks, Jorge! 🙌 Shipping in v0.1.9.

Since this came from a fork I couldn't push to directly, I folded the two review points into a small hardening commit on top when merging, so they're in:

  • run_git_capture is now bounded by GIT_TIMEOUT (5s) with stdin nulled — mirroring the read-only run_git — so a wedged git (stalled network FS, lock contention, or a stray credential/hook prompt) can no longer freeze the UI on "Running…".
  • canClean now excludes actively-edited worktrees, matching the comment's "never … active": it returns false when isActive(latestEditUnder(w.path), now).

Everything else went in as-is — the no---force / let-git-be-the-gatekeeper design and the hermetic temp-repo test are exactly right. Really solid, conservative work. 🚀

(I left the per-row Prune wording as-is for now — harmless, and the -v dry-run shows exactly what goes.)

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.

2 participants