Always remove linked worktree workspaces before archive cleanup

Richard Feldman created

When archiving a thread's last reference to a linked worktree, the
worktree workspace must be removed from the MultiWorkspace before
the background cleanup task runs git worktree remove. Previously,
only the workspace_to_remove (found via exact PathList match on the
thread's folder_paths) was removed. This missed cases where the
workspace's root paths diverged from the thread's folder_paths
(e.g. after folders were added/removed from the workspace).

Now we also scan roots_to_archive for any linked worktree workspaces
that contain the worktree being archived and include them in the
removal set. This ensures all editors are dropped, releasing their
Entity<Worktree> references (held through File structs in buffers),
so wait_for_worktree_release completes and git worktree remove can
proceed.

Change summary

crates/sidebar/src/sidebar.rs | 47 +++++++++++++++++++++++++++++++++---
1 file changed, 43 insertions(+), 4 deletions(-)

Detailed changes

crates/sidebar/src/sidebar.rs 🔗

@@ -2831,17 +2831,56 @@ impl Sidebar {
             (group_key.path_list() != folder_paths).then_some(workspace)
         });
 
-        if let Some(workspace_to_remove) = workspace_to_remove {
+        // Also find workspaces for root plans that aren't covered by
+        // workspace_to_remove. This handles edge cases where the thread's
+        // folder_paths don't exactly match any workspace's root paths
+        // (e.g. after a folder was added/removed), but a linked worktree
+        // workspace does contain the worktree that needs to be removed
+        // from disk. Without removing these workspaces first, their open
+        // editors hold Entity<Worktree> references (through File structs
+        // in buffers), preventing the worktree entity from being released
+        // and blocking git worktree removal indefinitely.
+        let mut workspaces_to_remove: Vec<Entity<Workspace>> = Vec::new();
+        if let Some(ws) = workspace_to_remove {
+            workspaces_to_remove.push(ws);
+        }
+        if let Some(multi_workspace) = self.multi_workspace.upgrade() {
+            let mw = multi_workspace.read(cx);
+            for root in &roots_to_archive {
+                for workspace in mw.workspaces() {
+                    if workspaces_to_remove.contains(workspace) {
+                        continue;
+                    }
+                    let has_worktree = workspace
+                        .read(cx)
+                        .project()
+                        .read(cx)
+                        .visible_worktrees(cx)
+                        .any(|wt| wt.read(cx).abs_path().as_ref() == root.root_path.as_path());
+                    if !has_worktree {
+                        continue;
+                    }
+                    let group_key = workspace.read(cx).project_group_key(cx);
+                    let root_paths = PathList::new(&workspace.read(cx).root_paths(cx));
+                    if root_paths != *group_key.path_list() {
+                        workspaces_to_remove.push(workspace.clone());
+                    }
+                }
+            }
+        }
+
+        if !workspaces_to_remove.is_empty() {
             let multi_workspace = self.multi_workspace.upgrade().unwrap();
             let session_id = session_id.clone();
 
             // For the workspace-removal fallback, use the neighbor's workspace
-            // paths if available, otherwise fall back to the project group key.
+            // paths if available, otherwise fall back to the project group key
+            // of the first workspace being removed.
             let fallback_paths = neighbor
                 .as_ref()
                 .map(|(_, paths)| paths.clone())
                 .unwrap_or_else(|| {
-                    workspace_to_remove
+                    workspaces_to_remove[0]
                         .read(cx)
                         .project_group_key(cx)
                         .path_list()
@@ -2850,7 +2889,7 @@ impl Sidebar {
 
             let remove_task = multi_workspace.update(cx, |mw, cx| {
                 mw.remove(
-                    [workspace_to_remove],
+                    workspaces_to_remove,
                     move |this, window, cx| {
                         this.find_or_create_local_workspace(fallback_paths, window, cx)
                     },