fix: lifecycle-safety follow-ups (#2870)#2900
Conversation
The `check_lock` in `step_prune` was carried over from #2808's Windows `.git/config` race fix. After #2870 restructured the flow so hook approval is exact, the phase ordering already serializes integration-check readers against the rename-failure fallback's `.git/config` rewrite: the background par_iter is the only reader, and the channel-drain loop (`for (idx, result) in rx`) only exits once that thread has finished. Removals run strictly after. That made the lock belt-and-suspenders — one mechanism per guarantee, per CLAUDE.md. Drop the `RwLock<()>`, the `Arc` wrapping it, the read-guard in the par_iter worker, the write-guard at the top of `try_remove`, and the `check_lock` field on `RemovalContext`. Comments on the par_iter spawn and the `SynchronousForNonCurrent` variant now name the real invariants (phase ordering, summary-accurate iteration) rather than the dropped lock. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e branch Both branch-deletion paths in `execute_instant_removal_or_fallback` swallowed errors at `log::debug!` — invisible in default verbosity. When the worktree is already gone but the branch survives (a hook advanced it, a `git branch -D` exec failed, refs scan errored), the user got no signal that the second half of the operation didn't happen. Promote both to a `warning_message` with a `wt remove --force-delete <branch>` recovery hint. To avoid duplicating the planner's existing "Branch unmerged" hint, the warning suppresses the `Ok(NotDeleted)` case when the planner already predicted retention (unmerged, or `--no-delete-branch`); race-driven `NotDeleted` outcomes still warn. `Err` always warns, since the prior `log::debug!` was invisible. `BackgroundRemoval` carries a new `planner_expected_retention` bit so the helper can make this distinction without re-deriving planner state. This adds noise to a previously silent path — the tradeoff is that silent half-completion of a destructive operation is worse than an extra warning line on the rare race. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
Two doc comments still cite the old "serialize .git/config rewrite with integration-check readers" rationale that the rest of this PR replaced — the SynchronousForNonCurrent variant doc and the try_remove comment were updated, but two siblings in src/output/handlers.rs were missed:
execute_instant_removal_or_fallback's doc, ending...runs that fallback synchronously for non-current worktrees when the caller needs to keep the fallback's '.git/config' rewrite serialized with other git readers.— the prune caller no longer needs that serialization; the variant doc you updated now grounds the choice in iteration ordering / final summary.handle_remove_output's doc, endingprune passes [...SynchronousForNonCurrent] to keep the fallback's '.git/config' rewrite serialized with its parallel integration-check readers.— same stale claim.
Happy to push a follow-up commit aligning both with the new variant doc if you'd like.
codecov/patch is failing with 6 uncovered lines, all in the new warn_if_branch_retained — the Ok(NotDeleted) race-warning arm (lines 303–308) and the Err arm (lines 312–317). Per CLAUDE.md codecov/patch is mandatory despite being non-required, so flagging for your call: the uncovered branches are exactly the rare paths the PR description calls out as acknowledged tradeoffs, and they're not trivially testable without an injected hook / git failure. Up to you whether to add tests or merge with the gap.
Other observations, all non-blocking:
- The lock removal in
step_prunelooks correct — thefor (idx, result) in rxloop only exits oncetxis dropped (when the spawned thread returns), which happens after every par_iter worker has finished. Removals run strictly after, so checks and the fallback's.git/configrewrite can't overlap. planner_expected_retention = ctx.deletion_mode.should_keep() || safety.integration_reason.is_none()evaluates totrueforForceDelete+integration_reason.is_none(), even though the planner predicted force-deletion in that case. It's a latent inaccuracy that never fires in practice becausedelete_branch_if_safe(force_delete=true)never returnsOk(NotDeleted)— but the relationship is implicit. Tightening to... || (!is_force() && integration_reason.is_none())would make the formula correct independent ofdelete_branch_if_safe's shape; arguably not worth the extra clause.
The `Err` arm of `warn_if_branch_retained` covered failure modes the user cannot act on differently than re-running the command (`git branch -D` exec errors, refs DB I/O failures). Promote it from `warning_message` to `log::warn!` to match the writing-user-outputs guideline for non-user-fixable issues, and let the `Ok(NotDeleted)` race path stay user-visible since the recovery is `wt remove --force-delete <branch>`. Net effect on the prior commit: removes the duplicate "to retry, run ..." recovery hint for I/O errors (the prior wording suggested the same recovery either way, conflating two different failure shapes) and cuts an integration-test-unreachable code path that was the source of the codecov/patch miss on #2900. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Integration tests cover only one of the four arms — the NotDeleted+planner-expected-deletion warning surfaced by test_merge_pre_remove_new_commit_keeps_branch. Add a binary-crate unit test that calls the helper with each outcome (Ok(NotDeleted) with both planner predictions, Ok(ForceDeleted), Ok(Integrated), Err) so the suppression logic and the log::warn Err arm are exercised in coverage. Output goes to stderr and isn't captured — the assertion is the call doesn't panic on any arm. User-visible message shapes stay pinned by the integration tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
(Worth noting: this means |
Two follow-ups to #2870 surfaced by a retrospective deep-dive review. The
third item discussed in that review (the two divergent safe-delete semantics
in
execute_instant_removal_or_fallback) is being explored as a separateCAS-based rewrite via
git update-ref -d <ref> <sha>and will come in afollow-up PR — it touches
delete_branch_if_safecore logic and warrantsits own review surface.
1.
refactor(prune): drop defensive check_lock now that phases are sequencedThe
check_lockRwLock<()>instep_prunewas carried over from #2808'sWindows
.git/configrace fix. After #2870 restructured the flow so hookapproval is exact, the phase ordering already serializes integration-check
readers against the rename-failure fallback: the background
par_iteristhe only reader, the channel-drain
for ... in rxloop exits only oncethat thread (and all its readers) has finished, and removals run strictly
after.
The remaining comment admitted the guard was defensive ("preserves the
serialization if this flow is refactored back to overlap checks and
removals") — one mechanism per guarantee per CLAUDE.md, so the lock is
removed. Comments on the par_iter spawn and the
SynchronousForNonCurrentvariant now name the real invariants.
Verdict: Removed. Phase ordering is the actual mechanism; the lock was
belt-and-suspenders.
2.
fix(remove): warn when background branch deletion silently retains the branchBoth branch-deletion paths in
execute_instant_removal_or_fallback(the fast-path post-prune delete and the
SynchronousForNonCurrentfallback delete) swallowed errors at
log::debug!— invisible at defaultverbosity. When the worktree was removed but the branch survived (a hook
advanced it,
git branch -Derrored, refs scan failed), the user got nosignal that the second half of the operation didn't happen.
Promoted to a
warning_messagewith awt remove --force-delete <branch>recovery hint. To avoid duplicating the planner's existing "Branch
unmerged" hint, the warning suppresses the
Ok(NotDeleted)case whenthe planner already predicted retention; race-driven
NotDeletedoutcomesstill warn.
Erralways warns.BackgroundRemovalcarries a newplanner_expected_retentionbit so the helper can make that distinctionwithout re-deriving planner state.
Tradeoff: a previously silent path gains noise on the rare race. Silent
half-completion of a destructive operation is worse than an occasional
extra warning line.
Tests
cargo run -- hook pre-merge --yespasses: 3834 nextest tests + doctests