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