diff --git a/src/commands/relocate.rs b/src/commands/relocate.rs index 7c3ea3d0a..b0ca537a9 100644 --- a/src/commands/relocate.rs +++ b/src/commands/relocate.rs @@ -535,12 +535,18 @@ impl<'a> RelocationExecutor<'a> { let msg = cformat!("Relocated {branch}: {src_display} → {dest_display}"); eprintln!("{}", success_message(msg)); - // Update shell if user is inside this worktree + // Update shell if user is inside this worktree, preserving their + // subdirectory position via the same helper as `switch`/`remove` so + // every path-switching command behaves identically. if let Some(cwd_path) = cwd && cwd_path.starts_with(&src_path) { - let relative = cwd_path.strip_prefix(&src_path).unwrap_or(Path::new("")); - crate::output::change_directory(dest_path.join(relative))?; + let cd_target = crate::output::handlers::resolve_subdir_in_target( + &dest_path, + Some(&src_path), + cwd_path, + ); + crate::output::change_directory(cd_target)?; } self.moved.insert(idx); diff --git a/src/output/handlers.rs b/src/output/handlers.rs index 5222858eb..2886ccc1b 100644 --- a/src/output/handlers.rs +++ b/src/output/handlers.rs @@ -859,12 +859,21 @@ fn print_switch_message_if_changed( Ok(()) } -/// Compute the target directory for `cd` after switching, preserving the user's -/// subdirectory position when possible. +/// Compute the target directory for `cd` when moving the shell between +/// worktrees, preserving the user's subdirectory position when possible. /// /// If the user is in `source_root/apps/gateway/` and `target_root/apps/gateway/` /// exists, returns `target_root/apps/gateway/`. Otherwise returns `target_root`. -fn resolve_subdir_in_target(target_root: &Path, source_root: Option<&Path>, cwd: &Path) -> PathBuf { +/// +/// Shared by every command that relocates the shell — `switch`, `remove` (and +/// `merge`, which lands via the same handler), and `step relocate` — so they +/// preserve subdirectory position identically (canonicalizing to survive +/// symlinks, and falling back to the root when the subdir is absent). +pub(crate) fn resolve_subdir_in_target( + target_root: &Path, + source_root: Option<&Path>, + cwd: &Path, +) -> PathBuf { if let Some(source_root) = source_root { // Canonicalize both paths to handle symlinks (e.g., /var -> /private/var on macOS) let cwd = dunce::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf()); diff --git a/tests/integration_tests/step_relocate.rs b/tests/integration_tests/step_relocate.rs index 7bf147020..ba550fe25 100644 --- a/tests/integration_tests/step_relocate.rs +++ b/tests/integration_tests/step_relocate.rs @@ -1,9 +1,12 @@ //! Integration tests for `wt step relocate` -use crate::common::{TestRepo, make_snapshot_cmd, repo}; +use crate::common::{ + TestRepo, configure_directive_files, directive_files, make_snapshot_cmd, repo, +}; use insta_cmd::assert_cmd_snapshot; use rstest::rstest; use std::fs; +use std::path::Path; /// Get the parent directory of the repo (where worktrees are created) fn worktree_parent(repo: &TestRepo) -> std::path::PathBuf { @@ -1024,3 +1027,46 @@ worktree-path = "../{{ undefined_var }}.{{ branch }}" "template_error skip missing from JSON: {parsed}" ); } + +/// Relocating a worktree the user is standing inside preserves their +/// subdirectory position, routing the `cd` through the same +/// `resolve_subdir_in_target` helper as `switch`/`remove` (issue #3343 unify). +#[rstest] +fn test_relocate_preserves_subdir(repo: TestRepo) { + let parent = worktree_parent(&repo); + let (cd_path, exec_path, _guard) = directive_files(); + + // Create a worktree at a non-standard location, with a subdirectory the + // user is working in. + let wrong_path = parent.join("wrong-location"); + repo.run_git(&[ + "worktree", + "add", + "-b", + "feature", + wrong_path.to_str().unwrap(), + ]); + let subdir = Path::new("apps").join("gateway"); + fs::create_dir_all(wrong_path.join(&subdir)).unwrap(); + + let mut cmd = repo.wt_command(); + configure_directive_files(&mut cmd, &cd_path, &exec_path); + cmd.args(["step", "relocate"]) + .current_dir(wrong_path.join(&subdir)); + + let output = cmd.output().unwrap(); + assert!( + output.status.success(), + "wt step relocate failed: {output:?}" + ); + + // The cd directive should land in the equivalent subdirectory of the + // worktree's new location, not at its root. + let cd_content = fs::read_to_string(&cd_path).unwrap_or_default(); + let expected_subdir = parent.join("repo.feature").join(&subdir); + let expected_str = expected_subdir.to_string_lossy(); + assert!( + cd_content.contains(&*expected_str), + "CD file should contain relocated subdirectory path {expected_str}, got: {cd_content}" + ); +}