1use std::any::Any;
2
3use ::settings::Settings;
4use command_palette_hooks::CommandPaletteFilter;
5use commit_modal::CommitModal;
6use editor::Editor;
7mod blame_ui;
8use git::{
9 repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
10 status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
11};
12use git_panel_settings::GitPanelSettings;
13use gpui::{Action, App, FocusHandle, actions};
14use onboarding::GitOnboardingModal;
15use project_diff::ProjectDiff;
16use ui::prelude::*;
17use workspace::Workspace;
18
19mod askpass_modal;
20pub mod branch_picker;
21mod commit_modal;
22pub mod commit_tooltip;
23mod commit_view;
24mod conflict_view;
25pub mod git_panel;
26mod git_panel_settings;
27pub mod onboarding;
28pub mod picker_prompt;
29pub mod project_diff;
30pub(crate) mod remote_output;
31pub mod repository_selector;
32
33actions!(git, [ResetOnboarding]);
34
35pub fn init(cx: &mut App) {
36 GitPanelSettings::register(cx);
37
38 editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
39
40 cx.observe_new(|editor: &mut Editor, _, cx| {
41 conflict_view::register_editor(editor, editor.buffer().clone(), cx);
42 })
43 .detach();
44
45 cx.observe_new(|workspace: &mut Workspace, _, cx| {
46 ProjectDiff::register(workspace, cx);
47 CommitModal::register(workspace);
48 git_panel::register(workspace);
49 repository_selector::register(workspace);
50 branch_picker::register(workspace);
51
52 let project = workspace.project().read(cx);
53 if project.is_read_only(cx) {
54 return;
55 }
56 if !project.is_via_collab() {
57 workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
58 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
59 return;
60 };
61 panel.update(cx, |panel, cx| {
62 panel.fetch(true, window, cx);
63 });
64 });
65 workspace.register_action(|workspace, _: &git::FetchFrom, window, cx| {
66 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
67 return;
68 };
69 panel.update(cx, |panel, cx| {
70 panel.fetch(false, window, cx);
71 });
72 });
73 workspace.register_action(|workspace, _: &git::Push, window, cx| {
74 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
75 return;
76 };
77 panel.update(cx, |panel, cx| {
78 panel.push(false, false, window, cx);
79 });
80 });
81 workspace.register_action(|workspace, _: &git::PushTo, window, cx| {
82 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
83 return;
84 };
85 panel.update(cx, |panel, cx| {
86 panel.push(false, true, window, cx);
87 });
88 });
89 workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
90 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
91 return;
92 };
93 panel.update(cx, |panel, cx| {
94 panel.push(true, false, window, cx);
95 });
96 });
97 workspace.register_action(|workspace, _: &git::Pull, window, cx| {
98 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
99 return;
100 };
101 panel.update(cx, |panel, cx| {
102 panel.pull(window, cx);
103 });
104 });
105 }
106 workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
107 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
108 return;
109 };
110 panel.update(cx, |panel, cx| {
111 panel.stage_all(action, window, cx);
112 });
113 });
114 workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
115 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
116 return;
117 };
118 panel.update(cx, |panel, cx| {
119 panel.unstage_all(action, window, cx);
120 });
121 });
122 CommandPaletteFilter::update_global(cx, |filter, _cx| {
123 filter.hide_action_types(&[
124 zed_actions::OpenGitIntegrationOnboarding.type_id(),
125 // ResetOnboarding.type_id(),
126 ]);
127 });
128 workspace.register_action(
129 move |workspace, _: &zed_actions::OpenGitIntegrationOnboarding, window, cx| {
130 GitOnboardingModal::toggle(workspace, window, cx)
131 },
132 );
133 workspace.register_action(move |_, _: &ResetOnboarding, window, cx| {
134 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
135 window.refresh();
136 });
137 workspace.register_action(|workspace, _action: &git::Init, window, cx| {
138 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
139 return;
140 };
141 panel.update(cx, |panel, cx| {
142 panel.git_init(window, cx);
143 });
144 });
145 })
146 .detach();
147}
148
149pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
150 GitStatusIcon::new(status)
151}
152
153fn render_remote_button(
154 id: impl Into<SharedString>,
155 branch: &Branch,
156 keybinding_target: Option<FocusHandle>,
157 show_fetch_button: bool,
158) -> Option<impl IntoElement> {
159 let id = id.into();
160 let upstream = branch.upstream.as_ref();
161 match upstream {
162 Some(Upstream {
163 tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
164 ..
165 }) => match (*ahead, *behind) {
166 (0, 0) if show_fetch_button => {
167 Some(remote_button::render_fetch_button(keybinding_target, id))
168 }
169 (0, 0) => None,
170 (ahead, 0) => Some(remote_button::render_push_button(
171 keybinding_target.clone(),
172 id,
173 ahead,
174 )),
175 (ahead, behind) => Some(remote_button::render_pull_button(
176 keybinding_target.clone(),
177 id,
178 ahead,
179 behind,
180 )),
181 },
182 Some(Upstream {
183 tracking: UpstreamTracking::Gone,
184 ..
185 }) => Some(remote_button::render_republish_button(
186 keybinding_target,
187 id,
188 )),
189 None => Some(remote_button::render_publish_button(keybinding_target, id)),
190 }
191}
192
193mod remote_button {
194 use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle};
195 use ui::{
196 App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder, Icon, IconName,
197 IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement,
198 PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window, div, h_flex, rems,
199 };
200
201 pub fn render_fetch_button(
202 keybinding_target: Option<FocusHandle>,
203 id: SharedString,
204 ) -> SplitButton {
205 split_button(
206 id,
207 "Fetch",
208 0,
209 0,
210 Some(IconName::ArrowCircle),
211 keybinding_target.clone(),
212 move |_, window, cx| {
213 window.dispatch_action(Box::new(git::Fetch), cx);
214 },
215 move |window, cx| {
216 git_action_tooltip(
217 "Fetch updates from remote",
218 &git::Fetch,
219 "git fetch",
220 keybinding_target.clone(),
221 window,
222 cx,
223 )
224 },
225 )
226 }
227
228 pub fn render_push_button(
229 keybinding_target: Option<FocusHandle>,
230 id: SharedString,
231 ahead: u32,
232 ) -> SplitButton {
233 split_button(
234 id,
235 "Push",
236 ahead as usize,
237 0,
238 None,
239 keybinding_target.clone(),
240 move |_, window, cx| {
241 window.dispatch_action(Box::new(git::Push), cx);
242 },
243 move |window, cx| {
244 git_action_tooltip(
245 "Push committed changes to remote",
246 &git::Push,
247 "git push",
248 keybinding_target.clone(),
249 window,
250 cx,
251 )
252 },
253 )
254 }
255
256 pub fn render_pull_button(
257 keybinding_target: Option<FocusHandle>,
258 id: SharedString,
259 ahead: u32,
260 behind: u32,
261 ) -> SplitButton {
262 split_button(
263 id,
264 "Pull",
265 ahead as usize,
266 behind as usize,
267 None,
268 keybinding_target.clone(),
269 move |_, window, cx| {
270 window.dispatch_action(Box::new(git::Pull), cx);
271 },
272 move |window, cx| {
273 git_action_tooltip(
274 "Pull",
275 &git::Pull,
276 "git pull",
277 keybinding_target.clone(),
278 window,
279 cx,
280 )
281 },
282 )
283 }
284
285 pub fn render_publish_button(
286 keybinding_target: Option<FocusHandle>,
287 id: SharedString,
288 ) -> SplitButton {
289 split_button(
290 id,
291 "Publish",
292 0,
293 0,
294 Some(IconName::ArrowUpFromLine),
295 keybinding_target.clone(),
296 move |_, window, cx| {
297 window.dispatch_action(Box::new(git::Push), cx);
298 },
299 move |window, cx| {
300 git_action_tooltip(
301 "Publish branch to remote",
302 &git::Push,
303 "git push --set-upstream",
304 keybinding_target.clone(),
305 window,
306 cx,
307 )
308 },
309 )
310 }
311
312 pub fn render_republish_button(
313 keybinding_target: Option<FocusHandle>,
314 id: SharedString,
315 ) -> SplitButton {
316 split_button(
317 id,
318 "Republish",
319 0,
320 0,
321 Some(IconName::ArrowUpFromLine),
322 keybinding_target.clone(),
323 move |_, window, cx| {
324 window.dispatch_action(Box::new(git::Push), cx);
325 },
326 move |window, cx| {
327 git_action_tooltip(
328 "Re-publish branch to remote",
329 &git::Push,
330 "git push --set-upstream",
331 keybinding_target.clone(),
332 window,
333 cx,
334 )
335 },
336 )
337 }
338
339 fn git_action_tooltip(
340 label: impl Into<SharedString>,
341 action: &dyn Action,
342 command: impl Into<SharedString>,
343 focus_handle: Option<FocusHandle>,
344 window: &mut Window,
345 cx: &mut App,
346 ) -> AnyView {
347 let label = label.into();
348 let command = command.into();
349
350 if let Some(handle) = focus_handle {
351 Tooltip::with_meta_in(
352 label.clone(),
353 Some(action),
354 command.clone(),
355 &handle,
356 window,
357 cx,
358 )
359 } else {
360 Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
361 }
362 }
363
364 fn render_git_action_menu(
365 id: impl Into<ElementId>,
366 keybinding_target: Option<FocusHandle>,
367 ) -> impl IntoElement {
368 PopoverMenu::new(id.into())
369 .trigger(
370 ui::ButtonLike::new_rounded_right("split-button-right")
371 .layer(ui::ElevationIndex::ModalSurface)
372 .size(ui::ButtonSize::None)
373 .child(
374 div()
375 .px_1()
376 .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
377 ),
378 )
379 .menu(move |window, cx| {
380 Some(ContextMenu::build(window, cx, |context_menu, _, _| {
381 context_menu
382 .when_some(keybinding_target.clone(), |el, keybinding_target| {
383 el.context(keybinding_target.clone())
384 })
385 .action("Fetch", git::Fetch.boxed_clone())
386 .action("Fetch From", git::FetchFrom.boxed_clone())
387 .action("Pull", git::Pull.boxed_clone())
388 .separator()
389 .action("Push", git::Push.boxed_clone())
390 .action("Push To", git::PushTo.boxed_clone())
391 .action("Force Push", git::ForcePush.boxed_clone())
392 }))
393 })
394 .anchor(Corner::TopRight)
395 }
396
397 #[allow(clippy::too_many_arguments)]
398 fn split_button(
399 id: SharedString,
400 left_label: impl Into<SharedString>,
401 ahead_count: usize,
402 behind_count: usize,
403 left_icon: Option<IconName>,
404 keybinding_target: Option<FocusHandle>,
405 left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
406 tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
407 ) -> SplitButton {
408 fn count(count: usize) -> impl IntoElement {
409 h_flex()
410 .ml_neg_px()
411 .h(rems(0.875))
412 .items_center()
413 .overflow_hidden()
414 .px_0p5()
415 .child(
416 Label::new(count.to_string())
417 .size(LabelSize::XSmall)
418 .line_height_style(LineHeightStyle::UiLabel),
419 )
420 }
421
422 let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
423
424 let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
425 format!("split-button-left-{}", id).into(),
426 ))
427 .layer(ui::ElevationIndex::ModalSurface)
428 .size(ui::ButtonSize::Compact)
429 .when(should_render_counts, |this| {
430 this.child(
431 h_flex()
432 .ml_neg_0p5()
433 .mr_1()
434 .when(behind_count > 0, |this| {
435 this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
436 .child(count(behind_count))
437 })
438 .when(ahead_count > 0, |this| {
439 this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
440 .child(count(ahead_count))
441 }),
442 )
443 })
444 .when_some(left_icon, |this, left_icon| {
445 this.child(
446 h_flex()
447 .ml_neg_0p5()
448 .mr_1()
449 .child(Icon::new(left_icon).size(IconSize::XSmall)),
450 )
451 })
452 .child(
453 div()
454 .child(Label::new(left_label).size(LabelSize::Small))
455 .mr_0p5(),
456 )
457 .on_click(left_on_click)
458 .tooltip(tooltip);
459
460 let right = render_git_action_menu(
461 ElementId::Name(format!("split-button-right-{}", id).into()),
462 keybinding_target,
463 )
464 .into_any_element();
465
466 SplitButton { left, right }
467 }
468}
469
470/// A visual representation of a file's Git status.
471#[derive(IntoElement, RegisterComponent)]
472pub struct GitStatusIcon {
473 status: FileStatus,
474}
475
476impl GitStatusIcon {
477 pub fn new(status: FileStatus) -> Self {
478 Self { status }
479 }
480}
481
482impl RenderOnce for GitStatusIcon {
483 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
484 let status = self.status;
485
486 let (icon_name, color) = if status.is_conflicted() {
487 (
488 IconName::Warning,
489 cx.theme().colors().version_control_conflict,
490 )
491 } else if status.is_deleted() {
492 (
493 IconName::SquareMinus,
494 cx.theme().colors().version_control_deleted,
495 )
496 } else if status.is_modified() {
497 (
498 IconName::SquareDot,
499 cx.theme().colors().version_control_modified,
500 )
501 } else {
502 (
503 IconName::SquarePlus,
504 cx.theme().colors().version_control_added,
505 )
506 };
507
508 Icon::new(icon_name).color(Color::Custom(color))
509 }
510}
511
512// View this component preview using `workspace: open component-preview`
513impl Component for GitStatusIcon {
514 fn scope() -> ComponentScope {
515 ComponentScope::VersionControl
516 }
517
518 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
519 fn tracked_file_status(code: StatusCode) -> FileStatus {
520 FileStatus::Tracked(git::status::TrackedStatus {
521 index_status: code,
522 worktree_status: code,
523 })
524 }
525
526 let modified = tracked_file_status(StatusCode::Modified);
527 let added = tracked_file_status(StatusCode::Added);
528 let deleted = tracked_file_status(StatusCode::Deleted);
529 let conflict = UnmergedStatus {
530 first_head: UnmergedStatusCode::Updated,
531 second_head: UnmergedStatusCode::Updated,
532 }
533 .into();
534
535 Some(
536 v_flex()
537 .gap_6()
538 .children(vec![example_group(vec![
539 single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
540 single_example("Added", GitStatusIcon::new(added).into_any_element()),
541 single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
542 single_example(
543 "Conflicted",
544 GitStatusIcon::new(conflict).into_any_element(),
545 ),
546 ])])
547 .into_any_element(),
548 )
549 }
550}