diff --git a/crates/agent_ui/src/thread_worktree_archive.rs b/crates/agent_ui/src/thread_worktree_archive.rs index d131daf5cc5af410a63a854ef4821efbc8f7180d..bc2dfdd07114dc20bae64deb43c86acec3635376 100644 --- a/crates/agent_ui/src/thread_worktree_archive.rs +++ b/crates/agent_ui/src/thread_worktree_archive.rs @@ -247,78 +247,9 @@ async fn remove_root_after_worktree_removal( // alive until the repo removes the worktree drop(project); result.context("git worktree metadata cleanup failed")?; - - // Empty-parent cleanup uses local std::fs — skip for remote projects. - if root.remote_connection.is_none() { - remove_empty_parent_dirs_up_to_worktrees_base( - root.root_path.clone(), - root.main_repo_path.clone(), - cx, - ) - .await; - } - Ok(()) } -/// After `git worktree remove` deletes the worktree directory, clean up any -/// empty parent directories between it and the Zed-managed worktrees base -/// directory (configured via `git.worktree_directory`). The base directory -/// itself is never removed. -/// -/// If the base directory is not an ancestor of `root_path`, no parent -/// directories are removed. -async fn remove_empty_parent_dirs_up_to_worktrees_base( - root_path: PathBuf, - main_repo_path: PathBuf, - cx: &mut AsyncApp, -) { - let worktrees_base = cx.update(|cx| worktrees_base_for_repo(&main_repo_path, cx)); - - if let Some(worktrees_base) = worktrees_base { - cx.background_executor() - .spawn(async move { - remove_empty_ancestors(&root_path, &worktrees_base); - }) - .await; - } -} - -/// Removes empty directories between `child_path` and `base_path`. -/// -/// Walks upward from `child_path`, removing each empty parent directory, -/// stopping before `base_path` itself is removed. If `base_path` is not -/// an ancestor of `child_path`, nothing is removed. If any directory is -/// non-empty (i.e. `std::fs::remove_dir` fails), the walk stops. -fn remove_empty_ancestors(child_path: &Path, base_path: &Path) { - let mut current = child_path; - while let Some(parent) = current.parent() { - if parent == base_path { - break; - } - if !parent.starts_with(base_path) { - break; - } - match std::fs::remove_dir(parent) { - Ok(()) => { - log::info!("Removed empty parent directory: {}", parent.display()); - } - Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => break, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - // Already removed by a concurrent process; keep walking upward. - } - Err(err) => { - log::error!( - "Failed to remove parent directory {}: {err}", - parent.display() - ); - break; - } - } - current = parent; - } -} - /// Finds a live `Repository` entity for the given path, or creates a temporary /// project to obtain one. /// @@ -913,7 +844,6 @@ mod tests { use project::Project; use serde_json::json; use settings::SettingsStore; - use tempfile::TempDir; use workspace::MultiWorkspace; fn init_test(cx: &mut TestAppContext) { @@ -926,117 +856,6 @@ mod tests { }); } - #[test] - fn test_remove_empty_ancestors_single_empty_parent() { - let tmp = TempDir::new().unwrap(); - let base = tmp.path().join("worktrees"); - let branch_dir = base.join("my-branch"); - let child = branch_dir.join("zed"); - - std::fs::create_dir_all(&child).unwrap(); - // Simulate git worktree remove having deleted the child. - std::fs::remove_dir(&child).unwrap(); - - assert!(branch_dir.exists()); - remove_empty_ancestors(&child, &base); - assert!(!branch_dir.exists(), "empty parent should be removed"); - assert!(base.exists(), "base directory should be preserved"); - } - - #[test] - fn test_remove_empty_ancestors_nested_empty_parents() { - let tmp = TempDir::new().unwrap(); - let base = tmp.path().join("worktrees"); - // Branch name with slash creates nested dirs: fix/thing/zed - let child = base.join("fix").join("thing").join("zed"); - - std::fs::create_dir_all(&child).unwrap(); - std::fs::remove_dir(&child).unwrap(); - - assert!(base.join("fix").join("thing").exists()); - remove_empty_ancestors(&child, &base); - assert!(!base.join("fix").join("thing").exists()); - assert!( - !base.join("fix").exists(), - "all empty ancestors should be removed" - ); - assert!(base.exists(), "base directory should be preserved"); - } - - #[test] - fn test_remove_empty_ancestors_stops_at_non_empty_parent() { - let tmp = TempDir::new().unwrap(); - let base = tmp.path().join("worktrees"); - let branch_dir = base.join("my-branch"); - let child = branch_dir.join("zed"); - let sibling = branch_dir.join("other-file.txt"); - - std::fs::create_dir_all(&child).unwrap(); - std::fs::write(&sibling, "content").unwrap(); - std::fs::remove_dir(&child).unwrap(); - - remove_empty_ancestors(&child, &base); - assert!(branch_dir.exists(), "non-empty parent should be preserved"); - assert!(sibling.exists()); - } - - #[test] - fn test_remove_empty_ancestors_not_an_ancestor() { - let tmp = TempDir::new().unwrap(); - let base = tmp.path().join("worktrees"); - let unrelated = tmp.path().join("other-place").join("branch").join("zed"); - - std::fs::create_dir_all(&base).unwrap(); - std::fs::create_dir_all(&unrelated).unwrap(); - std::fs::remove_dir(&unrelated).unwrap(); - - let parent = unrelated.parent().unwrap(); - assert!(parent.exists()); - remove_empty_ancestors(&unrelated, &base); - assert!(parent.exists(), "should not remove dirs outside base"); - } - - #[test] - fn test_remove_empty_ancestors_child_is_direct_child_of_base() { - let tmp = TempDir::new().unwrap(); - let base = tmp.path().join("worktrees"); - let child = base.join("zed"); - - std::fs::create_dir_all(&child).unwrap(); - std::fs::remove_dir(&child).unwrap(); - - remove_empty_ancestors(&child, &base); - assert!(base.exists(), "base directory should be preserved"); - } - - #[test] - fn test_remove_empty_ancestors_partially_non_empty_chain() { - let tmp = TempDir::new().unwrap(); - let base = tmp.path().join("worktrees"); - // Structure: base/a/b/c/zed where a/ has another child besides b/ - let child = base.join("a").join("b").join("c").join("zed"); - let other_in_a = base.join("a").join("other-branch"); - - std::fs::create_dir_all(&child).unwrap(); - std::fs::create_dir_all(&other_in_a).unwrap(); - std::fs::remove_dir(&child).unwrap(); - - remove_empty_ancestors(&child, &base); - assert!( - !base.join("a").join("b").join("c").exists(), - "c/ should be removed (empty)" - ); - assert!( - !base.join("a").join("b").exists(), - "b/ should be removed (empty)" - ); - assert!( - base.join("a").exists(), - "a/ should be preserved (has other-branch sibling)" - ); - assert!(other_in_a.exists()); - } - #[gpui::test] async fn test_build_root_plan_returns_none_for_main_worktree(cx: &mut TestAppContext) { init_test(cx);