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