git: Forbid deleting current git worktree or branch from picker (#52327)

Anthony Eid created

## Context

This just makes the UI enforce some git cli rules more clearly and
prevents some unexpected behavior.

## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

crates/git_ui/src/branch_picker.rs   | 39 ++++++++++++------
crates/git_ui/src/worktree_picker.rs | 61 +++++++++++++++++++++++------
2 files changed, 74 insertions(+), 26 deletions(-)

Detailed changes

crates/git_ui/src/branch_picker.rs 🔗

@@ -486,6 +486,10 @@ impl BranchListDelegate {
             let is_remote;
             let result = match &entry {
                 Entry::Branch { branch, .. } => {
+                    if branch.is_head {
+                        return Ok(());
+                    }
+
                     is_remote = branch.is_remote();
                     repo.update(cx, |repo, _| {
                         repo.delete_branch(is_remote, branch.name().to_string())
@@ -1151,20 +1155,29 @@ impl PickerDelegate for BranchListDelegate {
 
                 let delete_and_select_btns = h_flex()
                     .gap_1()
-                    .child(
-                        Button::new("delete-branch", "Delete")
-                            .key_binding(
-                                KeyBinding::for_action_in(
-                                    &branch_picker::DeleteBranch,
-                                    &focus_handle,
-                                    cx,
-                                )
-                                .map(|kb| kb.size(rems_from_px(12.))),
+                    .when(
+                        !selected_entry
+                            .and_then(|entry| entry.as_branch())
+                            .is_some_and(|branch| branch.is_head),
+                        |this| {
+                            this.child(
+                                Button::new("delete-branch", "Delete")
+                                    .key_binding(
+                                        KeyBinding::for_action_in(
+                                            &branch_picker::DeleteBranch,
+                                            &focus_handle,
+                                            cx,
+                                        )
+                                        .map(|kb| kb.size(rems_from_px(12.))),
+                                    )
+                                    .on_click(|_, window, cx| {
+                                        window.dispatch_action(
+                                            branch_picker::DeleteBranch.boxed_clone(),
+                                            cx,
+                                        );
+                                    }),
                             )
-                            .on_click(|_, window, cx| {
-                                window
-                                    .dispatch_action(branch_picker::DeleteBranch.boxed_clone(), cx);
-                            }),
+                        },
                     )
                     .child(
                         Button::new("select_branch", "Select")

crates/git_ui/src/worktree_picker.rs 🔗

@@ -19,7 +19,7 @@ use remote_connection::{RemoteConnectionModal, connect};
 use settings::Settings;
 use std::{path::PathBuf, sync::Arc};
 use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
-use util::ResultExt;
+use util::{ResultExt, debug_panic};
 use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr};
 
 use crate::git_panel::show_error_toast;
@@ -115,6 +115,7 @@ impl WorktreeList {
                 this.picker.update(cx, |picker, cx| {
                     picker.delegate.all_worktrees = Some(all_worktrees);
                     picker.delegate.default_branch = default_branch;
+                    picker.delegate.refresh_forbidden_deletion_path(cx);
                     picker.refresh(window, cx);
                 })
             })?;
@@ -261,6 +262,7 @@ pub struct WorktreeListDelegate {
     modifiers: Modifiers,
     focus_handle: FocusHandle,
     default_branch: Option<SharedString>,
+    forbidden_deletion_path: Option<PathBuf>,
 }
 
 impl WorktreeListDelegate {
@@ -280,6 +282,7 @@ impl WorktreeListDelegate {
             modifiers: Default::default(),
             focus_handle: cx.focus_handle(),
             default_branch: None,
+            forbidden_deletion_path: None,
         }
     }
 
@@ -452,7 +455,7 @@ impl WorktreeListDelegate {
         let Some(entry) = self.matches.get(idx).cloned() else {
             return;
         };
-        if entry.is_new {
+        if entry.is_new || self.forbidden_deletion_path.as_ref() == Some(&entry.worktree.path) {
             return;
         }
         let Some(repo) = self.repo.clone() else {
@@ -486,6 +489,7 @@ impl WorktreeListDelegate {
                 if let Some(all_worktrees) = &mut picker.delegate.all_worktrees {
                     all_worktrees.retain(|w| w.path != path);
                 }
+                picker.delegate.refresh_forbidden_deletion_path(cx);
                 if picker.delegate.matches.is_empty() {
                     picker.delegate.selected_index = 0;
                 } else if picker.delegate.selected_index >= picker.delegate.matches.len() {
@@ -498,6 +502,29 @@ impl WorktreeListDelegate {
         })
         .detach();
     }
+
+    fn refresh_forbidden_deletion_path(&mut self, cx: &App) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            debug_panic!("Workspace should always be available or else the picker would be closed");
+            self.forbidden_deletion_path = None;
+            return;
+        };
+
+        let visible_worktree_paths = workspace.read_with(cx, |workspace, cx| {
+            workspace
+                .project()
+                .read(cx)
+                .visible_worktrees(cx)
+                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
+                .collect::<Vec<_>>()
+        });
+
+        self.forbidden_deletion_path = if visible_worktree_paths.len() == 1 {
+            visible_worktree_paths.into_iter().next()
+        } else {
+            None
+        };
+    }
 }
 
 async fn open_remote_worktree(
@@ -771,6 +798,9 @@ impl PickerDelegate for WorktreeListDelegate {
 
         let focus_handle = self.focus_handle.clone();
 
+        let can_delete =
+            !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path);
+
         let delete_button = |entry_ix: usize| {
             IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
                 .icon_size(IconSize::Small)
@@ -839,7 +869,7 @@ impl PickerDelegate for WorktreeListDelegate {
                             }
                         })),
                 )
-                .when(!entry.is_new, |this| {
+                .when(can_delete, |this| {
                     if selected {
                         this.end_slot(delete_button(ix))
                     } else {
@@ -857,6 +887,9 @@ impl PickerDelegate for WorktreeListDelegate {
         let focus_handle = self.focus_handle.clone();
         let selected_entry = self.matches.get(self.selected_index);
         let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
+        let can_delete = selected_entry.is_some_and(|entry| {
+            !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path)
+        });
 
         let footer_container = h_flex()
             .w_full()
@@ -904,16 +937,18 @@ impl PickerDelegate for WorktreeListDelegate {
         } else {
             Some(
                 footer_container
-                    .child(
-                        Button::new("delete-worktree", "Delete")
-                            .key_binding(
-                                KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
-                                    .map(|kb| kb.size(rems_from_px(12.))),
-                            )
-                            .on_click(|_, window, cx| {
-                                window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
-                            }),
-                    )
+                    .when(can_delete, |this| {
+                        this.child(
+                            Button::new("delete-worktree", "Delete")
+                                .key_binding(
+                                    KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
+                                        .map(|kb| kb.size(rems_from_px(12.))),
+                                )
+                                .on_click(|_, window, cx| {
+                                    window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
+                                }),
+                        )
+                    })
                     .child(
                         Button::new("open-in-new-window", "Open in New Window")
                             .key_binding(