Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/commands/relocate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,12 +535,18 @@ impl<'a> RelocationExecutor<'a> {
let msg = cformat!("Relocated <bold>{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);
Expand Down
15 changes: 12 additions & 3 deletions src/output/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
48 changes: 47 additions & 1 deletion tests/integration_tests/step_relocate.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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}"
);
}
Loading