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