Make worktree/branch deletion reliable: transient retries + long-path & long-filename handling#25
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the branch-folder delete worktree & branch action reliable in the face of the things that were leaving worktrees half-deleted:
.lockraces, network blips — are now retried.filename too long— deep worktrees that cross Windows' 260-charMAX_PATHlimit 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 longwhen the tree crossed the 260-characterMAX_PATHlimit (a deepnode_modules, generated output), leaving the worktree stuck. Two-part fix:git worktree add/removenow run with-c core.longpaths=true, so git's own file operations use the Windows extended-length API and can create/remove files pastMAX_PATH.DeleteWorktreeAsyncthrows a typedWorktreeRemovalExceptionand 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 runsgit worktree pruneto clear the dangling registration and finishes the ticked branch deletions. It's an explicit, clearly-labelled confirmation (newForceDeleteDialog); 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
GitRetryservice (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 namedfix.lockfile-bug).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 aGitCommandRunnerdelegate seam so tests can script git output (transient failures, a "too long" remove) without a real repo; addedPruneWorktreesAsync.Docs/Features.md,CHANGELOG.md) — document the retry behaviour, long-path handling, and the force-delete fallback, named by the exactfilename too longerror 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.Coreand 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 throughOpenerServicevia 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 thecore.longpathsflag is passed to worktree add/remove.🤖 Generated with Claude Code
https://claude.ai/code/session_01KfYxaFc4xQHTt75dhdQoWx