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