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