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