From 61e6c8f0767ce1bee5cb7ebb92acad79fd3ccfab Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 10 Apr 2026 23:39:09 -0400 Subject: [PATCH] Clean up empty parent directories after git worktree remove Zed creates worktrees inside an intermediate directory named after the branch: ///. 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. --- .../agent_ui/src/thread_worktree_archive.rs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/thread_worktree_archive.rs b/crates/agent_ui/src/thread_worktree_archive.rs index 4398a2154d4abd550535b247ab1a9e518f84b39d..a397cad0997316840e3dbc627485ca37253dcb9d 100644 --- a/crates/agent_ui/src/thread_worktree_archive.rs +++ b/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. `///`). After the inner + // directory is removed, the `/` 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