Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:34:26.471Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

branch_diff.rs

1200 lines · 41.4 KB · rust
1use crate::{
2    branch_picker,
3    diff_multibuffer::DiffMultibuffer,
4    project_diff::{
5        self, CompareWithBranch, DeployBranchDiff, ReviewDiff, render_send_review_to_agent_button,
6    },
7};
8use agent_settings::AgentSettings;
9use anyhow::{Context as _, Result, anyhow};
10use editor::{
11    Addon, Editor, EditorEvent, RestoreOnlyDiffHunkDelegate, SplittableEditor,
12    actions::SendReviewToAgent,
13};
14use git::{repository::DiffType, status::FileStatus};
15use gpui::{
16    Action, AnyElement, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, Render,
17    SharedString, Subscription, Task, WeakEntity,
18};
19use language::{BufferId, Capability};
20use project::{
21    Project, ProjectPath,
22    git_store::{
23        Repository,
24        diff_buffer_list::{self, DiffBase},
25    },
26};
27use settings::Settings;
28use std::{
29    any::{Any, TypeId},
30    sync::Arc,
31};
32use ui::{DiffStat, Divider, PopoverMenu, Tooltip, prelude::*};
33use workspace::{
34    ItemHandle, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
35    ToolbarItemView, Workspace,
36    item::{Item, ItemEvent, SaveOptions, TabContentParams},
37    notifications::NotifyTaskExt,
38    searchable::SearchableItemHandle,
39};
40use zed_actions::agent::ReviewBranchDiff;
41
42/// The workspace item for a branch (merge-base) diff: "Changes since {branch}".
43/// It wraps a single [`DiffMultibuffer`] over [`DiffBase::Merge`] and delegates
44/// the [`Item`] surface to it. The merge base can be changed in place via the
45/// [`BranchDiffToolbar`]'s branch picker, which reloads without reconfiguring
46/// the editor (the merge styling is identical for every base ref).
47pub struct BranchDiff {
48    diff: Entity<DiffMultibuffer>,
49    project: Entity<Project>,
50    workspace: WeakEntity<Workspace>,
51    _diff_event_subscription: Subscription,
52}
53
54struct BranchDiffAddon {
55    branch_diff: Entity<diff_buffer_list::DiffBufferList>,
56}
57
58impl Addon for BranchDiffAddon {
59    fn to_any(&self) -> &dyn std::any::Any {
60        self
61    }
62
63    fn override_status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
64        self.branch_diff
65            .read(cx)
66            .status_for_buffer_id(buffer_id, cx)
67    }
68}
69
70impl BranchDiff {
71    pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
72        workspace.register_action(Self::deploy_branch_diff);
73        workspace.register_action(Self::compare_with_branch);
74        workspace::register_serializable_item::<Self>(cx);
75    }
76
77    fn deploy_branch_diff(
78        workspace: &mut Workspace,
79        _: &DeployBranchDiff,
80        window: &mut Window,
81        cx: &mut Context<Workspace>,
82    ) {
83        telemetry::event!("Git Branch Diff Opened");
84        let project = workspace.project().clone();
85        let Some(intended_repo) = project.read(cx).active_repository(cx) else {
86            let workspace = cx.entity().downgrade();
87            window
88                .spawn(cx, async |_cx| {
89                    let result: Result<()> = Err(anyhow!("No active repository"));
90                    result
91                })
92                .detach_and_notify_err(workspace, window, cx);
93            return;
94        };
95
96        let default_branch = intended_repo.update(cx, |repo, _| repo.default_branch(true));
97        let workspace = cx.entity();
98        let workspace_weak = workspace.downgrade();
99        window
100            .spawn(cx, async move |cx| {
101                let base_ref = default_branch
102                    .await??
103                    .context("Could not determine default branch")?;
104
105                workspace.update_in(cx, |workspace, window, cx| {
106                    Self::deploy_branch_diff_with_base_ref(
107                        workspace,
108                        project,
109                        intended_repo,
110                        base_ref,
111                        window,
112                        cx,
113                    );
114                })?;
115
116                anyhow::Ok(())
117            })
118            .detach_and_notify_err(workspace_weak, window, cx);
119    }
120
121    fn compare_with_branch(
122        workspace: &mut Workspace,
123        _: &CompareWithBranch,
124        window: &mut Window,
125        cx: &mut Context<Workspace>,
126    ) {
127        let project = workspace.project().clone();
128        let Some(repository) = project.read(cx).active_repository(cx) else {
129            let workspace = cx.entity().downgrade();
130            window
131                .spawn(cx, async |_cx| {
132                    let result: Result<()> = Err(anyhow!("No active repository"));
133                    result
134                })
135                .detach_and_notify_err(workspace, window, cx);
136            return;
137        };
138        let selected_branch = workspace.active_item_as::<Self>(cx).and_then(|item| {
139            match item.read(cx).diff_base(cx) {
140                DiffBase::Merge { base_ref } => Some(base_ref.clone()),
141                DiffBase::Head | DiffBase::Index | DiffBase::Staged => None,
142            }
143        });
144        let workspace_handle = workspace.weak_handle();
145        let on_select = Arc::new({
146            let repository = repository.clone();
147            let workspace = workspace_handle.clone();
148            move |branch: git::repository::Branch, window: &mut Window, cx: &mut App| {
149                let base_ref: SharedString = branch.name().to_owned().into();
150                workspace
151                    .update(cx, |workspace, cx| {
152                        Self::deploy_branch_diff_with_base_ref(
153                            workspace,
154                            project.clone(),
155                            repository.clone(),
156                            base_ref,
157                            window,
158                            cx,
159                        );
160                    })
161                    .ok();
162            }
163        });
164
165        workspace.toggle_modal(window, cx, |window, cx| {
166            branch_picker::select_modal(
167                workspace_handle,
168                Some(repository),
169                selected_branch,
170                on_select,
171                window,
172                cx,
173            )
174        });
175    }
176
177    fn deploy_branch_diff_with_base_ref(
178        workspace: &mut Workspace,
179        project: Entity<Project>,
180        intended_repo: Entity<Repository>,
181        base_ref: SharedString,
182        window: &mut Window,
183        cx: &mut Context<Workspace>,
184    ) {
185        let existing = workspace.items_of_type::<Self>(cx).find(|item| {
186            let item = item.read(cx);
187            matches!(
188                item.diff_base(cx),
189                DiffBase::Merge { base_ref: existing_base_ref } if existing_base_ref == &base_ref
190            )
191        });
192        if let Some(existing) = existing {
193            workspace.activate_item(&existing, true, true, window, cx);
194
195            let needs_switch = existing.read(cx).repo(cx).map_or(true, |current| {
196                current.read(cx).id != intended_repo.read(cx).id
197            });
198
199            if needs_switch {
200                existing.update(cx, |branch_diff, cx| {
201                    branch_diff.set_repo(Some(intended_repo), cx);
202                });
203            }
204
205            return;
206        }
207
208        let workspace = cx.entity();
209        let workspace_weak = workspace.downgrade();
210        window
211            .spawn(cx, async move |cx| {
212                let this = cx
213                    .update(|window, cx| {
214                        Self::new_with_branch_base(
215                            project,
216                            workspace.clone(),
217                            base_ref,
218                            intended_repo,
219                            window,
220                            cx,
221                        )
222                    })?
223                    .await?;
224                workspace
225                    .update_in(cx, |workspace, window, cx| {
226                        workspace.add_item_to_active_pane(Box::new(this), None, true, window, cx);
227                    })
228                    .ok();
229                anyhow::Ok(())
230            })
231            .detach_and_notify_err(workspace_weak, window, cx);
232    }
233
234    #[cfg(any(test, feature = "test-support"))]
235    pub fn new_with_default_branch(
236        project: Entity<Project>,
237        workspace: Entity<Workspace>,
238        window: &mut Window,
239        cx: &mut App,
240    ) -> Task<Result<Entity<Self>>> {
241        let Some(repo) = project.read(cx).git_store().read(cx).active_repository() else {
242            return Task::ready(Err(anyhow!("No active repository")));
243        };
244        let main_branch = repo.update(cx, |repo, _| repo.default_branch(true));
245        window.spawn(cx, async move |cx| {
246            let base_ref = main_branch
247                .await??
248                .context("Could not determine default branch")?;
249            cx.update(|window, cx| {
250                cx.new(|cx| {
251                    Self::new_with_base_ref(project, workspace, base_ref, Some(repo), window, cx)
252                })
253            })
254        })
255    }
256
257    pub(crate) fn new_with_branch_base(
258        project: Entity<Project>,
259        workspace: Entity<Workspace>,
260        base_ref: SharedString,
261        repo: Entity<Repository>,
262        window: &mut Window,
263        cx: &mut App,
264    ) -> Task<Result<Entity<Self>>> {
265        window.spawn(cx, async move |cx| {
266            cx.update(|window, cx| {
267                cx.new(|cx| {
268                    Self::new_with_base_ref(project, workspace, base_ref, Some(repo), window, cx)
269                })
270            })
271        })
272    }
273
274    pub(crate) fn new_with_base_ref(
275        project: Entity<Project>,
276        workspace: Entity<Workspace>,
277        base_ref: SharedString,
278        repo: Option<Entity<Repository>>,
279        window: &mut Window,
280        cx: &mut Context<Self>,
281    ) -> Self {
282        let branch_diff = cx.new(|cx| {
283            let mut branch_diff = diff_buffer_list::DiffBufferList::new(
284                DiffBase::Merge { base_ref },
285                project.clone(),
286                window,
287                cx,
288            );
289            if repo.is_some() {
290                branch_diff.set_repo(repo, cx);
291            }
292            branch_diff
293        });
294        let branch_diff_for_addon = branch_diff.clone();
295        let diff = cx.new(|cx| {
296            DiffMultibuffer::new(
297                branch_diff,
298                Capability::ReadWrite,
299                "No changes",
300                move |editor, cx| {
301                    editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyDiffHunkDelegate)), cx);
302                    editor.rhs_editor().update(cx, move |rhs_editor, _cx| {
303                        rhs_editor.set_read_only(false);
304                        rhs_editor.register_addon(BranchDiffAddon {
305                            branch_diff: branch_diff_for_addon,
306                        });
307                    });
308                },
309                project.clone(),
310                workspace.clone(),
311                window,
312                cx,
313            )
314        });
315        Self::from_diff(diff, project, workspace, cx)
316    }
317
318    fn from_diff(
319        diff: Entity<DiffMultibuffer>,
320        project: Entity<Project>,
321        workspace: Entity<Workspace>,
322        cx: &mut Context<Self>,
323    ) -> Self {
324        let diff_event_subscription = cx.subscribe(&diff, |_, _, event: &EditorEvent, cx| {
325            cx.emit(event.clone())
326        });
327        Self {
328            diff,
329            project,
330            workspace: workspace.downgrade(),
331            _diff_event_subscription: diff_event_subscription,
332        }
333    }
334
335    pub(crate) fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase {
336        self.diff.read(cx).diff_base(cx)
337    }
338
339    pub(crate) fn repo(&self, cx: &App) -> Option<Entity<Repository>> {
340        self.diff.read(cx).repo(cx)
341    }
342
343    pub(crate) fn set_repo(&mut self, repo: Option<Entity<Repository>>, cx: &mut Context<Self>) {
344        self.diff.update(cx, |diff, cx| diff.set_repo(repo, cx));
345    }
346
347    fn set_merge_base(&mut self, base_ref: SharedString, cx: &mut Context<Self>) {
348        self.diff.update(cx, |diff, cx| {
349            diff.branch_diff().update(cx, |branch_diff, cx| {
350                branch_diff.set_diff_base(DiffBase::Merge { base_ref }, cx);
351            });
352        });
353    }
354
355    fn review_diff(&mut self, _: &ReviewDiff, window: &mut Window, cx: &mut Context<Self>) {
356        let DiffBase::Merge { base_ref } = self.diff_base(cx).clone() else {
357            return;
358        };
359        let Some(repo) = self.repo(cx) else {
360            return;
361        };
362
363        let diff_receiver = repo.update(cx, |repo, cx| {
364            repo.diff(
365                DiffType::MergeBase {
366                    base_ref: base_ref.clone(),
367                },
368                cx,
369            )
370        });
371
372        let workspace = self.workspace.clone();
373        window
374            .spawn(cx, {
375                let workspace = workspace.clone();
376                async move |cx| {
377                    let diff_text = diff_receiver.await??;
378
379                    if let Some(workspace) = workspace.upgrade() {
380                        workspace.update_in(cx, |_workspace, window, cx| {
381                            window.dispatch_action(
382                                ReviewBranchDiff {
383                                    diff_text: diff_text.into(),
384                                    base_ref,
385                                }
386                                .boxed_clone(),
387                                cx,
388                            );
389                        })?;
390                    }
391
392                    anyhow::Ok(())
393                }
394            })
395            .detach_and_notify_err(workspace, window, cx);
396    }
397
398    #[cfg(any(test, feature = "test-support"))]
399    pub fn editor(&self, cx: &App) -> Entity<SplittableEditor> {
400        self.diff.read(cx).editor().clone()
401    }
402}
403
404impl EventEmitter<EditorEvent> for BranchDiff {}
405
406impl Focusable for BranchDiff {
407    fn focus_handle(&self, cx: &App) -> FocusHandle {
408        self.diff.read(cx).focus_handle(cx)
409    }
410}
411
412impl Item for BranchDiff {
413    type Event = EditorEvent;
414
415    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
416        Some(Icon::new(IconName::GitBranch).color(Color::Muted))
417    }
418
419    fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
420        Editor::to_item_events(event, f)
421    }
422
423    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
424        self.diff
425            .update(cx, |diff, cx| diff.deactivated(window, cx));
426    }
427
428    fn navigate(
429        &mut self,
430        data: Arc<dyn Any + Send>,
431        window: &mut Window,
432        cx: &mut Context<Self>,
433    ) -> bool {
434        self.diff
435            .update(cx, |diff, cx| diff.navigate(data, window, cx))
436    }
437
438    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
439        Some(self.tab_content_text(0, cx))
440    }
441
442    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
443        Label::new(self.tab_content_text(0, cx))
444            .color(if params.selected {
445                Color::Default
446            } else {
447                Color::Muted
448            })
449            .into_any_element()
450    }
451
452    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
453        match self.diff_base(cx) {
454            DiffBase::Merge { base_ref } => format!("Changes since {}", base_ref).into(),
455            DiffBase::Head | DiffBase::Index | DiffBase::Staged => "Changes".into(),
456        }
457    }
458
459    fn telemetry_event_text(&self) -> Option<&'static str> {
460        Some("Branch Diff Opened")
461    }
462
463    fn as_searchable(&self, _: &Entity<Self>, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
464        Some(Box::new(self.diff.read(cx).editor().clone()))
465    }
466
467    fn for_each_project_item(
468        &self,
469        cx: &App,
470        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
471    ) {
472        self.diff.read(cx).for_each_project_item(cx, f);
473    }
474
475    fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
476        self.diff.read(cx).active_project_path(cx)
477    }
478
479    fn set_nav_history(
480        &mut self,
481        nav_history: ItemNavHistory,
482        _: &mut Window,
483        cx: &mut Context<Self>,
484    ) {
485        self.diff
486            .update(cx, |diff, cx| diff.set_nav_history(nav_history, cx));
487    }
488
489    fn can_split(&self) -> bool {
490        true
491    }
492
493    fn clone_on_split(
494        &self,
495        _workspace_id: Option<workspace::WorkspaceId>,
496        window: &mut Window,
497        cx: &mut Context<Self>,
498    ) -> Task<Option<Entity<Self>>>
499    where
500        Self: Sized,
501    {
502        let Some(workspace) = self.workspace.upgrade() else {
503            return Task::ready(None);
504        };
505        let DiffBase::Merge { base_ref } = self.diff_base(cx).clone() else {
506            return Task::ready(None);
507        };
508        let repo = self.repo(cx);
509        let project = self.project.clone();
510        Task::ready(Some(cx.new(|cx| {
511            Self::new_with_base_ref(project, workspace, base_ref, repo, window, cx)
512        })))
513    }
514
515    fn is_dirty(&self, cx: &App) -> bool {
516        self.diff.read(cx).is_dirty(cx)
517    }
518
519    fn has_conflict(&self, cx: &App) -> bool {
520        self.diff.read(cx).has_conflict(cx)
521    }
522
523    fn can_save(&self, _cx: &App) -> bool {
524        true
525    }
526
527    fn save(
528        &mut self,
529        options: SaveOptions,
530        project: Entity<Project>,
531        window: &mut Window,
532        cx: &mut Context<Self>,
533    ) -> Task<Result<()>> {
534        self.diff
535            .update(cx, |diff, cx| diff.save(options, project, window, cx))
536    }
537
538    fn save_as(
539        &mut self,
540        _: Entity<Project>,
541        _: ProjectPath,
542        _: &mut Window,
543        _: &mut Context<Self>,
544    ) -> Task<Result<()>> {
545        unreachable!()
546    }
547
548    fn reload(
549        &mut self,
550        project: Entity<Project>,
551        window: &mut Window,
552        cx: &mut Context<Self>,
553    ) -> Task<Result<()>> {
554        self.diff
555            .update(cx, |diff, cx| diff.reload(project, window, cx))
556    }
557
558    fn act_as_type<'a>(
559        &'a self,
560        type_id: TypeId,
561        self_handle: &'a Entity<Self>,
562        cx: &'a App,
563    ) -> Option<gpui::AnyEntity> {
564        if type_id == TypeId::of::<Self>() {
565            Some(self_handle.clone().into())
566        } else if type_id == TypeId::of::<DiffMultibuffer>() {
567            Some(self.diff.clone().into())
568        } else if type_id == TypeId::of::<Editor>() {
569            Some(
570                self.diff
571                    .read(cx)
572                    .editor()
573                    .read(cx)
574                    .rhs_editor()
575                    .clone()
576                    .into(),
577            )
578        } else if type_id == TypeId::of::<SplittableEditor>() {
579            Some(self.diff.read(cx).editor().clone().into())
580        } else if type_id == TypeId::of::<diff_buffer_list::DiffBufferList>() {
581            Some(self.diff.read(cx).branch_diff().clone().into())
582        } else {
583            None
584        }
585    }
586
587    fn added_to_workspace(
588        &mut self,
589        workspace: &mut Workspace,
590        window: &mut Window,
591        cx: &mut Context<Self>,
592    ) {
593        self.diff.update(cx, |diff, cx| {
594            diff.added_to_workspace(workspace, window, cx)
595        });
596    }
597}
598
599impl Render for BranchDiff {
600    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
601        div()
602            .size_full()
603            .on_action(cx.listener(Self::review_diff))
604            .child(self.diff.clone())
605    }
606}
607
608impl SerializableItem for BranchDiff {
609    fn serialized_item_kind() -> &'static str {
610        "BranchDiff"
611    }
612
613    fn cleanup(
614        _: workspace::WorkspaceId,
615        _: Vec<workspace::ItemId>,
616        _: &mut Window,
617        _: &mut App,
618    ) -> Task<Result<()>> {
619        Task::ready(Ok(()))
620    }
621
622    fn deserialize(
623        project: Entity<Project>,
624        workspace: WeakEntity<Workspace>,
625        workspace_id: workspace::WorkspaceId,
626        item_id: workspace::ItemId,
627        window: &mut Window,
628        cx: &mut App,
629    ) -> Task<Result<Entity<Self>>> {
630        let db = project_diff::persistence::ProjectDiffDb::global(cx);
631        window.spawn(cx, async move |cx| {
632            let diff_base = db.get_project_diff_base(item_id, workspace_id)?;
633            let DiffBase::Merge { base_ref } = diff_base else {
634                anyhow::bail!("expected a merge base for a branch diff");
635            };
636            let workspace = workspace.upgrade().context("workspace gone")?;
637            cx.update(|window, cx| {
638                cx.new(|cx| Self::new_with_base_ref(project, workspace, base_ref, None, window, cx))
639            })
640        })
641    }
642
643    fn serialize(
644        &mut self,
645        workspace: &mut Workspace,
646        item_id: workspace::ItemId,
647        _closing: bool,
648        _window: &mut Window,
649        cx: &mut Context<Self>,
650    ) -> Option<Task<Result<()>>> {
651        let workspace_id = workspace.database_id()?;
652        let DiffBase::Merge { base_ref } = self.diff_base(cx).clone() else {
653            return None;
654        };
655        let diff_base = DiffBase::Merge { base_ref };
656        let db = project_diff::persistence::ProjectDiffDb::global(cx);
657        Some(cx.background_spawn(async move {
658            db.save_project_diff_base(item_id, workspace_id, diff_base)
659                .await
660        }))
661    }
662
663    fn should_serialize(&self, _: &Self::Event) -> bool {
664        false
665    }
666}
667
668pub struct BranchDiffToolbar {
669    branch_diff: Option<WeakEntity<BranchDiff>>,
670}
671
672impl BranchDiffToolbar {
673    pub fn new(_cx: &mut Context<Self>) -> Self {
674        Self { branch_diff: None }
675    }
676
677    fn branch_diff(&self, _: &App) -> Option<Entity<BranchDiff>> {
678        self.branch_diff.as_ref()?.upgrade()
679    }
680
681    fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
682        if let Some(branch_diff) = self.branch_diff(cx) {
683            branch_diff.focus_handle(cx).focus(window, cx);
684        }
685        let action = action.boxed_clone();
686        cx.defer(move |cx| {
687            cx.dispatch_action(action.as_ref());
688        })
689    }
690}
691
692impl EventEmitter<ToolbarItemEvent> for BranchDiffToolbar {}
693
694impl ToolbarItemView for BranchDiffToolbar {
695    fn set_active_pane_item(
696        &mut self,
697        active_pane_item: Option<&dyn ItemHandle>,
698        _: &mut Window,
699        cx: &mut Context<Self>,
700    ) -> ToolbarItemLocation {
701        self.branch_diff = active_pane_item
702            .and_then(|item| item.act_as::<BranchDiff>(cx))
703            .map(|entity| entity.downgrade());
704        if self.branch_diff.is_some() {
705            ToolbarItemLocation::PrimaryRight
706        } else {
707            ToolbarItemLocation::Hidden
708        }
709    }
710
711    fn pane_focus_update(
712        &mut self,
713        _pane_focused: bool,
714        _window: &mut Window,
715        _cx: &mut Context<Self>,
716    ) {
717    }
718}
719
720impl Render for BranchDiffToolbar {
721    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
722        let Some(branch_diff) = self.branch_diff(cx) else {
723            return div();
724        };
725        let focus_handle = branch_diff.focus_handle(cx);
726        let review_count = branch_diff
727            .read(cx)
728            .diff
729            .read(cx)
730            .total_review_comment_count();
731        let (additions, deletions) = branch_diff
732            .read(cx)
733            .diff
734            .read(cx)
735            .calculate_changed_lines(cx);
736        let diff_base = branch_diff.read(cx).diff_base(cx).clone();
737        let DiffBase::Merge { base_ref } = diff_base else {
738            return div();
739        };
740        let selected_base_ref = base_ref.clone();
741        let base_ref_label = format!("Base: {base_ref}");
742        let repository = branch_diff.read(cx).repo(cx);
743        let workspace = branch_diff.read(cx).workspace.clone();
744        let view_for_picker = branch_diff.downgrade();
745
746        let is_multibuffer_empty = branch_diff
747            .read(cx)
748            .diff
749            .read(cx)
750            .multibuffer()
751            .read(cx)
752            .is_empty();
753        let is_ai_enabled = AgentSettings::get_global(cx).enabled(cx);
754
755        let show_review_button = !is_multibuffer_empty && is_ai_enabled;
756
757        h_flex()
758            .my_neg_1()
759            .py_1()
760            .gap_1p5()
761            .flex_wrap()
762            .justify_between()
763            .when(!is_multibuffer_empty, |this| {
764                this.child(DiffStat::new(
765                    "branch-diff-stat",
766                    additions as usize,
767                    deletions as usize,
768                ))
769            })
770            .child(Divider::vertical().ml_1())
771            .child(
772                PopoverMenu::new("branch-diff-base-branch-picker")
773                    .menu(move |window, cx| {
774                        let view_for_picker = view_for_picker.clone();
775                        let on_select = Arc::new(
776                            move |branch: git::repository::Branch,
777                                  _window: &mut Window,
778                                  cx: &mut App| {
779                                let base_ref: SharedString = branch.name().to_owned().into();
780                                view_for_picker
781                                    .update(cx, |branch_diff, cx| {
782                                        branch_diff.set_merge_base(base_ref, cx);
783                                        cx.notify();
784                                    })
785                                    .ok();
786                            },
787                        );
788
789                        Some(branch_picker::select_popover(
790                            workspace.clone(),
791                            repository.clone(),
792                            Some(selected_base_ref.clone()),
793                            on_select,
794                            window,
795                            cx,
796                        ))
797                    })
798                    .trigger_with_tooltip(
799                        Button::new("branch-diff-base-branch", base_ref_label).end_icon(
800                            Icon::new(IconName::ChevronDown)
801                                .size(IconSize::XSmall)
802                                .color(Color::Muted),
803                        ),
804                        Tooltip::text("Select Base Branch"),
805                    ),
806            )
807            .when(show_review_button, |this| {
808                let focus_handle = focus_handle.clone();
809                this.child(Divider::vertical()).child(
810                    Button::new("review-diff", "Review Diff")
811                        .start_icon(
812                            Icon::new(IconName::OmegaAssistant)
813                                .size(IconSize::Small)
814                                .color(Color::Muted),
815                        )
816                        .tooltip(move |_, cx| {
817                            Tooltip::with_meta_in(
818                                "Review Diff",
819                                Some(&ReviewDiff),
820                                "Send this diff for your last agent to review.",
821                                &focus_handle,
822                                cx,
823                            )
824                        })
825                        .on_click(cx.listener(|this, _, window, cx| {
826                            this.dispatch_action(&ReviewDiff, window, cx);
827                        })),
828                )
829            })
830            .when(review_count > 0, |this| {
831                this.child(Divider::vertical()).child(
832                    render_send_review_to_agent_button(review_count, &focus_handle).on_click(
833                        cx.listener(|this, _, window, cx| {
834                            this.dispatch_action(&SendReviewToAgent, window, cx)
835                        }),
836                    ),
837                )
838            })
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use anyhow::anyhow;
845    use collections::HashMap;
846    use editor::test::editor_test_context::assert_state_with_diff;
847    use git::status::{FileStatus, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
848    use gpui::TestAppContext;
849    use project::FakeFs;
850    use serde_json::json;
851    use settings::{DiffViewStyle, SettingsStore};
852    use std::path::Path;
853    use std::sync::Arc;
854    use unindent::Unindent as _;
855    use util::{
856        path,
857        rel_path::{RelPath, rel_path},
858    };
859    use workspace::MultiWorkspace;
860
861    use super::*;
862
863    fn init_test(cx: &mut TestAppContext) {
864        cx.update(|cx| {
865            let store = SettingsStore::test(cx);
866            cx.set_global(store);
867            cx.update_global::<SettingsStore, _>(|store, cx| {
868                store.update_user_settings(cx, |settings| {
869                    settings.editor.diff_view_style = Some(DiffViewStyle::Unified);
870                });
871            });
872            theme_settings::init(theme::LoadThemes::JustBase, cx);
873            editor::init(cx);
874            crate::init(cx);
875        });
876    }
877
878    #[gpui::test(iterations = 50)]
879    async fn test_split_diff_conflict_path_transition_with_dirty_buffer_invalid_anchor_panics(
880        cx: &mut TestAppContext,
881    ) {
882        init_test(cx);
883
884        cx.update(|cx| {
885            cx.update_global::<SettingsStore, _>(|store, cx| {
886                store.update_user_settings(cx, |settings| {
887                    settings.editor.diff_view_style = Some(DiffViewStyle::Split);
888                });
889            });
890        });
891
892        let build_conflict_text: fn(usize) -> String = |tag: usize| {
893            let mut lines = (0..80)
894                .map(|line_index| format!("line {line_index}"))
895                .collect::<Vec<_>>();
896            for offset in [5usize, 20, 37, 61] {
897                lines[offset] = format!("base-{tag}-line-{offset}");
898            }
899            format!("{}\n", lines.join("\n"))
900        };
901        let initial_conflict_text = build_conflict_text(0);
902        let fs = FakeFs::new(cx.executor());
903        fs.insert_tree(
904            path!("/project"),
905            json!({
906                ".git": {},
907                "helper.txt": "same\n",
908                "conflict.txt": initial_conflict_text,
909            }),
910        )
911        .await;
912        fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
913            state
914                .refs
915                .insert("MERGE_HEAD".into(), "conflict-head".into());
916        })
917        .unwrap();
918        fs.set_status_for_repo(
919            path!("/project/.git").as_ref(),
920            &[(
921                "conflict.txt",
922                FileStatus::Unmerged(UnmergedStatus {
923                    first_head: UnmergedStatusCode::Updated,
924                    second_head: UnmergedStatusCode::Updated,
925                }),
926            )],
927        );
928        fs.set_merge_base_content_for_repo(
929            path!("/project/.git").as_ref(),
930            &[
931                ("conflict.txt", build_conflict_text(1)),
932                ("helper.txt", "same\n".to_string()),
933            ],
934        );
935
936        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
937        let (multi_workspace, cx) =
938            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
939        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
940        let _branch_diff = cx
941            .update(|window, cx| {
942                BranchDiff::new_with_default_branch(project.clone(), workspace, window, cx)
943            })
944            .await
945            .unwrap();
946        cx.run_until_parked();
947
948        let buffer = project
949            .update(cx, |project, cx| {
950                project.open_local_buffer(path!("/project/conflict.txt"), cx)
951            })
952            .await
953            .unwrap();
954        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "dirty\n")], None, cx));
955        assert!(buffer.read_with(cx, |buffer, _| buffer.is_dirty()));
956        cx.run_until_parked();
957
958        cx.update(|window, cx| {
959            let fs = fs.clone();
960            window
961                .spawn(cx, async move |cx| {
962                    cx.background_executor().simulate_random_delay().await;
963                    fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
964                        state.refs.insert("HEAD".into(), "head-1".into());
965                        state.refs.remove("MERGE_HEAD");
966                    })
967                    .unwrap();
968                    fs.set_status_for_repo(
969                        path!("/project/.git").as_ref(),
970                        &[
971                            (
972                                "conflict.txt",
973                                FileStatus::Tracked(TrackedStatus {
974                                    index_status: git::status::StatusCode::Modified,
975                                    worktree_status: git::status::StatusCode::Modified,
976                                }),
977                            ),
978                            (
979                                "helper.txt",
980                                FileStatus::Tracked(TrackedStatus {
981                                    index_status: git::status::StatusCode::Modified,
982                                    worktree_status: git::status::StatusCode::Modified,
983                                }),
984                            ),
985                        ],
986                    );
987                    // FakeFs assigns deterministic OIDs by entry position; flipping order churns
988                    // conflict diff identity without reaching into view internals.
989                    fs.set_merge_base_content_for_repo(
990                        path!("/project/.git").as_ref(),
991                        &[
992                            ("helper.txt", "helper-base\n".to_string()),
993                            ("conflict.txt", build_conflict_text(2)),
994                        ],
995                    );
996                })
997                .detach();
998        });
999
1000        cx.update(|window, cx| {
1001            let buffer = buffer.clone();
1002            window
1003                .spawn(cx, async move |cx| {
1004                    cx.background_executor().simulate_random_delay().await;
1005                    for edit_index in 0..10 {
1006                        if edit_index > 0 {
1007                            cx.background_executor().simulate_random_delay().await;
1008                        }
1009                        buffer.update(cx, |buffer, cx| {
1010                            let len = buffer.len();
1011                            if edit_index % 2 == 0 {
1012                                buffer.edit(
1013                                    [(0..0, format!("status-burst-head-{edit_index}\n"))],
1014                                    None,
1015                                    cx,
1016                                );
1017                            } else {
1018                                buffer.edit(
1019                                    [(len..len, format!("status-burst-tail-{edit_index}\n"))],
1020                                    None,
1021                                    cx,
1022                                );
1023                            }
1024                        });
1025                    }
1026                })
1027                .detach();
1028        });
1029
1030        cx.run_until_parked();
1031    }
1032
1033    #[gpui::test]
1034    async fn test_branch_diff(cx: &mut TestAppContext) {
1035        init_test(cx);
1036
1037        let fs = FakeFs::new(cx.executor());
1038        fs.insert_tree(
1039            path!("/project"),
1040            json!({
1041                ".git": {},
1042                "a.txt": "C",
1043                "b.txt": "new",
1044                "c.txt": "in-merge-base-and-work-tree",
1045                "d.txt": "created-in-head",
1046            }),
1047        )
1048        .await;
1049        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1050        let (multi_workspace, cx) =
1051            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1052        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1053        let diff = cx
1054            .update(|window, cx| {
1055                BranchDiff::new_with_default_branch(project.clone(), workspace, window, cx)
1056            })
1057            .await
1058            .unwrap();
1059        cx.run_until_parked();
1060
1061        fs.set_head_for_repo(
1062            Path::new(path!("/project/.git")),
1063            &[("a.txt", "B".into()), ("d.txt", "created-in-head".into())],
1064            "sha",
1065        );
1066        fs.set_merge_base_content_for_repo(
1067            Path::new(path!("/project/.git")),
1068            &[
1069                ("a.txt", "A".into()),
1070                ("c.txt", "in-merge-base-and-work-tree".into()),
1071            ],
1072        );
1073        cx.run_until_parked();
1074
1075        let editor = diff.read_with(cx, |diff, cx| diff.editor(cx).read(cx).rhs_editor().clone());
1076
1077        assert_state_with_diff(
1078            &editor,
1079            cx,
1080            &"
1081                - A
1082                + ˇC
1083                + new
1084                + created-in-head"
1085                .unindent(),
1086        );
1087
1088        let statuses: HashMap<Arc<RelPath>, Option<FileStatus>> =
1089            editor.update(cx, |editor, cx| {
1090                editor
1091                    .buffer()
1092                    .read(cx)
1093                    .all_buffers()
1094                    .iter()
1095                    .map(|buffer| {
1096                        (
1097                            buffer.read(cx).file().unwrap().path().clone(),
1098                            editor.status_for_buffer_id(buffer.read(cx).remote_id(), cx),
1099                        )
1100                    })
1101                    .collect()
1102            });
1103
1104        assert_eq!(
1105            statuses,
1106            HashMap::from_iter([
1107                (
1108                    rel_path("a.txt").into_arc(),
1109                    Some(FileStatus::Tracked(TrackedStatus {
1110                        index_status: git::status::StatusCode::Modified,
1111                        worktree_status: git::status::StatusCode::Modified
1112                    }))
1113                ),
1114                (rel_path("b.txt").into_arc(), Some(FileStatus::Untracked)),
1115                (
1116                    rel_path("d.txt").into_arc(),
1117                    Some(FileStatus::Tracked(TrackedStatus {
1118                        index_status: git::status::StatusCode::Added,
1119                        worktree_status: git::status::StatusCode::Added
1120                    }))
1121                )
1122            ])
1123        );
1124    }
1125
1126    #[gpui::test]
1127    async fn test_branch_diff_action_matches_existing_item_by_base_ref(cx: &mut TestAppContext) {
1128        init_test(cx);
1129
1130        let fs = FakeFs::new(cx.executor());
1131        fs.insert_tree(
1132            path!("/project"),
1133            json!({
1134                ".git": {},
1135                "a.txt": "changed",
1136            }),
1137        )
1138        .await;
1139        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1140        let (multi_workspace, cx) =
1141            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1142        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1143
1144        let target_branch_diff = cx
1145            .update(|window, cx| {
1146                let Some(repository) = project.read(cx).active_repository(cx) else {
1147                    return Task::ready(Err(anyhow!("No active repository")));
1148                };
1149                BranchDiff::new_with_branch_base(
1150                    project.clone(),
1151                    workspace.clone(),
1152                    "topic".into(),
1153                    repository,
1154                    window,
1155                    cx,
1156                )
1157            })
1158            .await
1159            .unwrap();
1160        workspace.update_in(cx, |workspace, window, cx| {
1161            workspace.add_item_to_active_pane(
1162                Box::new(target_branch_diff.clone()),
1163                None,
1164                true,
1165                window,
1166                cx,
1167            );
1168        });
1169        cx.run_until_parked();
1170
1171        cx.focus(&workspace);
1172        cx.update(|window, cx| {
1173            window.dispatch_action(DeployBranchDiff.boxed_clone(), cx);
1174        });
1175        cx.run_until_parked();
1176
1177        let (active_base_ref, mut base_refs) = workspace.update(cx, |workspace, cx| {
1178            let active_item = workspace.active_item_as::<BranchDiff>(cx).unwrap();
1179            let active_base_ref = match active_item.read(cx).diff_base(cx) {
1180                DiffBase::Merge { base_ref } => base_ref.to_string(),
1181                DiffBase::Head | DiffBase::Index | DiffBase::Staged => {
1182                    panic!("expected active item to be a branch diff")
1183                }
1184            };
1185            let base_refs = workspace
1186                .items_of_type::<BranchDiff>(cx)
1187                .filter_map(|item| match item.read(cx).diff_base(cx) {
1188                    DiffBase::Merge { base_ref } => Some(base_ref.to_string()),
1189                    DiffBase::Head | DiffBase::Index | DiffBase::Staged => None,
1190                })
1191                .collect::<Vec<_>>();
1192            (active_base_ref, base_refs)
1193        });
1194        base_refs.sort();
1195
1196        assert_eq!(active_base_ref, "origin/main");
1197        assert_eq!(base_refs, vec!["origin/main", "topic"]);
1198    }
1199}
1200
Served at tenant.openagents/omega Member data and write actions are omitted.