Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:34:26.418Z 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

git_ui.rs

1421 lines · 48.1 KB · rust
1use anyhow::anyhow;
2use commit_modal::CommitModal;
3use editor::{Editor, actions::DiffClipboardWithSelectionData};
4
5use workspace::{Toast, notifications::NotificationId};
6
7mod blame_ui;
8pub mod clone;
9
10use git::{
11    repository::{Branch, CommitDetails, Upstream, UpstreamTracking, UpstreamTrackingStatus},
12    status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
13};
14use gpui::{
15    App, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
16    SharedString, Subscription, Task, TaskExt, WeakEntity, Window,
17};
18use menu::{Cancel, Confirm};
19use project::git_store::Repository;
20use project_diff::ProjectDiff;
21use time::OffsetDateTime;
22use ui::{ButtonLike, ContextMenu, ElevationIndex, PopoverMenuHandle, TintColor, prelude::*};
23use workspace::{
24    ModalView, OpenMode, Workspace,
25    notifications::{DetachAndPromptErr, NotifyTaskExt},
26};
27use zed_actions;
28
29use crate::{
30    commit_view::CommitView,
31    git_panel::{GitPanel, GitStatusEntry, RemoteOperationKind},
32    solo_diff_view::SoloDiffView,
33    text_diff_view::TextDiffView,
34};
35
36mod askpass_modal;
37pub mod branch_diff;
38pub mod branch_picker;
39mod commit_context_menu;
40mod commit_modal;
41pub mod commit_tooltip;
42pub mod commit_view;
43mod conflict_view;
44pub mod created_worktrees;
45mod diff_multibuffer;
46pub mod file_diff_view;
47pub mod git_graph;
48pub mod git_panel;
49mod git_panel_settings;
50pub mod git_picker;
51mod git_runtime_diagnostics;
52pub mod multi_diff_view;
53pub mod picker_prompt;
54pub mod project_diff;
55pub(crate) mod remote_output;
56pub mod repository_selector;
57pub mod solo_diff_view;
58pub mod staged_diff;
59pub mod stash_picker;
60pub mod text_diff_view;
61pub mod unstaged_diff;
62pub mod worktree_names;
63pub mod worktree_picker;
64pub mod worktree_service;
65
66pub use blame_ui::GitBlameStatus;
67pub use conflict_view::MergeConflictIndicator;
68
69pub fn get_provider_icon(name: &str) -> IconName {
70    match name {
71        "Bitbucket" => IconName::Bitbucket,
72        "Chromium" => IconName::Gerrit,
73        "Codeberg" => IconName::Codeberg,
74        "Forgejo Self-Hosted" => IconName::Forgejo,
75        "GitHub" => IconName::Github,
76        "GitLab" => IconName::Gitlab,
77        "Gitea" => IconName::Gitea,
78        "SourceHut" => IconName::Sourcehut,
79        _ => IconName::Link,
80    }
81}
82
83pub fn init(cx: &mut App) {
84    editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
85    commit_view::init(cx);
86    git_graph::init(cx);
87
88    cx.observe_new(|editor: &mut Editor, _, cx| {
89        conflict_view::register_editor(editor, editor.buffer().clone(), cx);
90    })
91    .detach();
92
93    cx.observe_new(|workspace: &mut Workspace, _, cx| {
94        ProjectDiff::register(workspace, cx);
95        staged_diff::StagedDiff::register(workspace, cx);
96        unstaged_diff::UnstagedDiff::register(workspace, cx);
97        branch_diff::BranchDiff::register(workspace, cx);
98        CommitModal::register(workspace);
99        git_panel::register(workspace);
100        repository_selector::register(workspace);
101        git_picker::register(workspace);
102
103        workspace.register_action(
104            |workspace, action: &zed_actions::CreateWorktree, window, cx| {
105                worktree_service::handle_create_worktree(workspace, action, window, None, cx);
106            },
107        );
108        workspace.register_action(
109            |workspace, action: &zed_actions::SwitchWorktree, window, cx| {
110                worktree_service::handle_switch_worktree(workspace, action, window, None, cx);
111            },
112        );
113
114        workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| {
115            let focused_dock = workspace.focused_dock_position(window, cx);
116            let project = workspace.project().clone();
117            let workspace_handle = workspace.weak_handle();
118            workspace.toggle_modal(window, cx, |window, cx| {
119                worktree_picker::WorktreePicker::new_modal(
120                    project,
121                    workspace_handle,
122                    focused_dock,
123                    window,
124                    cx,
125                )
126            });
127        });
128
129        workspace.register_action(
130            |workspace, action: &zed_actions::OpenWorktreeInNewWindow, window, cx| {
131                let path = action.path.clone();
132                let is_remote = !workspace.project().read(cx).is_local();
133
134                if is_remote {
135                    let connection_options =
136                        workspace.project().read(cx).remote_connection_options(cx);
137                    let app_state = workspace.app_state().clone();
138                    let workspace_handle = workspace.weak_handle();
139                    cx.spawn_in(window, async move |_, cx| {
140                        if let Some(connection_options) = connection_options {
141                            crate::worktree_picker::open_remote_worktree(
142                                connection_options,
143                                vec![path],
144                                app_state,
145                                workspace_handle,
146                                cx,
147                            )
148                            .await?;
149                        }
150                        anyhow::Ok(())
151                    })
152                    .detach_and_log_err(cx);
153                } else {
154                    workspace
155                        .open_workspace_for_paths(OpenMode::NewWindow, vec![path], window, cx)
156                        .detach_and_log_err(cx);
157                }
158            },
159        );
160
161        let project = workspace.project().read(cx);
162        if project.is_read_only(cx) {
163            return;
164        }
165        if !project.is_via_collab() {
166            workspace.register_action(
167                |workspace, _: &zed_actions::git::CreatePullRequest, window, cx| {
168                    if let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) {
169                        panel.update(cx, |panel, cx| {
170                            panel.create_pull_request(window, cx);
171                        });
172                    }
173                },
174            );
175            workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
176                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
177                    return;
178                };
179                panel.update(cx, |panel, cx| {
180                    panel.fetch(true, window, cx);
181                });
182            });
183            workspace.register_action(|workspace, _: &git::FetchFrom, window, cx| {
184                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
185                    return;
186                };
187                panel.update(cx, |panel, cx| {
188                    panel.fetch(false, window, cx);
189                });
190            });
191            workspace.register_action(|workspace, _: &git::Push, window, cx| {
192                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
193                    return;
194                };
195                panel.update(cx, |panel, cx| {
196                    panel.push(false, false, window, cx);
197                });
198            });
199            workspace.register_action(|workspace, _: &git::PushTo, window, cx| {
200                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
201                    return;
202                };
203                panel.update(cx, |panel, cx| {
204                    panel.push(false, true, window, cx);
205                });
206            });
207            workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
208                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
209                    return;
210                };
211                panel.update(cx, |panel, cx| {
212                    panel.push(true, false, window, cx);
213                });
214            });
215            workspace.register_action(|workspace, _: &git::Pull, window, cx| {
216                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
217                    return;
218                };
219                panel.update(cx, |panel, cx| {
220                    panel.pull(false, window, cx);
221                });
222            });
223            workspace.register_action(|workspace, _: &git::PullRebase, window, cx| {
224                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
225                    return;
226                };
227                panel.update(cx, |panel, cx| {
228                    panel.pull(true, window, cx);
229                });
230            });
231        }
232        workspace.register_action(|workspace, action: &git::StashAll, window, cx| {
233            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
234                return;
235            };
236            panel.update(cx, |panel, cx| {
237                panel.stash_all(action, window, cx);
238            });
239        });
240        workspace.register_action(|workspace, action: &git::StashPop, window, cx| {
241            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
242                return;
243            };
244            panel.update(cx, |panel, cx| {
245                panel.stash_pop(action, window, cx);
246            });
247        });
248        workspace.register_action(|workspace, action: &git::StashApply, window, cx| {
249            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
250                return;
251            };
252            panel.update(cx, |panel, cx| {
253                panel.stash_apply(action, window, cx);
254            });
255        });
256        workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
257            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
258                return;
259            };
260            panel.update(cx, |panel, cx| {
261                panel.stage_all(action, window, cx);
262            });
263        });
264        workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
265            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
266                return;
267            };
268            panel.update(cx, |panel, cx| {
269                panel.unstage_all(action, window, cx);
270            });
271        });
272        workspace.register_action(|workspace, _: &git::Uncommit, window, cx| {
273            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
274                return;
275            };
276            panel.update(cx, |panel, cx| {
277                panel.uncommit(window, cx);
278            })
279        });
280        workspace.register_action(|workspace, _action: &git::Init, window, cx| {
281            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
282                return;
283            };
284            panel.update(cx, |panel, cx| {
285                panel.git_init(window, cx);
286            });
287        });
288        workspace.register_action(|workspace, _action: &git::Clone, window, cx| {
289            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
290                return;
291            };
292
293            workspace.toggle_modal(window, cx, |window, cx| {
294                GitCloneModal::show(panel, window, cx)
295            });
296        });
297        workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
298            open_modified_files(workspace, window, cx);
299        });
300        workspace.register_action_renderer(|div, workspace, _window, cx| {
301            div.when_some(
302                file_diff_entry(workspace, cx),
303                |div, (entry, repository)| {
304                    let workspace = workspace.weak_handle();
305                    div.on_action(move |_: &git::OpenFileDiff, window, cx| {
306                        open_file_diff(
307                            entry.clone(),
308                            repository.clone(),
309                            workspace.clone(),
310                            window,
311                            cx,
312                        );
313                    })
314                },
315            )
316        });
317        workspace.register_action(|workspace, _: &git::RenameBranch, window, cx| {
318            rename_current_branch(workspace, window, cx);
319        });
320        workspace.register_action(|workspace, _: &git::CopyBranchName, _, cx| {
321            copy_branch_name(workspace, cx);
322        });
323        workspace.register_action(show_ref_picker);
324        workspace.register_action(
325            |workspace, action: &DiffClipboardWithSelectionData, window, cx| {
326                if let Some(task) = TextDiffView::open(action, workspace, window, cx) {
327                    task.detach();
328                };
329            },
330        );
331    })
332    .detach();
333}
334
335fn open_file_diff(
336    entry: GitStatusEntry,
337    repository: Entity<Repository>,
338    workspace: WeakEntity<Workspace>,
339    window: &mut Window,
340    cx: &mut App,
341) {
342    window.defer(cx, move |window, cx| {
343        SoloDiffView::open_or_focus(entry, repository, workspace.clone(), window, cx)
344            .detach_and_notify_err(workspace, window, cx);
345    });
346}
347
348fn file_diff_entry(
349    workspace: &Workspace,
350    cx: &App,
351) -> Option<(GitStatusEntry, Entity<Repository>)> {
352    let project_path = workspace.active_item(cx)?.project_path(cx)?;
353
354    workspace
355        .project()
356        .read(cx)
357        .repositories(cx)
358        .values()
359        .find_map(|repository| {
360            let repo_path = repository
361                .read(cx)
362                .project_path_to_repo_path(&project_path, cx)?;
363            let status_entry = repository.read(cx).status_for_path(&repo_path)?;
364            Some((
365                GitStatusEntry {
366                    repo_path,
367                    status: status_entry.status,
368                    staging: status_entry.status.staging(),
369                    diff_stat: status_entry.diff_stat,
370                },
371                repository.clone(),
372            ))
373        })
374}
375
376fn open_modified_files(
377    workspace: &mut Workspace,
378    window: &mut Window,
379    cx: &mut Context<Workspace>,
380) {
381    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
382        return;
383    };
384    let modified_paths: Vec<_> = panel.update(cx, |panel, cx| {
385        let Some(repo) = panel.active_repository.as_ref() else {
386            return Vec::new();
387        };
388        let repo = repo.read(cx);
389        repo.cached_status()
390            .filter_map(|entry| {
391                if entry.status.is_modified() {
392                    repo.repo_path_to_project_path(&entry.repo_path, cx)
393                } else {
394                    None
395                }
396            })
397            .collect()
398    });
399    for path in modified_paths {
400        workspace.open_path(path, None, true, window, cx).detach();
401    }
402}
403
404pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
405    GitStatusIcon::new(status)
406}
407
408struct RenameBranchModal {
409    current_branch: SharedString,
410    editor: Entity<Editor>,
411    repo: Entity<Repository>,
412}
413
414impl RenameBranchModal {
415    fn new(
416        current_branch: String,
417        repo: Entity<Repository>,
418        window: &mut Window,
419        cx: &mut Context<Self>,
420    ) -> Self {
421        let editor = cx.new(|cx| {
422            let mut editor = Editor::single_line(window, cx);
423            editor.set_text(current_branch.clone(), window, cx);
424            editor
425        });
426        Self {
427            current_branch: current_branch.into(),
428            editor,
429            repo,
430        }
431    }
432
433    fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
434        cx.emit(DismissEvent);
435    }
436
437    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
438        let new_name = self.editor.read(cx).text(cx);
439        if new_name.is_empty() || new_name == self.current_branch.as_ref() {
440            cx.emit(DismissEvent);
441            return;
442        }
443
444        let repo = self.repo.clone();
445        let current_branch = self.current_branch.to_string();
446        cx.spawn(async move |_, cx| {
447            match repo
448                .update(cx, |repo, _| {
449                    repo.rename_branch(current_branch, new_name.clone())
450                })
451                .await
452            {
453                Ok(Ok(_)) => Ok(()),
454                Ok(Err(error)) => Err(error),
455                Err(_) => Err(anyhow!("Operation was canceled")),
456            }
457        })
458        .detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None);
459        cx.emit(DismissEvent);
460    }
461}
462
463impl EventEmitter<DismissEvent> for RenameBranchModal {}
464impl ModalView for RenameBranchModal {}
465impl Focusable for RenameBranchModal {
466    fn focus_handle(&self, cx: &App) -> FocusHandle {
467        self.editor.focus_handle(cx)
468    }
469}
470
471impl Render for RenameBranchModal {
472    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
473        v_flex()
474            .key_context("RenameBranchModal")
475            .on_action(cx.listener(Self::cancel))
476            .on_action(cx.listener(Self::confirm))
477            .elevation_2(cx)
478            .w(rems(34.))
479            .child(
480                h_flex()
481                    .px_3()
482                    .pt_2()
483                    .pb_1()
484                    .w_full()
485                    .gap_1p5()
486                    .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
487                    .child(
488                        Headline::new(format!("Rename Branch ({})", self.current_branch))
489                            .size(HeadlineSize::XSmall),
490                    ),
491            )
492            .child(div().px_3().pb_3().w_full().child(self.editor.clone()))
493    }
494}
495
496fn rename_current_branch(
497    workspace: &mut Workspace,
498    window: &mut Window,
499    cx: &mut Context<Workspace>,
500) {
501    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
502        return;
503    };
504    let current_branch: Option<String> = panel.update(cx, |panel, cx| {
505        let repo = panel.active_repository.as_ref()?;
506        let repo = repo.read(cx);
507        repo.branch.as_ref().map(|branch| branch.name().to_string())
508    });
509
510    let Some(current_branch_name) = current_branch else {
511        return;
512    };
513
514    let repo = panel.read(cx).active_repository.clone();
515    let Some(repo) = repo else {
516        return;
517    };
518
519    workspace.toggle_modal(window, cx, |window, cx| {
520        RenameBranchModal::new(current_branch_name, repo, window, cx)
521    });
522}
523
524fn copy_branch_name(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
525    let Some(panel) = workspace.panel::<GitPanel>(cx) else {
526        return;
527    };
528    let branch_name = panel.update(cx, |panel, cx| {
529        let repo = panel.active_repository.as_ref()?;
530        let repo = repo.read(cx);
531        repo.branch.as_ref().map(|branch| branch.name().to_string())
532    });
533    if let Some(name) = branch_name {
534        cx.write_to_clipboard(ClipboardItem::new_string(name));
535    }
536}
537
538struct RefPickerModal {
539    editor: Entity<Editor>,
540    repo: Entity<Repository>,
541    workspace: Entity<Workspace>,
542    commit_details: Option<CommitDetails>,
543    lookup_task: Option<Task<()>>,
544    _editor_subscription: Subscription,
545}
546
547impl RefPickerModal {
548    fn new(
549        repo: Entity<Repository>,
550        workspace: Entity<Workspace>,
551        window: &mut Window,
552        cx: &mut Context<Self>,
553    ) -> Self {
554        let editor = cx.new(|cx| {
555            let mut editor = Editor::single_line(window, cx);
556            editor.set_placeholder_text("Enter git ref...", window, cx);
557            editor
558        });
559
560        let _editor_subscription = cx.subscribe_in(
561            &editor,
562            window,
563            |this, _editor, event: &editor::EditorEvent, window, cx| {
564                if let editor::EditorEvent::BufferEdited = event {
565                    this.lookup_commit_details(window, cx);
566                }
567            },
568        );
569
570        Self {
571            editor,
572            repo,
573            workspace,
574            commit_details: None,
575            lookup_task: None,
576            _editor_subscription,
577        }
578    }
579
580    fn lookup_commit_details(&mut self, window: &mut Window, cx: &mut Context<Self>) {
581        let git_ref = self.editor.read(cx).text(cx);
582        let git_ref = git_ref.trim().to_string();
583
584        if git_ref.is_empty() {
585            self.commit_details = None;
586            cx.notify();
587            return;
588        }
589
590        let repo = self.repo.clone();
591        self.lookup_task = Some(cx.spawn_in(window, async move |this, cx| {
592            cx.background_executor()
593                .timer(std::time::Duration::from_millis(300))
594                .await;
595
596            let show_result = repo
597                .update(cx, |repo, _| repo.show(git_ref.clone()))
598                .await
599                .ok();
600
601            if let Some(show_future) = show_result {
602                if let Ok(details) = show_future {
603                    this.update(cx, |this, cx| {
604                        this.commit_details = Some(details);
605                        cx.notify();
606                    })
607                    .ok();
608                } else {
609                    this.update(cx, |this, cx| {
610                        this.commit_details = None;
611                        cx.notify();
612                    })
613                    .ok();
614                }
615            }
616        }));
617    }
618
619    fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
620        cx.emit(DismissEvent);
621    }
622
623    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
624        let git_ref = self.editor.read(cx).text(cx);
625        let git_ref = git_ref.trim();
626
627        if git_ref.is_empty() {
628            cx.emit(DismissEvent);
629            return;
630        }
631
632        let git_ref_string = git_ref.to_string();
633
634        let repo = self.repo.clone();
635        let workspace = self.workspace.clone();
636
637        window
638            .spawn(cx, async move |cx| -> anyhow::Result<()> {
639                let show_future = repo.update(cx, |repo, _| repo.show(git_ref_string.clone()));
640                let show_result = show_future.await;
641
642                match show_result {
643                    Ok(Ok(details)) => {
644                        workspace.update_in(cx, |workspace, window, cx| {
645                            CommitView::open(
646                                details.sha.to_string(),
647                                repo.downgrade(),
648                                workspace.weak_handle(),
649                                None,
650                                None,
651                                window,
652                                cx,
653                            );
654                        })?;
655                    }
656                    Ok(Err(_)) | Err(_) => {
657                        workspace.update(cx, |workspace, cx| {
658                            let error = anyhow::anyhow!("View commit failed");
659                            Self::show_git_error_toast(&git_ref_string, error, workspace, cx);
660                        });
661                    }
662                }
663
664                Ok(())
665            })
666            .detach();
667        cx.emit(DismissEvent);
668    }
669
670    fn show_git_error_toast(
671        _git_ref: &str,
672        error: anyhow::Error,
673        workspace: &mut Workspace,
674        cx: &mut Context<Workspace>,
675    ) {
676        let toast = Toast::new(NotificationId::unique::<()>(), error.to_string());
677        workspace.show_toast(toast, cx);
678    }
679}
680
681impl EventEmitter<DismissEvent> for RefPickerModal {}
682impl ModalView for RefPickerModal {}
683impl Focusable for RefPickerModal {
684    fn focus_handle(&self, cx: &App) -> FocusHandle {
685        self.editor.focus_handle(cx)
686    }
687}
688
689impl Render for RefPickerModal {
690    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
691        let has_commit_details = self.commit_details.is_some();
692        let commit_preview = self.commit_details.as_ref().map(|details| {
693            let commit_time = OffsetDateTime::from_unix_timestamp(details.commit_timestamp)
694                .unwrap_or_else(|_| OffsetDateTime::now_utc());
695            let local_offset =
696                time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
697            let formatted_time = time_format::format_localized_timestamp(
698                commit_time,
699                OffsetDateTime::now_utc(),
700                local_offset,
701                time_format::TimestampFormat::Relative,
702            );
703
704            let subject = details.message.lines().next().unwrap_or("").to_string();
705            let author_and_subject = format!("{} • {}", details.author_name, subject);
706
707            h_flex()
708                .w_full()
709                .gap_6()
710                .justify_between()
711                .overflow_x_hidden()
712                .child(
713                    div().max_w_96().child(
714                        Label::new(author_and_subject)
715                            .size(LabelSize::Small)
716                            .truncate()
717                            .color(Color::Muted),
718                    ),
719                )
720                .child(
721                    Label::new(formatted_time)
722                        .size(LabelSize::Small)
723                        .color(Color::Muted),
724                )
725        });
726
727        v_flex()
728            .key_context("RefPickerModal")
729            .on_action(cx.listener(Self::cancel))
730            .on_action(cx.listener(Self::confirm))
731            .elevation_2(cx)
732            .w(rems(34.))
733            .child(
734                h_flex()
735                    .px_3()
736                    .pt_2()
737                    .pb_1()
738                    .w_full()
739                    .gap_1p5()
740                    .child(Icon::new(IconName::Hash).size(IconSize::XSmall))
741                    .child(Headline::new("View Commit").size(HeadlineSize::XSmall)),
742            )
743            .child(div().px_3().w_full().child(self.editor.clone()))
744            .when_some(commit_preview, |el, preview| {
745                el.child(div().px_3().pb_3().w_full().child(preview))
746            })
747            .when(!has_commit_details, |el| el.child(div().pb_3()))
748    }
749}
750
751fn show_ref_picker(
752    workspace: &mut Workspace,
753    _: &git::ViewCommit,
754    window: &mut Window,
755    cx: &mut Context<Workspace>,
756) {
757    let Some(repo) = workspace.project().read(cx).active_repository(cx) else {
758        return;
759    };
760
761    let workspace_entity = cx.entity();
762    workspace.toggle_modal(window, cx, |window, cx| {
763        RefPickerModal::new(repo, workspace_entity, window, cx)
764    });
765}
766
767fn render_remote_button(
768    id: impl Into<SharedString>,
769    branch: &Branch,
770    keybinding_target: Option<FocusHandle>,
771    show_fetch_button: bool,
772    in_progress_operation: Option<RemoteOperationKind>,
773    menu_handle: PopoverMenuHandle<ContextMenu>,
774) -> Option<impl IntoElement> {
775    let id = id.into();
776    let upstream = branch.upstream.as_ref();
777    match upstream {
778        Some(Upstream {
779            tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
780            ..
781        }) => match (*ahead, *behind) {
782            (0, 0) if show_fetch_button => Some(remote_button::render_fetch_button(
783                keybinding_target,
784                id,
785                in_progress_operation,
786                menu_handle,
787            )),
788            (0, 0) => None,
789            (ahead, 0) => Some(remote_button::render_push_button(
790                keybinding_target,
791                id,
792                ahead,
793                in_progress_operation,
794                menu_handle,
795            )),
796            (ahead, behind) => Some(remote_button::render_pull_button(
797                keybinding_target,
798                id,
799                ahead,
800                behind,
801                in_progress_operation,
802                menu_handle,
803            )),
804        },
805        Some(Upstream {
806            tracking: UpstreamTracking::Gone,
807            ..
808        }) => Some(remote_button::render_republish_button(
809            keybinding_target,
810            id,
811            in_progress_operation,
812            menu_handle,
813        )),
814        None => Some(remote_button::render_publish_button(
815            keybinding_target,
816            id,
817            in_progress_operation,
818            menu_handle,
819        )),
820    }
821}
822
823mod remote_button {
824    use crate::git_panel::RemoteOperationKind;
825    use gpui::{Action, Anchor, AnyView, ClickEvent, FocusHandle};
826    use ui::{
827        ButtonLike, CommonAnimationExt, ContextMenu, ElevationIndex, PopoverMenu,
828        PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
829    };
830
831    pub fn render_fetch_button(
832        keybinding_target: Option<FocusHandle>,
833        id: SharedString,
834        in_progress_operation: Option<RemoteOperationKind>,
835        menu_handle: PopoverMenuHandle<ContextMenu>,
836    ) -> SplitButton {
837        split_button(
838            id,
839            "Fetch",
840            0,
841            0,
842            Some(IconName::ArrowCircle),
843            keybinding_target.clone(),
844            in_progress_operation,
845            menu_handle,
846            move |_, window, cx| {
847                window.dispatch_action(Box::new(git::Fetch), cx);
848            },
849            move |_window, cx| {
850                git_action_tooltip(
851                    "Fetch updates from remote",
852                    &git::Fetch,
853                    "git fetch",
854                    keybinding_target.clone(),
855                    cx,
856                )
857            },
858        )
859    }
860
861    pub fn render_push_button(
862        keybinding_target: Option<FocusHandle>,
863        id: SharedString,
864        ahead: u32,
865        in_progress_operation: Option<RemoteOperationKind>,
866        menu_handle: PopoverMenuHandle<ContextMenu>,
867    ) -> SplitButton {
868        split_button(
869            id,
870            "Push",
871            ahead as usize,
872            0,
873            None,
874            keybinding_target.clone(),
875            in_progress_operation,
876            menu_handle,
877            move |_, window, cx| {
878                window.dispatch_action(Box::new(git::Push), cx);
879            },
880            move |_window, cx| {
881                git_action_tooltip(
882                    "Push committed changes to remote",
883                    &git::Push,
884                    "git push",
885                    keybinding_target.clone(),
886                    cx,
887                )
888            },
889        )
890    }
891
892    pub fn render_pull_button(
893        keybinding_target: Option<FocusHandle>,
894        id: SharedString,
895        ahead: u32,
896        behind: u32,
897        in_progress_operation: Option<RemoteOperationKind>,
898        menu_handle: PopoverMenuHandle<ContextMenu>,
899    ) -> SplitButton {
900        split_button(
901            id,
902            "Pull",
903            ahead as usize,
904            behind as usize,
905            None,
906            keybinding_target.clone(),
907            in_progress_operation,
908            menu_handle,
909            move |_, window, cx| {
910                window.dispatch_action(Box::new(git::Pull), cx);
911            },
912            move |_window, cx| {
913                git_action_tooltip(
914                    "Pull",
915                    &git::Pull,
916                    "git pull",
917                    keybinding_target.clone(),
918                    cx,
919                )
920            },
921        )
922    }
923
924    pub fn render_publish_button(
925        keybinding_target: Option<FocusHandle>,
926        id: SharedString,
927        in_progress_operation: Option<RemoteOperationKind>,
928        menu_handle: PopoverMenuHandle<ContextMenu>,
929    ) -> SplitButton {
930        split_button(
931            id,
932            "Publish",
933            0,
934            0,
935            Some(IconName::ExpandUp),
936            keybinding_target.clone(),
937            in_progress_operation,
938            menu_handle,
939            move |_, window, cx| {
940                window.dispatch_action(Box::new(git::Push), cx);
941            },
942            move |_window, cx| {
943                git_action_tooltip(
944                    "Publish branch to remote",
945                    &git::Push,
946                    "git push --set-upstream",
947                    keybinding_target.clone(),
948                    cx,
949                )
950            },
951        )
952    }
953
954    pub fn render_republish_button(
955        keybinding_target: Option<FocusHandle>,
956        id: SharedString,
957        in_progress_operation: Option<RemoteOperationKind>,
958        menu_handle: PopoverMenuHandle<ContextMenu>,
959    ) -> SplitButton {
960        split_button(
961            id,
962            "Republish",
963            0,
964            0,
965            Some(IconName::ExpandUp),
966            keybinding_target.clone(),
967            in_progress_operation,
968            menu_handle,
969            move |_, window, cx| {
970                window.dispatch_action(Box::new(git::Push), cx);
971            },
972            move |_window, cx| {
973                git_action_tooltip(
974                    "Re-publish branch to remote",
975                    &git::Push,
976                    "git push --set-upstream",
977                    keybinding_target.clone(),
978                    cx,
979                )
980            },
981        )
982    }
983
984    fn in_progress_tooltip(operation: RemoteOperationKind) -> &'static str {
985        match operation {
986            RemoteOperationKind::Fetch => "Fetch in Progress…",
987            RemoteOperationKind::Pull => "Pull in Progress…",
988            RemoteOperationKind::Push => "Push in Progress…",
989        }
990    }
991
992    fn git_action_tooltip(
993        label: impl Into<SharedString>,
994        action: &dyn Action,
995        command: impl Into<SharedString>,
996        focus_handle: Option<FocusHandle>,
997        cx: &mut App,
998    ) -> AnyView {
999        let label = label.into();
1000        let command = command.into();
1001
1002        if let Some(handle) = focus_handle {
1003            Tooltip::with_meta_in(label, Some(action), command, &handle, cx)
1004        } else {
1005            Tooltip::with_meta(label, Some(action), command, cx)
1006        }
1007    }
1008
1009    fn render_git_action_menu(
1010        id: impl Into<ElementId>,
1011        keybinding_target: Option<FocusHandle>,
1012        menu_handle: PopoverMenuHandle<ContextMenu>,
1013    ) -> impl IntoElement {
1014        let menu_open = menu_handle.is_deployed();
1015
1016        PopoverMenu::new(id.into())
1017            .trigger(crate::render_split_button_chevron_trigger(
1018                "split-button-right",
1019                menu_open,
1020            ))
1021            .with_handle(menu_handle)
1022            .menu(move |window, cx| {
1023                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
1024                    context_menu
1025                        .when_some(keybinding_target.clone(), |el, keybinding_target| {
1026                            el.context(keybinding_target)
1027                        })
1028                        .action("Fetch", git::Fetch.boxed_clone())
1029                        .action("Fetch From", git::FetchFrom.boxed_clone())
1030                        .action("Pull", git::Pull.boxed_clone())
1031                        .action("Pull (Rebase)", git::PullRebase.boxed_clone())
1032                        .separator()
1033                        .action("Push", git::Push.boxed_clone())
1034                        .action("Push To", git::PushTo.boxed_clone())
1035                        .action("Force Push", git::ForcePush.boxed_clone())
1036                }))
1037            })
1038            .anchor(Anchor::TopRight)
1039            .offset(gpui::Point {
1040                x: px(0.),
1041                y: px(2.),
1042            })
1043    }
1044
1045    #[allow(clippy::too_many_arguments)]
1046    fn split_button(
1047        id: SharedString,
1048        left_label: impl Into<SharedString>,
1049        ahead_count: usize,
1050        behind_count: usize,
1051        left_icon: Option<IconName>,
1052        keybinding_target: Option<FocusHandle>,
1053        in_progress_operation: Option<RemoteOperationKind>,
1054        menu_handle: PopoverMenuHandle<ContextMenu>,
1055        left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
1056        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
1057    ) -> SplitButton {
1058        fn count(count: usize) -> impl IntoElement {
1059            h_flex()
1060                .ml_neg_px()
1061                .h(rems(0.875))
1062                .overflow_hidden()
1063                .px_0p5()
1064                .child(
1065                    Label::new(count.to_string())
1066                        .size(LabelSize::XSmall)
1067                        .line_height_style(LineHeightStyle::UiLabel),
1068                )
1069        }
1070
1071        let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
1072        let is_in_progress = in_progress_operation.is_some();
1073
1074        let left = ButtonLike::new_rounded_left(format!("split-button-left-{}", id))
1075            .layer(ElevationIndex::ModalSurface)
1076            .size(ButtonSize::Compact)
1077            .disabled(is_in_progress)
1078            .when(should_render_counts, |this| {
1079                this.child(
1080                    h_flex()
1081                        .ml_neg_0p5()
1082                        .when(behind_count > 0, |this| {
1083                            this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
1084                                .child(count(behind_count))
1085                        })
1086                        .when(ahead_count > 0, |this| {
1087                            this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
1088                                .child(count(ahead_count))
1089                        }),
1090                )
1091            })
1092            .when_some(left_icon, |this, left_icon| {
1093                this.map(|this| {
1094                    if is_in_progress {
1095                        this.child(
1096                            Icon::new(IconName::LoadCircle)
1097                                .size(IconSize::XSmall)
1098                                .color(Color::Disabled)
1099                                .with_rotate_animation(2),
1100                        )
1101                    } else {
1102                        this.child(Icon::new(left_icon).size(IconSize::XSmall))
1103                    }
1104                })
1105            })
1106            .child(
1107                Label::new(left_label)
1108                    .size(LabelSize::Small)
1109                    .when(is_in_progress, |this| this.color(Color::Disabled))
1110                    .mr_0p5(),
1111            )
1112            .on_click(left_on_click)
1113            .tooltip(move |window, cx| {
1114                if let Some(operation) = in_progress_operation {
1115                    Tooltip::simple(in_progress_tooltip(operation), cx)
1116                } else {
1117                    tooltip(window, cx)
1118                }
1119            });
1120
1121        let right = render_git_action_menu(
1122            format!("split-button-right-{}", id),
1123            keybinding_target,
1124            menu_handle,
1125        )
1126        .into_any_element();
1127
1128        SplitButton::new(left, right)
1129    }
1130}
1131
1132pub(crate) fn render_split_button_chevron_trigger(
1133    id: impl Into<ElementId>,
1134    menu_open: bool,
1135) -> ButtonLike {
1136    let chevron_button_size = rems_from_px(20.);
1137    let chevron_icon = if menu_open {
1138        IconName::ChevronUp
1139    } else {
1140        IconName::ChevronDown
1141    };
1142
1143    ButtonLike::new_rounded_right(id)
1144        .layer(ElevationIndex::ModalSurface)
1145        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1146        .width(chevron_button_size)
1147        .height(chevron_button_size.into())
1148        .child(Icon::new(chevron_icon).size(IconSize::XSmall))
1149}
1150
1151/// A visual representation of a file's Git status.
1152#[derive(IntoElement, RegisterComponent)]
1153pub struct GitStatusIcon {
1154    status: FileStatus,
1155}
1156
1157impl GitStatusIcon {
1158    pub fn new(status: FileStatus) -> Self {
1159        Self { status }
1160    }
1161}
1162
1163impl RenderOnce for GitStatusIcon {
1164    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
1165        let status = self.status;
1166
1167        let (icon_name, color) = if status.is_conflicted() {
1168            (
1169                IconName::Warning,
1170                cx.theme().colors().version_control_conflict,
1171            )
1172        } else if status.is_deleted() {
1173            (
1174                IconName::SquareMinus,
1175                cx.theme().colors().version_control_deleted,
1176            )
1177        } else if status.is_modified() {
1178            (
1179                IconName::SquareDot,
1180                cx.theme().colors().version_control_modified,
1181            )
1182        } else {
1183            (
1184                IconName::SquarePlus,
1185                cx.theme().colors().version_control_added,
1186            )
1187        };
1188
1189        Icon::new(icon_name).color(Color::Custom(color))
1190    }
1191}
1192
1193// View this component preview using `workspace: open component-preview`
1194impl Component for GitStatusIcon {
1195    fn scope() -> ComponentScope {
1196        ComponentScope::VersionControl
1197    }
1198
1199    fn description() -> &'static str {
1200        "An icon that visually represents the git status of a file, \
1201        using a distinct glyph and color for modified, added, deleted, and conflicted states."
1202    }
1203
1204    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
1205        fn tracked_file_status(code: StatusCode) -> FileStatus {
1206            FileStatus::Tracked(git::status::TrackedStatus {
1207                index_status: code,
1208                worktree_status: code,
1209            })
1210        }
1211
1212        let modified = tracked_file_status(StatusCode::Modified);
1213        let added = tracked_file_status(StatusCode::Added);
1214        let deleted = tracked_file_status(StatusCode::Deleted);
1215        let conflict = UnmergedStatus {
1216            first_head: UnmergedStatusCode::Updated,
1217            second_head: UnmergedStatusCode::Updated,
1218        }
1219        .into();
1220
1221        v_flex()
1222            .gap_6()
1223            .children(vec![example_group(vec![
1224                single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
1225                single_example("Added", GitStatusIcon::new(added).into_any_element()),
1226                single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
1227                single_example(
1228                    "Conflicted",
1229                    GitStatusIcon::new(conflict).into_any_element(),
1230                ),
1231            ])])
1232            .into_any_element()
1233    }
1234}
1235
1236struct GitCloneModal {
1237    panel: Entity<GitPanel>,
1238    repo_input: Entity<Editor>,
1239    focus_handle: FocusHandle,
1240}
1241
1242impl GitCloneModal {
1243    pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1244        let repo_input = cx.new(|cx| {
1245            let mut editor = Editor::single_line(window, cx);
1246            editor.set_placeholder_text("Enter repository URL…", window, cx);
1247            editor
1248        });
1249        let focus_handle = repo_input.focus_handle(cx);
1250
1251        window.focus(&focus_handle, cx);
1252
1253        Self {
1254            panel,
1255            repo_input,
1256            focus_handle,
1257        }
1258    }
1259}
1260
1261impl Focusable for GitCloneModal {
1262    fn focus_handle(&self, _: &App) -> FocusHandle {
1263        self.focus_handle.clone()
1264    }
1265}
1266
1267impl Render for GitCloneModal {
1268    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1269        div()
1270            .elevation_3(cx)
1271            .w(rems(34.))
1272            .flex_1()
1273            .overflow_hidden()
1274            .child(
1275                div()
1276                    .w_full()
1277                    .p_2()
1278                    .border_b_1()
1279                    .border_color(cx.theme().colors().border_variant)
1280                    .child(self.repo_input.clone()),
1281            )
1282            .child(
1283                h_flex()
1284                    .w_full()
1285                    .p_2()
1286                    .gap_0p5()
1287                    .rounded_b_sm()
1288                    .bg(cx.theme().colors().editor_background)
1289                    .child(
1290                        Label::new("Clone a repository from GitHub or other sources.")
1291                            .color(Color::Muted)
1292                            .size(LabelSize::Small),
1293                    )
1294                    .child(
1295                        Button::new("learn-more", "Learn More")
1296                            .label_size(LabelSize::Small)
1297                            .end_icon(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall))
1298                            .on_click(|_, _, cx| {
1299                                cx.open_url("https://github.com/git-guides/git-clone");
1300                            }),
1301                    ),
1302            )
1303            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
1304                cx.emit(DismissEvent);
1305            }))
1306            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1307                let repo = this.repo_input.read(cx).text(cx);
1308                this.panel.update(cx, |panel, cx| {
1309                    panel.git_clone(repo, window, cx);
1310                });
1311                cx.emit(DismissEvent);
1312            }))
1313    }
1314}
1315
1316impl EventEmitter<DismissEvent> for GitCloneModal {}
1317
1318impl ModalView for GitCloneModal {}
1319
1320#[cfg(test)]
1321mod view_commit_tests {
1322    use super::*;
1323    use gpui::{TestAppContext, VisualTestContext, WindowHandle};
1324    use language::language_settings::AllLanguageSettings;
1325    use project::project_settings::ProjectSettings;
1326    use project::{FakeFs, Project, WorktreeSettings};
1327    use serde_json::json;
1328    use settings::{Settings as _, SettingsStore};
1329    use std::path::Path;
1330    use std::sync::Arc;
1331    use theme::LoadThemes;
1332    use util::path;
1333    use workspace::WorkspaceSettings;
1334
1335    fn init_test(cx: &mut TestAppContext) {
1336        zlog::init_test();
1337        cx.update(|cx| {
1338            let settings_store = SettingsStore::test(cx);
1339            cx.set_global(settings_store);
1340            theme_settings::init(LoadThemes::JustBase, cx);
1341            AllLanguageSettings::register(cx);
1342            editor::init(cx);
1343            ProjectSettings::register(cx);
1344            WorktreeSettings::register(cx);
1345            WorkspaceSettings::register(cx);
1346        });
1347    }
1348
1349    async fn setup_git_repo(cx: &mut TestAppContext) -> Arc<FakeFs> {
1350        let fs = FakeFs::new(cx.background_executor.clone());
1351        fs.insert_tree(
1352            "/root",
1353            json!({
1354                "project": {
1355                    ".git": {},
1356                    "src": {
1357                        "main.rs": "fn main() {}"
1358                    }
1359                }
1360            }),
1361        )
1362        .await;
1363        fs
1364    }
1365
1366    async fn create_test_workspace(
1367        fs: Arc<FakeFs>,
1368        cx: &mut TestAppContext,
1369    ) -> (Entity<Project>, WindowHandle<Workspace>) {
1370        let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
1371        let workspace =
1372            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1373        cx.read(|cx| {
1374            project
1375                .read(cx)
1376                .worktrees(cx)
1377                .next()
1378                .unwrap()
1379                .read(cx)
1380                .as_local()
1381                .unwrap()
1382                .scan_complete()
1383        })
1384        .await;
1385        (project, workspace)
1386    }
1387
1388    #[gpui::test]
1389    async fn test_show_ref_picker_with_repository(cx: &mut TestAppContext) {
1390        init_test(cx);
1391        let fs = setup_git_repo(cx).await;
1392
1393        fs.set_status_for_repo(
1394            Path::new("/root/project/.git"),
1395            &[("src/main.rs", git::status::StatusCode::Modified.worktree())],
1396        );
1397
1398        let (_project, workspace) = create_test_workspace(fs, cx).await;
1399        let cx = &mut VisualTestContext::from_window(*workspace, cx);
1400
1401        let initial_modal_state = workspace
1402            .read_with(cx, |workspace, cx| {
1403                workspace.active_modal::<RefPickerModal>(cx).is_some()
1404            })
1405            .unwrap_or(false);
1406
1407        let _ = workspace.update(cx, |workspace, window, cx| {
1408            show_ref_picker(workspace, &git::ViewCommit, window, cx);
1409        });
1410
1411        let final_modal_state = workspace
1412            .read_with(cx, |workspace, cx| {
1413                workspace.active_modal::<RefPickerModal>(cx).is_some()
1414            })
1415            .unwrap_or(false);
1416
1417        assert!(!initial_modal_state);
1418        assert!(final_modal_state);
1419    }
1420}
1421
Served at tenant.openagents/omega Member data and write actions are omitted.