From eb0436e84ea784df706893e3279eea5e8b2bce4d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Sep 2025 17:43:23 -0700 Subject: [PATCH] branch diff --- crates/fs/src/fake_git_repo.rs | 4 + crates/git_ui/src/git_panel.rs | 10 +- crates/git_ui/src/git_ui.rs | 1 - crates/git_ui/src/project_diff.rs | 203 ++++++++++++++++++++++++------ crates/project/src/git_store.rs | 20 +++ 5 files changed, 197 insertions(+), 41 deletions(-) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index f02583b479d7709c07741c2c15cf5aa7cdf5e229..ca74109da026cd9a09d0b327a5bd55cd1945e4d1 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -115,6 +115,10 @@ impl GitRepository for FakeGitRepository { unimplemented!() } + fn merge_base(&self, _commit_a: String, _commit_b: String) -> BoxFuture<'_, Option> { + unimplemented!() + } + fn load_commit( &self, _commit: String, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 64163b0ebc33f908de5c5cd8c97a24418bf4ba43..468acd568e7717d36e8fe83d330f88c92409d9af 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -3,7 +3,7 @@ use crate::commit_modal::CommitModal; use crate::commit_tooltip::CommitTooltip; use crate::commit_view::CommitView; use crate::git_panel_settings::StatusStyle; -use crate::project_diff::{self, Diff, ProjectDiff}; +use crate::project_diff::{self, Diff, DiffBaseKind, ProjectDiff}; use crate::remote_output::{self, RemoteAction, SuccessMessage}; use crate::{branch_picker, picker_prompt, render_remote_button}; use crate::{ @@ -937,7 +937,13 @@ impl GitPanel { self.workspace .update(cx, |workspace, cx| { - ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx); + ProjectDiff::deploy_at( + workspace, + DiffBaseKind::Head, + Some(entry.clone()), + window, + cx, + ); }) .ok(); self.focus_handle.focus(window); diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 52fcf93f4d39039cc5c847141038ffb22ea38b26..5369b8b404ba7005bf738f4515d20b3fe4163a48 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -23,7 +23,6 @@ use zed_actions; use crate::{git_panel::GitPanel, text_diff_view::TextDiffView}; mod askpass_modal; -pub mod branch_diff; pub mod branch_picker; mod commit_modal; pub mod commit_tooltip; diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 69ebd83ea8c1a78f13f2218c020bd8654f2b4374..ffd4af9eec7f0ffdfa2d88850053b8a81e735bc2 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -30,8 +30,11 @@ use project::{ git_store::{GitStore, GitStoreEvent, RepositoryEvent}, }; use settings::{Settings, SettingsStore}; -use std::any::{Any, TypeId}; use std::ops::Range; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; use theme::ActiveTheme; use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider}; use util::ResultExt as _; @@ -48,7 +51,9 @@ actions!( /// Shows the diff between the working directory and the index. Diff, /// Adds files to the git staging area. - Add + Add, + /// Shows the diff between the working directory and the default branch. + DiffToDefaultBranch, ] ); @@ -61,10 +66,17 @@ pub struct ProjectDiff { focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, pending_scroll: Option, + diff_base_kind: DiffBaseKind, _task: Task>, _subscription: Subscription, } +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum DiffBaseKind { + Head, + MergeBaseOfDefaultBranch, +} + #[derive(Debug)] struct DiffBuffer { path_key: PathKey, @@ -80,6 +92,7 @@ const NEW_NAMESPACE: u32 = 3; impl ProjectDiff { pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context) { workspace.register_action(Self::deploy); + workspace.register_action(Self::diff_to_default_branch); workspace.register_action(|workspace, _: &Add, window, cx| { Self::deploy(workspace, &Diff, window, cx); }); @@ -92,39 +105,66 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context, ) { - Self::deploy_at(workspace, None, window, cx) + Self::deploy_at(workspace, DiffBaseKind::Head, None, window, cx) + } + + fn diff_to_default_branch( + workspace: &mut Workspace, + _: &DiffToDefaultBranch, + window: &mut Window, + cx: &mut Context, + ) { + Self::deploy_at( + workspace, + DiffBaseKind::MergeBaseOfDefaultBranch, + None, + window, + cx, + ); } pub fn deploy_at( workspace: &mut Workspace, + diff_base_kind: DiffBaseKind, entry: Option, window: &mut Window, cx: &mut Context, ) { telemetry::event!( - "Git Diff Opened", + match diff_base_kind { + DiffBaseKind::MergeBaseOfDefaultBranch => "Git Branch Diff Opened", + DiffBaseKind::Head => "Git Diff Opened", + }, source = if entry.is_some() { "Git Panel" } else { "Action" } ); - let project_diff = if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, true, true, window, cx); - existing - } else { - let workspace_handle = cx.entity(); - let project_diff = - cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx)); - workspace.add_item_to_active_pane( - Box::new(project_diff.clone()), - None, - true, - window, - cx, - ); - project_diff - }; + let project_diff = + if let Some(existing) = Self::existing_project_diff(workspace, diff_base_kind, cx) { + workspace.activate_item(&existing, true, true, window, cx); + existing + } else { + let workspace_handle = cx.entity(); + let project_diff = cx.new(|cx| { + Self::new( + workspace.project().clone(), + workspace_handle, + diff_base_kind, + window, + cx, + ) + }); + workspace.add_item_to_active_pane( + Box::new(project_diff.clone()), + None, + true, + window, + cx, + ); + project_diff + }; if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); @@ -132,6 +172,16 @@ impl ProjectDiff { } } + pub fn existing_project_diff( + workspace: &mut Workspace, + diff_base_kind: DiffBaseKind, + cx: &mut Context, + ) -> Option> { + workspace + .items_of_type::(cx) + .find(|item| item.read(cx).diff_base_kind == diff_base_kind) + } + pub fn autoscroll(&self, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.request_autoscroll(Autoscroll::fit(), cx); @@ -141,6 +191,7 @@ impl ProjectDiff { fn new( project: Entity, workspace: Entity, + diff_base_kind: DiffBaseKind, window: &mut Window, cx: &mut Context, ) -> Self { @@ -152,9 +203,20 @@ impl ProjectDiff { Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); diff_display_editor.disable_diagnostics(cx); diff_display_editor.set_expand_all_diff_hunks(cx); - diff_display_editor.register_addon(GitPanelAddon { - workspace: workspace.downgrade(), - }); + match diff_base_kind { + DiffBaseKind::Head => { + diff_display_editor.register_addon(GitPanelAddon { + workspace: workspace.downgrade(), + }); + } + DiffBaseKind::MergeBaseOfDefaultBranch => { + diff_display_editor.set_render_diff_hunk_controls( + Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), + cx, + ); + // + } + } diff_display_editor }); window.defer(cx, { @@ -205,7 +267,7 @@ impl ProjectDiff { let (mut send, recv) = postage::watch::channel::<()>(); let worker = window.spawn(cx, { let this = cx.weak_entity(); - async |cx| Self::handle_status_updates(this, recv, cx).await + async |cx| Self::handle_status_updates(this, diff_base_kind, recv, cx).await }); // Kick off a refresh immediately *send.borrow_mut() = (); @@ -214,6 +276,7 @@ impl ProjectDiff { project, git_store: git_store.clone(), workspace: workspace.downgrade(), + diff_base_kind, focus_handle, editor, multibuffer, @@ -351,6 +414,9 @@ impl ProjectDiff { let Some(project_path) = self.active_path(cx) else { return; }; + if self.diff_base_kind != DiffBaseKind::Head { + return; + } self.workspace .update(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::(cx) { @@ -506,21 +572,65 @@ impl ProjectDiff { } } + fn refresh_merge_base_of_default_branch( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let Some(repo) = self.git_store.read(cx).active_repository() else { + self.multibuffer.update(cx, |multibuffer, cx| { + multibuffer.clear(cx); + }); + return; + }; + let default_branch = repo.read(cx).default_branch(); + let task = cx.spawn(async move |this, cx| { + let Some(default_branch) = default_branch.await else { + return; + }; + let merge_base = repo + .update(cx, |repo, cx| { + repo.merge_base("HEAD".to_string(), default_branch) + }) + .await?; + let diff = repo + .update(cx, |repo, cx| repo.diff_to_commit(merge_base)) + .await?; + }); + + cx.foreground_executor() + .spawn(async move |this, cx| task.await.log_err()) + } + pub async fn handle_status_updates( this: WeakEntity, + diff_base_kind: DiffBaseKind, mut recv: postage::watch::Receiver<()>, cx: &mut AsyncWindowContext, ) -> Result<()> { while (recv.next().await).is_some() { - let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?; - for buffer_to_load in buffers_to_load { - if let Some(buffer) = buffer_to_load.await.log_err() { - cx.update(|window, cx| { - this.update(cx, |this, cx| this.register_buffer(buffer, window, cx)) - .ok(); - })?; + match diff_base_kind { + DiffBaseKind::Head => { + let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?; + for buffer_to_load in buffers_to_load { + if let Some(buffer) = buffer_to_load.await.log_err() { + cx.update(|window, cx| { + this.update(cx, |this, cx| { + this.register_buffer(buffer, window, cx) + }) + .ok(); + })?; + } + } } - } + DiffBaseKind::MergeBaseOfDefaultBranch => { + this.update_in(cx, |this, window, cx| { + this.refresh_merge_base_of_default_branch(window, cx) + })? + .await; + } + }; + this.update(cx, |this, cx| { this.pending_scroll.take(); cx.notify(); @@ -637,7 +747,15 @@ impl Item for ProjectDiff { Self: Sized, { let workspace = self.workspace.upgrade()?; - Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx))) + Some(cx.new(|cx| { + ProjectDiff::new( + self.project.clone(), + workspace, + self.diff_base_kind, + window, + cx, + ) + })) } fn is_dirty(&self, cx: &App) -> bool { @@ -805,7 +923,16 @@ impl SerializableItem for ProjectDiff { window.spawn(cx, async move |cx| { workspace.update_in(cx, |workspace, window, cx| { let workspace_handle = cx.entity(); - cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx)) + // todo!() + cx.new(|cx| { + Self::new( + workspace.project().clone(), + workspace_handle, + DiffBaseKind::Head, + window, + cx, + ) + }) }) }) } @@ -1399,7 +1526,7 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let diff = cx.new_window_entity(|window, cx| { - ProjectDiff::new(project.clone(), workspace, window, cx) + ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx) }); cx.run_until_parked(); @@ -1454,7 +1581,7 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let diff = cx.new_window_entity(|window, cx| { - ProjectDiff::new(project.clone(), workspace, window, cx) + ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx) }); cx.run_until_parked(); @@ -1536,7 +1663,7 @@ mod tests { Editor::for_buffer(buffer, Some(project.clone()), window, cx) }); let diff = cx.new_window_entity(|window, cx| { - ProjectDiff::new(project.clone(), workspace, window, cx) + ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx) }); cx.run_until_parked(); @@ -1827,7 +1954,7 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let diff = cx.new_window_entity(|window, cx| { - ProjectDiff::new(project.clone(), workspace, window, cx) + ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx) }); cx.run_until_parked(); diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 8d19f65a0d96fe1bd129fdfe5711e5de33f173d6..28dbc63d9be48c8421d3e85aa497be57283bbd69 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -3467,6 +3467,26 @@ impl Repository { }) } + pub fn merge_base( + &mut self, + commit_a: String, + commit_b: String, + ) -> oneshot::Receiver> { + let id = self.id; + self.send_job(None, move |git_repo, cx| async move { + match git_repo { + RepositoryState::Local { backend, .. } => { + backend.merge_base(commit_a, commit_b).await + } + RepositoryState::Remote { + client, project_id, .. + } => { + todo!(); + } + } + }) + } + pub fn diff_to_commit(&mut self, commit: String) -> oneshot::Receiver> { let id = self.id; self.send_job(None, move |git_repo, cx| async move {