Skip to content

Make worktree/branch deletion reliable: transient retries + long-path & long-filename handling#25

Merged
seankearon merged 4 commits into
mainfrom
claude/polly-retries-deletion-14rk0o
Jul 3, 2026
Merged

Make worktree/branch deletion reliable: transient retries + long-path & long-filename handling#25
seankearon merged 4 commits into
mainfrom
claude/polly-retries-deletion-14rk0o

Conversation

@seankearon

@seankearon seankearon commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Makes the branch-folder delete worktree & branch action reliable in the face of the things that were leaving worktrees half-deleted:

  1. Transient git failures — locked files, ref .lock races, network blips — are now retried.
  2. Long filenames / filename too long — deep worktrees that cross Windows' 260-char MAX_PATH limit are handled, with a permanent force-delete fallback when git still can't remove the folder.

Long filenames & the "filename too long" fix

Deleting a worktree could fail on Windows with filename too long / unable to unlink … Filename too long when the tree crossed the 260-character MAX_PATH limit (a deep node_modules, generated output), leaving the worktree stuck. Two-part fix:

  • Long-path-aware gitgit worktree add/remove now run with -c core.longpaths=true, so git's own file operations use the Windows extended-length API and can create/remove files past MAX_PATH.
  • Force-delete fallback — if git still can't remove the folder, DeleteWorktreeAsync throws a typed WorktreeRemovalException and the UI offers to delete the folder straight from disk: a recursive removal that bypasses the Recycle Bin and uses an extended-length (\\?\) path so it isn't defeated by the same limit. Then it runs git worktree prune to clear the dangling registration and finishes the ticked branch deletions. It's an explicit, clearly-labelled confirmation (new ForceDeleteDialog); backing out is a NO-GO and leaves everything in place. The folder delete runs off the UI thread so the window stays responsive.

Transient-failure retries

  • New GitRetry service (src/Services/GitRetry.cs) — a Polly (Polly.Core) resilience pipeline that retries transient git failures with exponential backoff + jitter. IsTransient() classifies failures via a curated set of filesystem-lock and network markers, deliberately narrow so permanent refusals (use --force, remote ref does not exist, "not fully merged") fail fast — including guarding against false positives like a worktree path that merely contains .lock (e.g. a branch named fix.lockfile-bug).
  • Wired into OpenerService — worktree removal and local/remote branch deletion each run through the pipeline, narrating every retry in the flight log. Remote-delete failures are logged and reflected in the outcome rather than thrown (local cleanup is already done). Cancellation propagates immediately without retry.

Other changes

  • GitService — added a GitCommandRunner delegate seam so tests can script git output (transient failures, a "too long" remove) without a real repo; added PruneWorktreesAsync.
  • Docs (Docs/Features.md, CHANGELOG.md) — document the retry behaviour, long-path handling, and the force-delete fallback, named by the exact filename too long error and surfaced in the At-a-glance table.

Testing

All 193 tests pass, and a Native AOT publish is clean (0 trim/AOT warnings — Polly.Core and the new dialog are AOT-safe). Coverage includes:

  • GitRetryTests — transient/permanent classification (incl. the .lock-in-path regression guard) and pipeline behaviour (retry-until-success, give-up-after-budget, no-retry-on-permanent).
  • OpenerDeletionRetryTests — retries driven through OpenerService via a faked git runner.
  • OpenerForceDeleteTests — force-delete removes the folder, prunes, and deletes branches against real git (and honours keeping an unticked branch).
  • DeleteWorktreeTests — the full window flow through the force-delete offer, both accepted and declined, driven end-to-end by injecting a worktree-remove failure.
  • GitServiceTests — asserts the core.longpaths flag is passed to worktree add/remove.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KfYxaFc4xQHTt75dhdQoWx

claude added 4 commits July 2, 2026 15:36
Worktree/branch deletion sometimes fails for reasons that clear on
their own: on Windows a worktree file is still held open by an editor
or antivirus scan, a racing git process leaves a ref/index .lock, or a
network blip interrupts deleting the branch on origin. Previously any of
these aborted the cleanup and left a half-tidied branch.

Wrap each git delete step (worktree remove, local branch delete, remote
branch delete) in a Polly resilience pipeline that retries only
transient-looking failures a few times with backoff, narrating each
retry in the flight log. Permanent refusals ("use --force", "remote ref
does not exist", "not fully merged") are classified as non-transient and
still fail fast on the first attempt, so behaviour and tests for those
paths are unchanged.

- New GitRetry: transient-failure predicate + the retry pipeline,
  isolated so it can be unit-tested without git.
- GitService gains an injectable GitCommandRunner seam so tests can
  script a transient-then-success git result end-to-end.
- Polly.Core (Native-AOT/trim-safe) added; verified a clean AOT publish
  with zero trim warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfYxaFc4xQHTt75dhdQoWx
Follow-up from an adversarial review of the retry classifier:

- Fix an over-broad marker: a bare ".lock" substring also matched a
  *permanent* "use --force" failure whenever it echoed a worktree path
  containing ".lock" (e.g. a branch named fix.lockfile-bug), needlessly
  retrying it. Match git's lock-creation phrasing instead ("cannot lock
  ref", "unable to create … .lock", "another git process seems to be
  running"), which still covers real lock contention. Added a regression
  test.
- Broaden coverage for the cases the feature targets: filesystem
  "directory not empty" / "device or resource busy", and transient
  network HTTP-5xx, TLS-handshake, and sideband-disconnect errors.
- Document, in the marker list, that permanent remote auth failures
  (HTTP 403 / SSH publickey) are knowingly retried — bounded and
  non-fatal — and, in DeleteWorktreeAsync, the rare non-destructive case
  where a drop after a successful origin delete is re-reported as failed.

All 188 tests pass; clean Native AOT publish retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfYxaFc4xQHTt75dhdQoWx
Deleting a worktree could fail with "filename too long" on Windows when
the tree crossed the 260-char MAX_PATH limit (deep node_modules,
generated output). Two-part fix:

- Run git's worktree add/remove with `-c core.longpaths=true` so git's
  own file operations use the Windows extended-length API.
- When git still can't remove the folder, DeleteWorktreeAsync now throws
  a typed WorktreeRemovalException and the UI offers a fallback: a
  permanent, recursive folder delete that bypasses the Recycle Bin (using
  a \\?\ extended-length path so it isn't stopped by the same limit),
  followed by `git worktree prune` to clear the dangling registration and
  then the ticked branch deletions. It's an explicit, clearly-labelled
  confirmation (new ForceDeleteDialog); backing out is a NO-GO and leaves
  everything in place.

Tests: git passes the long-paths flag; ForceDeleteWorktreeAsync deletes
the folder + prunes + deletes branches (incl. keeping an unticked
branch); and the full window flow through the offer, accepted and
declined, driven against real git via an injected worktree-remove
failure. All 193 tests pass; clean Native AOT publish retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfYxaFc4xQHTt75dhdQoWx
…summary

Make the long-path handling discoverable by the exact error users hit
("filename too long" / "unable to unlink … Filename too long") and add a
Delete-worktree row to the At-a-glance table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfYxaFc4xQHTt75dhdQoWx
@seankearon seankearon changed the title Add retry pipeline for transient git deletion failures Make worktree/branch deletion reliable: transient retries + long-path & long-filename handling Jul 3, 2026
@seankearon seankearon merged commit c7b9189 into main Jul 3, 2026
2 checks passed
@seankearon seankearon deleted the claude/polly-retries-deletion-14rk0o branch July 3, 2026 08:35
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