branch diff

Conrad Irwin created

Change summary

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

Detailed changes

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<String>> {
+        unimplemented!()
+    }
+
     fn load_commit(
         &self,
         _commit: String,

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

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;

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<PathKey>,
+    diff_base_kind: DiffBaseKind,
     _task: Task<Result<()>>,
     _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>) {
         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<Workspace>,
     ) {
-        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<Workspace>,
+    ) {
+        Self::deploy_at(
+            workspace,
+            DiffBaseKind::MergeBaseOfDefaultBranch,
+            None,
+            window,
+            cx,
+        );
     }
 
     pub fn deploy_at(
         workspace: &mut Workspace,
+        diff_base_kind: DiffBaseKind,
         entry: Option<GitStatusEntry>,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
         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::<Self>(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<Workspace>,
+    ) -> Option<Entity<Self>> {
+        workspace
+            .items_of_type::<Self>(cx)
+            .find(|item| item.read(cx).diff_base_kind == diff_base_kind)
+    }
+
     pub fn autoscroll(&self, cx: &mut Context<Self>) {
         self.editor.update(cx, |editor, cx| {
             editor.request_autoscroll(Autoscroll::fit(), cx);
@@ -141,6 +191,7 @@ impl ProjectDiff {
     fn new(
         project: Entity<Project>,
         workspace: Entity<Workspace>,
+        diff_base_kind: DiffBaseKind,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> 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::<GitPanel>(cx) {
@@ -506,21 +572,65 @@ impl ProjectDiff {
         }
     }
 
+    fn refresh_merge_base_of_default_branch(
+        &mut self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> 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<Self>,
+        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();
 

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<Option<String>> {
+        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<Result<CommitDiff>> {
         let id = self.id;
         self.send_job(None, move |git_repo, cx| async move {