refactor: unify subdir preservation for wt step relocate#3346
refactor: unify subdir preservation for wt step relocate#3346worktrunk-bot wants to merge 1 commit into
Conversation
max-sixty asked (in #3344) whether the subdirectory-preservation logic could be unified so every path-switching command behaves the same. `wt switch` and `wt remove`/`wt merge` already share `resolve_subdir_in_target` (merge lands via the same handler). The one remaining path that hand-rolled its own `dest.join(relative)` was `wt step relocate`. Route relocate's shell `cd` through the same helper, keeping its "only cd when the user is inside the moving worktree" guard. This also gains the helper's symlink canonicalization (relocate previously did a raw non-canonical `strip_prefix`) and the read-only `is_dir()` fallback (a no-op here since `git worktree move` carries the whole subtree, but keeps behavior identical across commands). Adds `test_relocate_preserves_subdir`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
The unification is clean and the calling convention matches the switch/remove sites. One note on the stated benefits, since it's easy to over-read: the symlink-canonicalization gain doesn't actually materialize for relocate. git worktree move runs at relocate.rs:531, before the cd block at 538–550, so by the time resolve_subdir_in_target calls dunce::canonicalize on src_path and cwd, neither path exists on disk anymore — both canonicalize calls fail and fall back to the raw paths (unwrap_or_else). So relocate effectively runs the same raw strip_prefix it did before, just with the added is_dir() fallback.
That's fine — it degrades gracefully to the prior behavior and the is_dir() fallback is a strict improvement — but the PR body frames symlink survival as an active gain here, and it isn't. switch/remove canonicalize before their source is gone, so they get it; relocate can't. Worth trimming that bullet so a future reader doesn't assume relocate survives symlinked cwds when it still relies on the pre-existing starts_with/strip_prefix string match.
No code change needed — the behavior is correct.
|
The red What fails. On Windows, renaming a directory tree fails with a sharing violation if any live process holds a current directory inside that tree. Here the spawned Why The part I could not verify from CI. Relocate takes its
I'm not pushing either change since I can't confirm the real-user Windows behavior end-to-end from here — flagging it so the direction is a deliberate call. Linux and macOS (including the advisory legs) are green, so the logic itself is sound; this is purely the Windows move-while-inside constraint. |
Context
Follow-up to #3344, where @max-sixty asked:
Current state
I traced every site that emits a
cdto move the shell between worktrees (change_directorycall sites):wt switch/switch --createhandlers.rs:931resolve_subdir_in_target✅wt remove/wt mergehandlers.rs:1622resolve_subdir_in_target✅wt step relocaterelocate.rs:543dest.join(relative)❌So
wt mergewas already unified — it lands via the same handler aswt remove, so #3344 gave it subdir preservation for free. The one remaining outlier that hand-rolled its own logic waswt step relocate.Change
Route relocate's shell
cdthrough the sharedresolve_subdir_in_targethelper, keeping its existing "only cd when the user is inside the moving worktree" guard (cwd.starts_with(src)) — the helper alone would wrongly cd a user who wasn't inside.The concrete behavior change is the
is_dir()fallback: the old code did an unconditionaldest.join(relative), so a missing subdir at the destination would have produced acdto a nonexistent path; the helper falls back to the worktree root instead. It's a no-op in practice (git worktree movecarries the whole subtree, so the subdir always exists at the destination), but it keeps the behavior identical across all three commands.Note the helper's symlink canonicalization does not materialize for
relocate.git worktree moveruns (relocate.rs:531) before thiscdblock, so by the time the helper canonicalizessrc_pathandcwd, neither exists on disk — bothdunce::canonicalizecalls fail and fall back to the raw paths. Relocate therefore keeps its pre-existing rawstarts_with/strip_prefixstring match and, unlikeswitch/remove(which canonicalize while their source still exists), does not survive symlinked cwds. No behavior regression — it degrades gracefully to the prior string-matching behavior.The helper is now
pub(crate)with an updated docstring naming its three callers.Testing
test_relocate_preserves_subdir— relocating a worktree from insideapps/gateway/lands thecddirective in the equivalent subdir at the new location.resolve_subdir_in_targetand theswitch/removesubdir integration tests continue to pass.relocatetest filter: 26 passed, 0 failed.🤖 Generated with Claude Code