Clean up empty parent directories after git worktree remove

Richard Feldman created

Zed creates worktrees inside an intermediate directory named after
the branch: <worktrees_dir>/<branch_name>/<project_name>/. When
git worktree remove deletes the inner project directory, the branch
directory is left behind as an empty directory. After a successful
git worktree remove, check if the parent directory is now empty and
remove it if so.

Change summary

crates/agent_ui/src/thread_worktree_archive.rs | 40 +++++++++++++++++++
1 file changed, 39 insertions(+), 1 deletion(-)

Detailed changes

crates/agent_ui/src/thread_worktree_archive.rs 🔗

@@ -5,6 +5,7 @@ use std::{
 
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, anyhow};
+
 use gpui::{App, AsyncApp, Entity, Task};
 use project::{
     LocalProjectFlags, Project, WorktreeId,
@@ -254,7 +255,44 @@ async fn remove_root_after_worktree_removal(
         .map_err(|_| anyhow!("git worktree removal was canceled"))?;
     // Keep _temp_project alive until after the await so the headless project isn't dropped mid-operation
     drop(_temp_project);
-    result
+    result?;
+
+    // `git worktree remove` deletes the worktree directory itself, but Zed
+    // creates worktrees inside an intermediate directory named after the
+    // branch (e.g. `<worktrees_dir>/<branch>/<project>/`). After the inner
+    // directory is removed, the `<branch>/` parent may be left behind as
+    // an empty directory. Clean it up if so.
+    if let Some(parent) = root.root_path.parent() {
+        if parent != root.main_repo_path {
+            remove_dir_if_empty(parent, cx).await;
+        }
+    }
+
+    Ok(())
+}
+
+/// Removes a directory only if it exists and is empty.
+async fn remove_dir_if_empty(path: &Path, cx: &mut AsyncApp) {
+    let Some(app_state) = current_app_state(cx) else {
+        return;
+    };
+    let is_empty = match app_state.fs.read_dir(path).await {
+        Ok(mut entries) => futures::StreamExt::next(&mut entries).await.is_none(),
+        Err(_) => return,
+    };
+    if is_empty {
+        app_state
+            .fs
+            .remove_dir(
+                path,
+                fs::RemoveOptions {
+                    recursive: false,
+                    ignore_if_not_exists: true,
+                },
+            )
+            .await
+            .log_err();
+    }
 }
 
 /// Finds a live `Repository` entity for the given path, or creates a temporary