Skip to content
Merged
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 @@ -828,12 +828,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
56 changes: 55 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,54 @@ 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).
///
/// Ignored on Windows: subdir preservation only fires when the cwd is inside the
/// moving worktree — which is exactly when `git worktree move` (a directory
/// rename) fails with a sharing violation, because a live process holds that cwd.
/// Unlike `remove` (where shell integration cds to main before removing), a real
/// Windows user hits this too: their shell holds the cwd across the move. So the
/// preservation path is reachable, and testable, only on Unix.
#[rstest]
#[cfg_attr(windows, ignore)]
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