Respect workspace override in git: diff (#48535)

Closes #ISSUE

Release Notes:

- Fixed an issue where the `git: diff` action would not respect the
active worktree
This commit is contained in:
Ben Kunkle 2026-02-06 11:03:52 -06:00 committed by GitHub
parent 980479fb7c
commit 101a53d904
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 147 additions and 55 deletions

View file

@ -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(

View file

@ -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)

View file

@ -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)
}

View file

@ -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");
}
}

View file

@ -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<()>,