diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 01393186249b82e4280d73ce9014c9b710a06682..6729f3fcfef471a2009dfc944b9c90c9efe2aa93 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1,4 +1,7 @@ -use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}; +use crate::{ + git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}, + remote_button::{render_publish_button, render_push_button}, +}; use anyhow::Result; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; use collections::HashSet; @@ -9,8 +12,9 @@ use editor::{ }; use futures::StreamExt; use git::{ - repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, - UnstageAll, UnstageAndNext, + repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, + status::FileStatus, + Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, }; use gpui::{ actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, @@ -1022,6 +1026,287 @@ impl Render for ProjectDiffToolbar { } } +#[derive(IntoElement, IntoComponent)] +#[component(scope = "Version Control")] +pub struct ProjectDiffEmptyState { + pub no_repo: bool, + pub can_push_and_pull: bool, + pub focus_handle: Option, + pub current_branch: Option, + // has_pending_commits: bool, + // ahead_of_remote: bool, + // no_git_repository: bool, +} + +impl RenderOnce for ProjectDiffEmptyState { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool { + match self.current_branch { + Some(Branch { + upstream: + Some(Upstream { + tracking: + UpstreamTracking::Tracked(UpstreamTrackingStatus { + ahead, behind, .. + }), + .. + }), + .. + }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true, + _ => false, + } + }; + + let change_count = |current_branch: &Branch| -> (usize, usize) { + match current_branch { + Branch { + upstream: + Some(Upstream { + tracking: + UpstreamTracking::Tracked(UpstreamTrackingStatus { + ahead, behind, .. + }), + .. + }), + .. + } => (*ahead as usize, *behind as usize), + _ => (0, 0), + } + }; + + let not_ahead_or_behind = status_against_remote(0, 0); + let ahead_of_remote = status_against_remote(1, 0); + let branch_not_on_remote = if let Some(branch) = self.current_branch.as_ref() { + branch.upstream.is_none() + } else { + false + }; + + let has_branch_container = |branch: &Branch| { + h_flex() + .max_w(px(420.)) + .bg(cx.theme().colors().text.opacity(0.05)) + .border_1() + .border_color(cx.theme().colors().border) + .rounded_sm() + .gap_8() + .px_6() + .py_4() + .map(|this| { + if ahead_of_remote { + let ahead_count = change_count(branch).0; + let ahead_string = format!("{} Commits Ahead", ahead_count); + this.child( + v_flex() + .child(Headline::new(ahead_string).size(HeadlineSize::Small)) + .child( + Label::new(format!("Push your changes to {}", branch.name)) + .color(Color::Muted), + ), + ) + .child(div().child(render_push_button( + self.focus_handle, + "push".into(), + ahead_count as u32, + ))) + } else if branch_not_on_remote { + this.child( + v_flex() + .child(Headline::new("Publish Branch").size(HeadlineSize::Small)) + .child( + Label::new(format!("Create {} on remote", branch.name)) + .color(Color::Muted), + ), + ) + .child( + div().child(render_publish_button(self.focus_handle, "publish".into())), + ) + } else { + this.child(Label::new("Remote status unknown").color(Color::Muted)) + } + }) + }; + + v_flex().size_full().items_center().justify_center().child( + v_flex() + .gap_1() + .when(self.no_repo, |this| { + // TODO: add git init + this.text_center() + .child(Label::new("No Repository").color(Color::Muted)) + }) + .map(|this| { + if not_ahead_or_behind && self.current_branch.is_some() { + this.text_center() + .child(Label::new("No Changes").color(Color::Muted)) + } else { + this.when_some(self.current_branch.as_ref(), |this, branch| { + this.child(has_branch_container(&branch)) + }) + } + }), + ) + } +} + +// .when(self.can_push_and_pull, |this| { +// let remote_button = crate::render_remote_button( +// "project-diff-remote-button", +// &branch, +// self.focus_handle.clone(), +// false, +// ); + +// match remote_button { +// Some(button) => { +// this.child(h_flex().justify_around().child(button)) +// } +// None => this.child( +// h_flex() +// .justify_around() +// .child(Label::new("Remote up to date")), +// ), +// } +// }), +// +// // .map(|this| { +// this.child(h_flex().justify_around().mt_1().child( +// Button::new("project-diff-close-button", "Close").when_some( +// self.focus_handle.clone(), +// |this, focus_handle| { +// this.key_binding(KeyBinding::for_action_in( +// &CloseActiveItem::default(), +// &focus_handle, +// window, +// cx, +// )) +// .on_click(move |_, window, cx| { +// window.focus(&focus_handle); +// window +// .dispatch_action(Box::new(CloseActiveItem::default()), cx); +// }) +// }, +// ), +// )) +// }), + +mod preview { + use git::repository::{ + Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus, + }; + use ui::prelude::*; + + use super::ProjectDiffEmptyState; + + // View this component preview using `workspace: open component-preview` + impl ComponentPreview for ProjectDiffEmptyState { + fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement { + let unknown_upstream: Option = None; + let ahead_of_upstream: Option = Some( + UpstreamTrackingStatus { + ahead: 2, + behind: 0, + } + .into(), + ); + + let not_ahead_or_behind_upstream: Option = Some( + UpstreamTrackingStatus { + ahead: 0, + behind: 0, + } + .into(), + ); + + fn branch(upstream: Option) -> Branch { + Branch { + is_head: true, + name: "some-branch".into(), + upstream: upstream.map(|tracking| Upstream { + ref_name: "origin/some-branch".into(), + tracking, + }), + most_recent_commit: Some(CommitSummary { + sha: "abc123".into(), + subject: "Modify stuff".into(), + commit_timestamp: 1710932954, + has_parent: true, + }), + } + } + + let no_repo_state = ProjectDiffEmptyState { + no_repo: true, + can_push_and_pull: false, + focus_handle: None, + current_branch: None, + }; + + let no_changes_state = ProjectDiffEmptyState { + no_repo: false, + can_push_and_pull: true, + focus_handle: None, + current_branch: Some(branch(not_ahead_or_behind_upstream)), + }; + + let ahead_of_upstream_state = ProjectDiffEmptyState { + no_repo: false, + can_push_and_pull: true, + focus_handle: None, + current_branch: Some(branch(ahead_of_upstream)), + }; + + let unknown_upstream_state = ProjectDiffEmptyState { + no_repo: false, + can_push_and_pull: true, + focus_handle: None, + current_branch: Some(branch(unknown_upstream)), + }; + + let (width, height) = (px(480.), px(320.)); + + v_flex() + .gap_6() + .children(vec![example_group(vec![ + single_example( + "No Repo", + div() + .w(width) + .h(height) + .child(no_repo_state) + .into_any_element(), + ), + single_example( + "No Changes", + div() + .w(width) + .h(height) + .child(no_changes_state) + .into_any_element(), + ), + single_example( + "Unknown Upstream", + div() + .w(width) + .h(height) + .child(unknown_upstream_state) + .into_any_element(), + ), + single_example( + "Ahead of Remote", + div() + .w(width) + .h(height) + .child(ahead_of_upstream_state) + .into_any_element(), + ), + ]) + .vertical()]) + .into_any_element() + } + } +} + #[cfg(not(target_os = "windows"))] #[cfg(test)] mod tests {