git_ui.rs

  1use anyhow::anyhow;
  2use commit_modal::CommitModal;
  3use editor::{Editor, actions::DiffClipboardWithSelectionData};
  4
  5use project::ProjectPath;
  6use ui::{
  7    Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled,
  8    StyledExt, div, h_flex, rems, v_flex,
  9};
 10
 11mod blame_ui;
 12pub mod clone;
 13
 14use git::{
 15    repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
 16    status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
 17};
 18use gpui::{
 19    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString, Window,
 20};
 21use menu::{Cancel, Confirm};
 22use project::git_store::Repository;
 23use project_diff::ProjectDiff;
 24use ui::prelude::*;
 25use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
 26use zed_actions;
 27
 28use crate::{git_panel::GitPanel, text_diff_view::TextDiffView};
 29
 30mod askpass_modal;
 31pub mod branch_picker;
 32mod commit_modal;
 33pub mod commit_tooltip;
 34pub mod commit_view;
 35mod conflict_view;
 36pub mod file_diff_view;
 37pub mod file_history_view;
 38pub mod git_panel;
 39mod git_panel_settings;
 40pub mod git_picker;
 41pub mod multi_diff_view;
 42pub mod picker_prompt;
 43pub mod project_diff;
 44pub(crate) mod remote_output;
 45pub mod repository_selector;
 46pub mod stash_picker;
 47pub mod text_diff_view;
 48pub mod worktree_picker;
 49
 50const BRANCH_DIFF_TIP_MESSAGE: &str = "
 51Want to review your changes but you've already made some commits? Use the Branch \
 52Diff to see the changes compared to the default branch.
 53";
 54
 55pub fn init(cx: &mut App) {
 56    editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
 57    commit_view::init(cx);
 58
 59    workspace::welcome::register_tip(
 60        workspace::welcome::Tip {
 61            title: "Review Your Branch Diff".into(),
 62            message: BRANCH_DIFF_TIP_MESSAGE.into(),
 63            icon: Some(ui::IconName::GitBranch),
 64            mentioned_actions: vec![Box::new(project_diff::BranchDiff)],
 65        },
 66        cx,
 67    );
 68
 69    cx.observe_new(|editor: &mut Editor, _, cx| {
 70        conflict_view::register_editor(editor, editor.buffer().clone(), cx);
 71    })
 72    .detach();
 73
 74    cx.observe_new(|workspace: &mut Workspace, _, cx| {
 75        ProjectDiff::register(workspace, cx);
 76        CommitModal::register(workspace);
 77        git_panel::register(workspace);
 78        repository_selector::register(workspace);
 79        git_picker::register(workspace);
 80        conflict_view::register_conflict_notification(workspace, cx);
 81
 82        let project = workspace.project().read(cx);
 83        if project.is_read_only(cx) {
 84            return;
 85        }
 86        if !project.is_via_collab() {
 87            workspace.register_action(
 88                |workspace, _: &zed_actions::git::CreatePullRequest, window, cx| {
 89                    if let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) {
 90                        panel.update(cx, |panel, cx| {
 91                            panel.create_pull_request(window, cx);
 92                        });
 93                    }
 94                },
 95            );
 96            workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
 97                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
 98                    return;
 99                };
100                panel.update(cx, |panel, cx| {
101                    panel.fetch(true, window, cx);
102                });
103            });
104            workspace.register_action(|workspace, _: &git::FetchFrom, window, cx| {
105                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
106                    return;
107                };
108                panel.update(cx, |panel, cx| {
109                    panel.fetch(false, window, cx);
110                });
111            });
112            workspace.register_action(|workspace, _: &git::Push, window, cx| {
113                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
114                    return;
115                };
116                panel.update(cx, |panel, cx| {
117                    panel.push(false, false, window, cx);
118                });
119            });
120            workspace.register_action(|workspace, _: &git::PushTo, window, cx| {
121                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
122                    return;
123                };
124                panel.update(cx, |panel, cx| {
125                    panel.push(false, true, window, cx);
126                });
127            });
128            workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
129                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
130                    return;
131                };
132                panel.update(cx, |panel, cx| {
133                    panel.push(true, false, window, cx);
134                });
135            });
136            workspace.register_action(|workspace, _: &git::Pull, window, cx| {
137                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
138                    return;
139                };
140                panel.update(cx, |panel, cx| {
141                    panel.pull(false, window, cx);
142                });
143            });
144            workspace.register_action(|workspace, _: &git::PullRebase, window, cx| {
145                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
146                    return;
147                };
148                panel.update(cx, |panel, cx| {
149                    panel.pull(true, window, cx);
150                });
151            });
152        }
153        workspace.register_action(|workspace, action: &git::StashAll, window, cx| {
154            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
155                return;
156            };
157            panel.update(cx, |panel, cx| {
158                panel.stash_all(action, window, cx);
159            });
160        });
161        workspace.register_action(|workspace, action: &git::StashPop, window, cx| {
162            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
163                return;
164            };
165            panel.update(cx, |panel, cx| {
166                panel.stash_pop(action, window, cx);
167            });
168        });
169        workspace.register_action(|workspace, action: &git::StashApply, window, cx| {
170            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
171                return;
172            };
173            panel.update(cx, |panel, cx| {
174                panel.stash_apply(action, window, cx);
175            });
176        });
177        workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
178            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
179                return;
180            };
181            panel.update(cx, |panel, cx| {
182                panel.stage_all(action, window, cx);
183            });
184        });
185        workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
186            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
187                return;
188            };
189            panel.update(cx, |panel, cx| {
190                panel.unstage_all(action, window, cx);
191            });
192        });
193        workspace.register_action(|workspace, _: &git::Uncommit, window, cx| {
194            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
195                return;
196            };
197            panel.update(cx, |panel, cx| {
198                panel.uncommit(window, cx);
199            })
200        });
201        workspace.register_action(|workspace, _action: &git::Init, window, cx| {
202            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
203                return;
204            };
205            panel.update(cx, |panel, cx| {
206                panel.git_init(window, cx);
207            });
208        });
209        workspace.register_action(|workspace, _action: &git::Clone, window, cx| {
210            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
211                return;
212            };
213
214            workspace.toggle_modal(window, cx, |window, cx| {
215                GitCloneModal::show(panel, window, cx)
216            });
217        });
218        workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
219            open_modified_files(workspace, window, cx);
220        });
221        workspace.register_action(|workspace, _: &git::RenameBranch, window, cx| {
222            rename_current_branch(workspace, window, cx);
223        });
224        workspace.register_action(
225            |workspace, action: &DiffClipboardWithSelectionData, window, cx| {
226                if let Some(task) = TextDiffView::open(action, workspace, window, cx) {
227                    task.detach();
228                };
229            },
230        );
231        workspace.register_action(|workspace, _: &git::FileHistory, window, cx| {
232            let Some(active_item) = workspace.active_item(cx) else {
233                return;
234            };
235            let Some(editor) = active_item.downcast::<Editor>() else {
236                return;
237            };
238            let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
239                return;
240            };
241            let Some(file) = buffer.read(cx).file() else {
242                return;
243            };
244            let worktree_id = file.worktree_id(cx);
245            let project_path = ProjectPath {
246                worktree_id,
247                path: file.path().clone(),
248            };
249            let project = workspace.project();
250            let git_store = project.read(cx).git_store();
251            let Some((repo, repo_path)) = git_store
252                .read(cx)
253                .repository_and_path_for_project_path(&project_path, cx)
254            else {
255                return;
256            };
257            file_history_view::FileHistoryView::open(
258                repo_path,
259                git_store.downgrade(),
260                repo.downgrade(),
261                workspace.weak_handle(),
262                window,
263                cx,
264            );
265        });
266    })
267    .detach();
268}
269
270fn open_modified_files(
271    workspace: &mut Workspace,
272    window: &mut Window,
273    cx: &mut Context<Workspace>,
274) {
275    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
276        return;
277    };
278    let modified_paths: Vec<_> = panel.update(cx, |panel, cx| {
279        let Some(repo) = panel.active_repository.as_ref() else {
280            return Vec::new();
281        };
282        let repo = repo.read(cx);
283        repo.cached_status()
284            .filter_map(|entry| {
285                if entry.status.is_modified() {
286                    repo.repo_path_to_project_path(&entry.repo_path, cx)
287                } else {
288                    None
289                }
290            })
291            .collect()
292    });
293    for path in modified_paths {
294        workspace.open_path(path, None, true, window, cx).detach();
295    }
296}
297
298/// Resolves the repository for git operations, respecting the workspace's
299/// active worktree override from the project dropdown.
300pub fn resolve_active_repository(workspace: &Workspace, cx: &App) -> Option<Entity<Repository>> {
301    let project = workspace.project().read(cx);
302    workspace
303        .active_worktree_override()
304        .and_then(|override_id| {
305            project
306                .worktree_for_id(override_id, cx)
307                .and_then(|worktree| {
308                    let worktree_abs_path = worktree.read(cx).abs_path();
309                    let git_store = project.git_store().read(cx);
310                    git_store
311                        .repositories()
312                        .values()
313                        .find(|repo| {
314                            let repo_path = &repo.read(cx).work_directory_abs_path;
315                            *repo_path == worktree_abs_path
316                                || worktree_abs_path.starts_with(repo_path.as_ref())
317                        })
318                        .cloned()
319                })
320        })
321        .or_else(|| project.active_repository(cx))
322}
323
324pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
325    GitStatusIcon::new(status)
326}
327
328struct RenameBranchModal {
329    current_branch: SharedString,
330    editor: Entity<Editor>,
331    repo: Entity<Repository>,
332}
333
334impl RenameBranchModal {
335    fn new(
336        current_branch: String,
337        repo: Entity<Repository>,
338        window: &mut Window,
339        cx: &mut Context<Self>,
340    ) -> Self {
341        let editor = cx.new(|cx| {
342            let mut editor = Editor::single_line(window, cx);
343            editor.set_text(current_branch.clone(), window, cx);
344            editor
345        });
346        Self {
347            current_branch: current_branch.into(),
348            editor,
349            repo,
350        }
351    }
352
353    fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
354        cx.emit(DismissEvent);
355    }
356
357    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
358        let new_name = self.editor.read(cx).text(cx);
359        if new_name.is_empty() || new_name == self.current_branch.as_ref() {
360            cx.emit(DismissEvent);
361            return;
362        }
363
364        let repo = self.repo.clone();
365        let current_branch = self.current_branch.to_string();
366        cx.spawn(async move |_, cx| {
367            match repo
368                .update(cx, |repo, _| {
369                    repo.rename_branch(current_branch, new_name.clone())
370                })
371                .await
372            {
373                Ok(Ok(_)) => Ok(()),
374                Ok(Err(error)) => Err(error),
375                Err(_) => Err(anyhow!("Operation was canceled")),
376            }
377        })
378        .detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None);
379        cx.emit(DismissEvent);
380    }
381}
382
383impl EventEmitter<DismissEvent> for RenameBranchModal {}
384impl ModalView for RenameBranchModal {}
385impl Focusable for RenameBranchModal {
386    fn focus_handle(&self, cx: &App) -> FocusHandle {
387        self.editor.focus_handle(cx)
388    }
389}
390
391impl Render for RenameBranchModal {
392    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
393        v_flex()
394            .key_context("RenameBranchModal")
395            .on_action(cx.listener(Self::cancel))
396            .on_action(cx.listener(Self::confirm))
397            .elevation_2(cx)
398            .w(rems(34.))
399            .child(
400                h_flex()
401                    .px_3()
402                    .pt_2()
403                    .pb_1()
404                    .w_full()
405                    .gap_1p5()
406                    .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
407                    .child(
408                        Headline::new(format!("Rename Branch ({})", self.current_branch))
409                            .size(HeadlineSize::XSmall),
410                    ),
411            )
412            .child(div().px_3().pb_3().w_full().child(self.editor.clone()))
413    }
414}
415
416fn rename_current_branch(
417    workspace: &mut Workspace,
418    window: &mut Window,
419    cx: &mut Context<Workspace>,
420) {
421    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
422        return;
423    };
424    let current_branch: Option<String> = panel.update(cx, |panel, cx| {
425        let repo = panel.active_repository.as_ref()?;
426        let repo = repo.read(cx);
427        repo.branch.as_ref().map(|branch| branch.name().to_string())
428    });
429
430    let Some(current_branch_name) = current_branch else {
431        return;
432    };
433
434    let repo = panel.read(cx).active_repository.clone();
435    let Some(repo) = repo else {
436        return;
437    };
438
439    workspace.toggle_modal(window, cx, |window, cx| {
440        RenameBranchModal::new(current_branch_name, repo, window, cx)
441    });
442}
443
444fn render_remote_button(
445    id: impl Into<SharedString>,
446    branch: &Branch,
447    keybinding_target: Option<FocusHandle>,
448    show_fetch_button: bool,
449) -> Option<impl IntoElement> {
450    let id = id.into();
451    let upstream = branch.upstream.as_ref();
452    match upstream {
453        Some(Upstream {
454            tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
455            ..
456        }) => match (*ahead, *behind) {
457            (0, 0) if show_fetch_button => {
458                Some(remote_button::render_fetch_button(keybinding_target, id))
459            }
460            (0, 0) => None,
461            (ahead, 0) => Some(remote_button::render_push_button(
462                keybinding_target,
463                id,
464                ahead,
465            )),
466            (ahead, behind) => Some(remote_button::render_pull_button(
467                keybinding_target,
468                id,
469                ahead,
470                behind,
471            )),
472        },
473        Some(Upstream {
474            tracking: UpstreamTracking::Gone,
475            ..
476        }) => Some(remote_button::render_republish_button(
477            keybinding_target,
478            id,
479        )),
480        None => Some(remote_button::render_publish_button(keybinding_target, id)),
481    }
482}
483
484mod remote_button {
485    use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle};
486    use ui::{
487        App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder, Icon, IconName,
488        IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement,
489        PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window, div, h_flex, rems,
490    };
491
492    pub fn render_fetch_button(
493        keybinding_target: Option<FocusHandle>,
494        id: SharedString,
495    ) -> SplitButton {
496        split_button(
497            id,
498            "Fetch",
499            0,
500            0,
501            Some(IconName::ArrowCircle),
502            keybinding_target.clone(),
503            move |_, window, cx| {
504                window.dispatch_action(Box::new(git::Fetch), cx);
505            },
506            move |_window, cx| {
507                git_action_tooltip(
508                    "Fetch updates from remote",
509                    &git::Fetch,
510                    "git fetch",
511                    keybinding_target.clone(),
512                    cx,
513                )
514            },
515        )
516    }
517
518    pub fn render_push_button(
519        keybinding_target: Option<FocusHandle>,
520        id: SharedString,
521        ahead: u32,
522    ) -> SplitButton {
523        split_button(
524            id,
525            "Push",
526            ahead as usize,
527            0,
528            None,
529            keybinding_target.clone(),
530            move |_, window, cx| {
531                window.dispatch_action(Box::new(git::Push), cx);
532            },
533            move |_window, cx| {
534                git_action_tooltip(
535                    "Push committed changes to remote",
536                    &git::Push,
537                    "git push",
538                    keybinding_target.clone(),
539                    cx,
540                )
541            },
542        )
543    }
544
545    pub fn render_pull_button(
546        keybinding_target: Option<FocusHandle>,
547        id: SharedString,
548        ahead: u32,
549        behind: u32,
550    ) -> SplitButton {
551        split_button(
552            id,
553            "Pull",
554            ahead as usize,
555            behind as usize,
556            None,
557            keybinding_target.clone(),
558            move |_, window, cx| {
559                window.dispatch_action(Box::new(git::Pull), cx);
560            },
561            move |_window, cx| {
562                git_action_tooltip(
563                    "Pull",
564                    &git::Pull,
565                    "git pull",
566                    keybinding_target.clone(),
567                    cx,
568                )
569            },
570        )
571    }
572
573    pub fn render_publish_button(
574        keybinding_target: Option<FocusHandle>,
575        id: SharedString,
576    ) -> SplitButton {
577        split_button(
578            id,
579            "Publish",
580            0,
581            0,
582            Some(IconName::ExpandUp),
583            keybinding_target.clone(),
584            move |_, window, cx| {
585                window.dispatch_action(Box::new(git::Push), cx);
586            },
587            move |_window, cx| {
588                git_action_tooltip(
589                    "Publish branch to remote",
590                    &git::Push,
591                    "git push --set-upstream",
592                    keybinding_target.clone(),
593                    cx,
594                )
595            },
596        )
597    }
598
599    pub fn render_republish_button(
600        keybinding_target: Option<FocusHandle>,
601        id: SharedString,
602    ) -> SplitButton {
603        split_button(
604            id,
605            "Republish",
606            0,
607            0,
608            Some(IconName::ExpandUp),
609            keybinding_target.clone(),
610            move |_, window, cx| {
611                window.dispatch_action(Box::new(git::Push), cx);
612            },
613            move |_window, cx| {
614                git_action_tooltip(
615                    "Re-publish branch to remote",
616                    &git::Push,
617                    "git push --set-upstream",
618                    keybinding_target.clone(),
619                    cx,
620                )
621            },
622        )
623    }
624
625    fn git_action_tooltip(
626        label: impl Into<SharedString>,
627        action: &dyn Action,
628        command: impl Into<SharedString>,
629        focus_handle: Option<FocusHandle>,
630        cx: &mut App,
631    ) -> AnyView {
632        let label = label.into();
633        let command = command.into();
634
635        if let Some(handle) = focus_handle {
636            Tooltip::with_meta_in(label, Some(action), command, &handle, cx)
637        } else {
638            Tooltip::with_meta(label, Some(action), command, cx)
639        }
640    }
641
642    fn render_git_action_menu(
643        id: impl Into<ElementId>,
644        keybinding_target: Option<FocusHandle>,
645    ) -> impl IntoElement {
646        PopoverMenu::new(id.into())
647            .trigger(
648                ui::ButtonLike::new_rounded_right("split-button-right")
649                    .layer(ui::ElevationIndex::ModalSurface)
650                    .size(ui::ButtonSize::None)
651                    .child(
652                        div()
653                            .px_1()
654                            .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
655                    ),
656            )
657            .menu(move |window, cx| {
658                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
659                    context_menu
660                        .when_some(keybinding_target.clone(), |el, keybinding_target| {
661                            el.context(keybinding_target)
662                        })
663                        .action("Fetch", git::Fetch.boxed_clone())
664                        .action("Fetch From", git::FetchFrom.boxed_clone())
665                        .action("Pull", git::Pull.boxed_clone())
666                        .action("Pull (Rebase)", git::PullRebase.boxed_clone())
667                        .separator()
668                        .action("Push", git::Push.boxed_clone())
669                        .action("Push To", git::PushTo.boxed_clone())
670                        .action("Force Push", git::ForcePush.boxed_clone())
671                }))
672            })
673            .anchor(Corner::TopRight)
674    }
675
676    #[allow(clippy::too_many_arguments)]
677    fn split_button(
678        id: SharedString,
679        left_label: impl Into<SharedString>,
680        ahead_count: usize,
681        behind_count: usize,
682        left_icon: Option<IconName>,
683        keybinding_target: Option<FocusHandle>,
684        left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
685        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
686    ) -> SplitButton {
687        fn count(count: usize) -> impl IntoElement {
688            h_flex()
689                .ml_neg_px()
690                .h(rems(0.875))
691                .items_center()
692                .overflow_hidden()
693                .px_0p5()
694                .child(
695                    Label::new(count.to_string())
696                        .size(LabelSize::XSmall)
697                        .line_height_style(LineHeightStyle::UiLabel),
698                )
699        }
700
701        let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
702
703        let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
704            format!("split-button-left-{}", id).into(),
705        ))
706        .layer(ui::ElevationIndex::ModalSurface)
707        .size(ui::ButtonSize::Compact)
708        .when(should_render_counts, |this| {
709            this.child(
710                h_flex()
711                    .ml_neg_0p5()
712                    .when(behind_count > 0, |this| {
713                        this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
714                            .child(count(behind_count))
715                    })
716                    .when(ahead_count > 0, |this| {
717                        this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
718                            .child(count(ahead_count))
719                    }),
720            )
721        })
722        .when_some(left_icon, |this, left_icon| {
723            this.child(
724                h_flex()
725                    .ml_neg_0p5()
726                    .child(Icon::new(left_icon).size(IconSize::XSmall)),
727            )
728        })
729        .child(
730            div()
731                .child(Label::new(left_label).size(LabelSize::Small))
732                .mr_0p5(),
733        )
734        .on_click(left_on_click)
735        .tooltip(tooltip);
736
737        let right = render_git_action_menu(
738            ElementId::Name(format!("split-button-right-{}", id).into()),
739            keybinding_target,
740        )
741        .into_any_element();
742
743        SplitButton::new(left, right)
744    }
745}
746
747/// A visual representation of a file's Git status.
748#[derive(IntoElement, RegisterComponent)]
749pub struct GitStatusIcon {
750    status: FileStatus,
751}
752
753impl GitStatusIcon {
754    pub fn new(status: FileStatus) -> Self {
755        Self { status }
756    }
757}
758
759impl RenderOnce for GitStatusIcon {
760    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
761        let status = self.status;
762
763        let (icon_name, color) = if status.is_conflicted() {
764            (
765                IconName::Warning,
766                cx.theme().colors().version_control_conflict,
767            )
768        } else if status.is_deleted() {
769            (
770                IconName::SquareMinus,
771                cx.theme().colors().version_control_deleted,
772            )
773        } else if status.is_modified() {
774            (
775                IconName::SquareDot,
776                cx.theme().colors().version_control_modified,
777            )
778        } else {
779            (
780                IconName::SquarePlus,
781                cx.theme().colors().version_control_added,
782            )
783        };
784
785        Icon::new(icon_name).color(Color::Custom(color))
786    }
787}
788
789// View this component preview using `workspace: open component-preview`
790impl Component for GitStatusIcon {
791    fn scope() -> ComponentScope {
792        ComponentScope::VersionControl
793    }
794
795    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
796        fn tracked_file_status(code: StatusCode) -> FileStatus {
797            FileStatus::Tracked(git::status::TrackedStatus {
798                index_status: code,
799                worktree_status: code,
800            })
801        }
802
803        let modified = tracked_file_status(StatusCode::Modified);
804        let added = tracked_file_status(StatusCode::Added);
805        let deleted = tracked_file_status(StatusCode::Deleted);
806        let conflict = UnmergedStatus {
807            first_head: UnmergedStatusCode::Updated,
808            second_head: UnmergedStatusCode::Updated,
809        }
810        .into();
811
812        Some(
813            v_flex()
814                .gap_6()
815                .children(vec![example_group(vec![
816                    single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
817                    single_example("Added", GitStatusIcon::new(added).into_any_element()),
818                    single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
819                    single_example(
820                        "Conflicted",
821                        GitStatusIcon::new(conflict).into_any_element(),
822                    ),
823                ])])
824                .into_any_element(),
825        )
826    }
827}
828
829struct GitCloneModal {
830    panel: Entity<GitPanel>,
831    repo_input: Entity<Editor>,
832    focus_handle: FocusHandle,
833}
834
835impl GitCloneModal {
836    pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
837        let repo_input = cx.new(|cx| {
838            let mut editor = Editor::single_line(window, cx);
839            editor.set_placeholder_text("Enter repository URL…", window, cx);
840            editor
841        });
842        let focus_handle = repo_input.focus_handle(cx);
843
844        window.focus(&focus_handle, cx);
845
846        Self {
847            panel,
848            repo_input,
849            focus_handle,
850        }
851    }
852}
853
854impl Focusable for GitCloneModal {
855    fn focus_handle(&self, _: &App) -> FocusHandle {
856        self.focus_handle.clone()
857    }
858}
859
860impl Render for GitCloneModal {
861    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
862        div()
863            .elevation_3(cx)
864            .w(rems(34.))
865            .flex_1()
866            .overflow_hidden()
867            .child(
868                div()
869                    .w_full()
870                    .p_2()
871                    .border_b_1()
872                    .border_color(cx.theme().colors().border_variant)
873                    .child(self.repo_input.clone()),
874            )
875            .child(
876                h_flex()
877                    .w_full()
878                    .p_2()
879                    .gap_0p5()
880                    .rounded_b_sm()
881                    .bg(cx.theme().colors().editor_background)
882                    .child(
883                        Label::new("Clone a repository from GitHub or other sources.")
884                            .color(Color::Muted)
885                            .size(LabelSize::Small),
886                    )
887                    .child(
888                        Button::new("learn-more", "Learn More")
889                            .label_size(LabelSize::Small)
890                            .icon(IconName::ArrowUpRight)
891                            .icon_size(IconSize::XSmall)
892                            .on_click(|_, _, cx| {
893                                cx.open_url("https://github.com/git-guides/git-clone");
894                            }),
895                    ),
896            )
897            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
898                cx.emit(DismissEvent);
899            }))
900            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
901                let repo = this.repo_input.read(cx).text(cx);
902                this.panel.update(cx, |panel, cx| {
903                    panel.git_clone(repo, window, cx);
904                });
905                cx.emit(DismissEvent);
906            }))
907    }
908}
909
910impl EventEmitter<DismissEvent> for GitCloneModal {}
911
912impl ModalView for GitCloneModal {}