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