Respect workspace override in `git: diff` (#48535)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- Fixed an issue where the `git: diff` action would not respect the
active worktree

Change summary

crates/git_ui/src/branch_picker.rs          |  30 -----
crates/git_ui/src/git_picker.rs             |  28 -----
crates/git_ui/src/git_ui.rs                 |  27 +++++
crates/git_ui/src/project_diff.rs           | 108 ++++++++++++++++++++++
crates/project/src/git_store/branch_diff.rs |   9 +
5 files changed, 147 insertions(+), 55 deletions(-)

Detailed changes

crates/git_ui/src/branch_picker.rs šŸ”—

@@ -25,7 +25,7 @@ use util::ResultExt;
 use workspace::notifications::DetachAndPromptErr;
 use workspace::{ModalView, Workspace};
 
-use crate::{branch_picker, git_panel::show_error_toast};
+use crate::{branch_picker, git_panel::show_error_toast, resolve_active_repository};
 
 actions!(
     branch_picker,
@@ -62,33 +62,7 @@ pub fn open(
     cx: &mut Context<Workspace>,
 ) {
     let workspace_handle = workspace.weak_handle();
-    let project = workspace.project().clone();
-
-    // Check if there's a worktree override from the project dropdown.
-    // This ensures the branch picker shows branches for the project the user
-    // explicitly selected in the title bar, not just the focused file's project.
-    // This is only relevant if for multi-projects workspaces.
-    let repository = workspace
-        .active_worktree_override()
-        .and_then(|override_id| {
-            let project_ref = project.read(cx);
-            project_ref
-                .worktree_for_id(override_id, cx)
-                .and_then(|worktree| {
-                    let worktree_abs_path = worktree.read(cx).abs_path();
-                    let git_store = project_ref.git_store().read(cx);
-                    git_store
-                        .repositories()
-                        .values()
-                        .find(|repo| {
-                            let repo_path = &repo.read(cx).work_directory_abs_path;
-                            *repo_path == worktree_abs_path
-                                || worktree_abs_path.starts_with(repo_path.as_ref())
-                        })
-                        .cloned()
-                })
-        })
-        .or_else(|| project.read(cx).active_repository(cx));
+    let repository = resolve_active_repository(workspace, cx);
 
     workspace.toggle_modal(window, cx, |window, cx| {
         BranchList::new(

crates/git_ui/src/git_picker.rs šŸ”—

@@ -568,33 +568,7 @@ fn open_with_tab(
     cx: &mut Context<Workspace>,
 ) {
     let workspace_handle = workspace.weak_handle();
-    let project = workspace.project().clone();
-
-    // Check if there's a worktree override from the project dropdown.
-    // This ensures the git picker shows info for the project the user
-    // explicitly selected in the title bar, not just the focused file's project.
-    // This is only relevant if for multi-projects workspaces.
-    let repository = workspace
-        .active_worktree_override()
-        .and_then(|override_id| {
-            let project_ref = project.read(cx);
-            project_ref
-                .worktree_for_id(override_id, cx)
-                .and_then(|worktree| {
-                    let worktree_abs_path = worktree.read(cx).abs_path();
-                    let git_store = project_ref.git_store().read(cx);
-                    git_store
-                        .repositories()
-                        .values()
-                        .find(|repo| {
-                            let repo_path = &repo.read(cx).work_directory_abs_path;
-                            *repo_path == worktree_abs_path
-                                || worktree_abs_path.starts_with(repo_path.as_ref())
-                        })
-                        .cloned()
-                })
-        })
-        .or_else(|| project.read(cx).active_repository(cx));
+    let repository = crate::resolve_active_repository(workspace, cx);
 
     workspace.toggle_modal(window, cx, |window, cx| {
         GitPicker::new(workspace_handle, repository, tab, rems(34.), window, cx)

crates/git_ui/src/git_ui.rs šŸ”—

@@ -4,6 +4,7 @@ use anyhow::anyhow;
 use command_palette_hooks::CommandPaletteFilter;
 use commit_modal::CommitModal;
 use editor::{Editor, actions::DiffClipboardWithSelectionData};
+
 use project::ProjectPath;
 use ui::{
     Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled,
@@ -308,6 +309,32 @@ fn open_modified_files(
     }
 }
 
+/// Resolves the repository for git operations, respecting the workspace's
+/// active worktree override from the project dropdown.
+pub fn resolve_active_repository(workspace: &Workspace, cx: &App) -> Option<Entity<Repository>> {
+    let project = workspace.project().read(cx);
+    workspace
+        .active_worktree_override()
+        .and_then(|override_id| {
+            project
+                .worktree_for_id(override_id, cx)
+                .and_then(|worktree| {
+                    let worktree_abs_path = worktree.read(cx).abs_path();
+                    let git_store = project.git_store().read(cx);
+                    git_store
+                        .repositories()
+                        .values()
+                        .find(|repo| {
+                            let repo_path = &repo.read(cx).work_directory_abs_path;
+                            *repo_path == worktree_abs_path
+                                || worktree_abs_path.starts_with(repo_path.as_ref())
+                        })
+                        .cloned()
+                })
+        })
+        .or_else(|| project.active_repository(cx))
+}
+
 pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
     GitStatusIcon::new(status)
 }

crates/git_ui/src/project_diff.rs šŸ”—

@@ -3,6 +3,7 @@ use crate::{
     git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
     git_panel_settings::GitPanelSettings,
     remote_button::{render_publish_button, render_push_button},
+    resolve_active_repository,
 };
 use anyhow::{Context as _, Result, anyhow};
 use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
@@ -154,6 +155,8 @@ impl ProjectDiff {
                 "Action"
             }
         );
+        let intended_repo = resolve_active_repository(workspace, cx);
+
         let existing = workspace
             .items_of_type::<Self>(cx)
             .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head));
@@ -177,6 +180,23 @@ impl ProjectDiff {
             );
             project_diff
         };
+
+        if let Some(intended) = &intended_repo {
+            let needs_switch = project_diff
+                .read(cx)
+                .branch_diff
+                .read(cx)
+                .repo()
+                .map_or(true, |current| current.read(cx).id != intended.read(cx).id);
+            if needs_switch {
+                project_diff.update(cx, |project_diff, cx| {
+                    project_diff.branch_diff.update(cx, |branch_diff, cx| {
+                        branch_diff.set_repo(Some(intended.clone()), cx);
+                    });
+                });
+            }
+        }
+
         if let Some(entry) = entry {
             project_diff.update(cx, |project_diff, cx| {
                 project_diff.move_to_entry(entry, window, cx);
@@ -2619,4 +2639,92 @@ mod tests {
 
         cx.assert_excerpts_with_selections("[EXCERPT]\nˇ# My cool project\nDetails to come.\n");
     }
+
+    #[gpui::test]
+    async fn test_deploy_at_respects_worktree_override(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/project_a"),
+            json!({
+                ".git": {},
+                "a.txt": "CHANGED_A\n",
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            path!("/project_b"),
+            json!({
+                ".git": {},
+                "b.txt": "CHANGED_B\n",
+            }),
+        )
+        .await;
+
+        fs.set_head_and_index_for_repo(
+            Path::new(path!("/project_a/.git")),
+            &[("a.txt", "original_a\n".to_string())],
+        );
+        fs.set_head_and_index_for_repo(
+            Path::new(path!("/project_b/.git")),
+            &[("b.txt", "original_b\n".to_string())],
+        );
+
+        let project = Project::test(
+            fs.clone(),
+            [
+                Path::new(path!("/project_a")),
+                Path::new(path!("/project_b")),
+            ],
+            cx,
+        )
+        .await;
+
+        let (worktree_a_id, worktree_b_id) = project.read_with(cx, |project, cx| {
+            let mut worktrees: Vec<_> = project.worktrees(cx).collect();
+            worktrees.sort_by_key(|w| w.read(cx).abs_path());
+            (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
+        });
+
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        cx.run_until_parked();
+
+        // Select project A via the dropdown override and open the diff.
+        workspace.update(cx, |workspace, cx| {
+            workspace.set_active_worktree_override(Some(worktree_a_id), cx);
+        });
+        cx.focus(&workspace);
+        cx.update(|window, cx| {
+            window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
+        });
+        cx.run_until_parked();
+
+        let diff_item = workspace.update(cx, |workspace, cx| {
+            workspace.active_item_as::<ProjectDiff>(cx).unwrap()
+        });
+        let paths_a = diff_item.read_with(cx, |diff, cx| diff.excerpt_paths(cx));
+        assert_eq!(paths_a.len(), 1);
+        assert_eq!(*paths_a[0], *"a.txt");
+
+        // Switch the override to project B and re-run the diff action.
+        workspace.update(cx, |workspace, cx| {
+            workspace.set_active_worktree_override(Some(worktree_b_id), cx);
+        });
+        cx.focus(&workspace);
+        cx.update(|window, cx| {
+            window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
+        });
+        cx.run_until_parked();
+
+        let same_diff_item = workspace.update(cx, |workspace, cx| {
+            workspace.active_item_as::<ProjectDiff>(cx).unwrap()
+        });
+        assert_eq!(diff_item.entity_id(), same_diff_item.entity_id());
+
+        let paths_b = diff_item.read_with(cx, |diff, cx| diff.excerpt_paths(cx));
+        assert_eq!(paths_b.len(), 1);
+        assert_eq!(*paths_b[0], *"b.txt");
+    }
 }

crates/project/src/git_store/branch_diff.rs šŸ”—

@@ -110,6 +110,15 @@ impl BranchDiff {
         &self.diff_base
     }
 
+    pub fn set_repo(&mut self, repo: Option<Entity<Repository>>, cx: &mut Context<Self>) {
+        self.repo = repo;
+        self.tree_diff = None;
+        self.base_commit = None;
+        self.head_commit = None;
+        cx.emit(BranchDiffEvent::FileListChanged);
+        *self.update_needed.borrow_mut() = ();
+    }
+
     pub async fn handle_status_updates(
         this: WeakEntity<Self>,
         mut recv: postage::watch::Receiver<()>,