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