From 5d50055da274c3c51e76728924f809f22e9338f7 Mon Sep 17 00:00:00 2001 From: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:37:03 +0000 Subject: [PATCH] fix: preserve subdirectory position on wt remove When switching, wt preserves the user's subdirectory position (e.g. apps/gateway/ in a monorepo). wt remove landed at the destination worktree root instead, forcing a manual cd back after every removal. Mirror the switch behavior: when the removed worktree is the current one, compute the cwd's position relative to the removed worktree root and cd into the equivalent subdirectory of the destination if it exists, falling back to the root otherwise. Reuses the existing resolve_subdir_in_target helper. Since wt merge lands via the same handler, it gains the same subdirectory preservation. Closes #3343 Co-Authored-By: Claude --- src/output/handlers.rs | 14 ++++- tests/integration_tests/directives.rs | 73 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/output/handlers.rs b/src/output/handlers.rs index 0dc64c0a84..5222858ebc 100644 --- a/src/output/handlers.rs +++ b/src/output/handlers.rs @@ -1606,10 +1606,20 @@ fn refresh_removal_safety_after_pre_remove( fn prepare_remove_directory_change( main_path: &Path, + worktree_path: &Path, changed_directory: bool, ) -> anyhow::Result<()> { if changed_directory { - super::change_directory(main_path)?; + // Preserve the user's subdirectory position, mirroring `wt switch` + // (#3343). The removal hasn't run yet, so the current directory still + // exists inside the worktree being removed — if the user is in + // `worktree/apps/gateway/` and `main/apps/gateway/` exists, cd there + // instead of the main worktree root. Falls back to the root when the + // subdir is absent in the destination or the cwd can't be read. + let cd_target = std::env::current_dir() + .map(|cwd| resolve_subdir_in_target(main_path, Some(worktree_path), &cwd)) + .unwrap_or_else(|_| main_path.to_path_buf()); + super::change_directory(&cd_target)?; stderr().flush()?; // Force flush to ensure shell processes the cd // Mark that the CWD worktree is being removed, so the error handler // can show a hint if a subsequent command (e.g., post-merge hook) fails. @@ -1831,7 +1841,7 @@ fn handle_removed_worktree_output( return remove_removed_worktree_silently(&repo, &ctx, &safety, announcer); } - prepare_remove_directory_change(ctx.main_path, ctx.changed_directory)?; + prepare_remove_directory_change(ctx.main_path, ctx.worktree_path, ctx.changed_directory)?; // Handle detached HEAD case (no branch known) let Some(branch_name) = ctx.branch_name else { diff --git a/tests/integration_tests/directives.rs b/tests/integration_tests/directives.rs index aba6505c26..fd7f9a4280 100644 --- a/tests/integration_tests/directives.rs +++ b/tests/integration_tests/directives.rs @@ -384,6 +384,79 @@ fn test_switch_create_preserves_subdir(#[from(repo_with_remote)] repo: TestRepo) ); } +#[rstest] +fn test_remove_preserves_subdir(#[from(repo_with_remote)] mut repo: TestRepo) { + let feature_wt = repo.add_worktree("feature"); + let (cd_path, exec_path, _guard) = directive_files(); + + // Create the same subdirectory in both the feature worktree (where the + // user is) and the main worktree (where removal lands). + let subdir = "apps/gateway"; + fs::create_dir_all(repo.root_path().join(subdir)).unwrap(); + fs::create_dir_all(feature_wt.join(subdir)).unwrap(); + + let mut cmd = wt_command(); + repo.configure_wt_cmd(&mut cmd); + configure_directive_files(&mut cmd, &cd_path, &exec_path); + cmd.arg("remove").current_dir(feature_wt.join(subdir)); + + let output = cmd.output().unwrap(); + assert!(output.status.success(), "wt remove failed: {:?}", output); + + // Verify cd file lands in the equivalent subdirectory of the main + // worktree, not at its root — mirroring `wt switch` (issue #3343). + let cd_content = fs::read_to_string(&cd_path).unwrap_or_default(); + let expected_subdir = repo.root_path().join(Path::new("apps").join("gateway")); + let expected_str = expected_subdir.to_string_lossy(); + assert!( + cd_content.contains(&*expected_str), + "CD file should contain subdirectory path {}, got: {}", + expected_str, + cd_content + ); +} + +#[rstest] +fn test_remove_falls_back_to_root_when_subdir_missing( + #[from(repo_with_remote)] mut repo: TestRepo, +) { + let feature_wt = repo.add_worktree("feature"); + let (cd_path, exec_path, _guard) = directive_files(); + + // Create the subdirectory only in the feature worktree (where the user + // is), not in the main worktree where removal lands. + let subdir = "apps/gateway"; + fs::create_dir_all(feature_wt.join(subdir)).unwrap(); + // Intentionally NOT creating the subdir in the main worktree. + + let mut cmd = wt_command(); + repo.configure_wt_cmd(&mut cmd); + configure_directive_files(&mut cmd, &cd_path, &exec_path); + cmd.arg("remove").current_dir(feature_wt.join(subdir)); + + let output = cmd.output().unwrap(); + assert!(output.status.success(), "wt remove failed: {:?}", output); + + // Verify cd file lands at the main worktree root (the subdir is absent + // there, so preservation falls back). + let cd_content = fs::read_to_string(&cd_path).unwrap_or_default(); + let root_str = repo.root_path().to_string_lossy(); + assert!( + cd_content.contains(&*root_str), + "CD file should contain main worktree root {}, got: {}", + root_str, + cd_content + ); + let subdir_path = repo.root_path().join(Path::new("apps").join("gateway")); + let subdir_str = subdir_path.to_string_lossy(); + assert!( + !cd_content.contains(&*subdir_str), + "CD file should NOT contain missing subdirectory path {}, got: {}", + subdir_str, + cd_content + ); +} + // ============================================================================ // --no-cd Tests // ============================================================================