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