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