diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index baa0ed09239fcd62b7e58a49397c33a0eb3813b6..592c04427dc860b77d8ba7a2a677c47ea648b47e 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -451,6 +451,13 @@ pub struct CommitDiff { pub files: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum CommitFileStatus { + Added, + Modified, + Deleted, +} + #[derive(Debug)] pub struct CommitFile { pub path: RepoPath, @@ -459,6 +466,16 @@ pub struct CommitFile { pub is_binary: bool, } +impl CommitFile { + pub fn status(&self) -> CommitFileStatus { + match (&self.old_text, &self.new_text) { + (None, Some(_)) => CommitFileStatus::Added, + (Some(_), None) => CommitFileStatus::Deleted, + _ => CommitFileStatus::Modified, + } + } +} + impl CommitDetails { pub fn short_sha(&self) -> SharedString { self.sha[..SHORT_SHA_LENGTH].to_string().into() diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index a8318e5122f7f37b00ee9a2d966b387282c8073d..dead8c377b4320cef912e09691eee4ee447c2aa1 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -3,7 +3,10 @@ use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use git::{ BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote, parse_git_remote_url, - repository::{CommitDiff, InitialGraphCommitData, LogOrder, LogSource}, + repository::{ + CommitDiff, CommitFile, CommitFileStatus, InitialGraphCommitData, LogOrder, LogSource, + RepoPath, + }, }; use git_ui::{commit_tooltip::CommitAvatar, commit_view::CommitView}; use gpui::{ @@ -11,7 +14,7 @@ use gpui::{ DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Hsla, InteractiveElement, ParentElement, PathBuilder, Pixels, Point, Render, ScrollStrategy, ScrollWheelEvent, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, - anchored, deferred, point, px, + anchored, deferred, point, px, uniform_list, }; use menu::{Cancel, SelectNext, SelectPrevious}; use project::{ @@ -41,6 +44,117 @@ const RESIZE_HANDLE_WIDTH: f32 = 8.0; struct DraggedSplitHandle; +#[derive(Clone)] +struct ChangedFileEntry { + icon_name: IconName, + icon_color: Hsla, + file_name: SharedString, + dir_path: SharedString, + repo_path: RepoPath, +} + +impl ChangedFileEntry { + fn from_commit_file(file: &CommitFile, cx: &App) -> Self { + let file_name: SharedString = file + .path + .file_name() + .map(|n| n.to_string()) + .unwrap_or_default() + .into(); + let dir_path: SharedString = file + .path + .parent() + .map(|p| p.as_unix_str().to_string()) + .unwrap_or_default() + .into(); + let colors = cx.theme().colors(); + let (icon_name, icon_color) = match file.status() { + CommitFileStatus::Added => (IconName::SquarePlus, colors.version_control_added), + CommitFileStatus::Modified => (IconName::SquareDot, colors.version_control_modified), + CommitFileStatus::Deleted => (IconName::SquareMinus, colors.version_control_deleted), + }; + Self { + icon_name, + icon_color, + file_name, + dir_path, + repo_path: file.path.clone(), + } + } + + fn open_in_commit_view( + &self, + commit_sha: &SharedString, + repository: &WeakEntity, + workspace: &WeakEntity, + window: &mut Window, + cx: &mut App, + ) { + CommitView::open( + commit_sha.to_string(), + repository.clone(), + workspace.clone(), + None, + Some(self.repo_path.clone()), + window, + cx, + ); + } + + fn render( + &self, + ix: usize, + commit_sha: SharedString, + repository: WeakEntity, + workspace: WeakEntity, + cx: &App, + ) -> AnyElement { + h_flex() + .id(("changed-file", ix)) + .px_3() + .py_px() + .gap_1() + .min_w_0() + .overflow_hidden() + .cursor_pointer() + .rounded_md() + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .child( + div().flex_none().child( + Icon::new(self.icon_name) + .size(IconSize::Small) + .color(Color::Custom(self.icon_color)), + ), + ) + .child( + div().flex_none().child( + Label::new(self.file_name.clone()) + .size(LabelSize::Small) + .single_line(), + ), + ) + .when(!self.dir_path.is_empty(), |this| { + this.child( + div().min_w_0().overflow_hidden().child( + Label::new(self.dir_path.clone()) + .size(LabelSize::Small) + .color(Color::Muted) + .truncate() + .single_line(), + ), + ) + }) + .on_click({ + let entry = self.clone(); + move |_, window, cx| { + entry.open_in_commit_view(&commit_sha, &repository, &workspace, window, cx); + } + }) + .into_any_element() + } +} + pub struct SplitState { left_ratio: f32, visible_left_ratio: f32, @@ -1154,7 +1268,22 @@ impl GitGraph { .map(|diff| diff.files.len()) .unwrap_or(0); + let sorted_file_entries: Rc> = Rc::new( + self.selected_commit_diff + .as_ref() + .map(|diff| { + let mut files: Vec<_> = diff.files.iter().collect(); + files.sort_by_key(|file| file.status()); + files + .into_iter() + .map(|file| ChangedFileEntry::from_commit_file(file, cx)) + .collect() + }) + .unwrap_or_default(), + ); + v_flex() + .relative() .w(px(300.)) .h_full() .border_l_1() @@ -1163,25 +1292,32 @@ impl GitGraph { .flex_basis(DefiniteLength::Fraction( self.commit_details_split_state.read(cx).right_ratio(), )) + .child( + div().absolute().top_2().right_2().child( + IconButton::new("close-detail", IconName::Close) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |this, _, _, cx| { + this.selected_entry_idx = None; + this.selected_commit_diff = None; + this._commit_diff_task = None; + cx.notify(); + })), + ), + ) .child( v_flex() - .p_3() + .w_full() + .min_w_0() + .flex_none() + .pt_3() .gap_3() - .child( - h_flex().justify_between().child(avatar).child( - IconButton::new("close-detail", IconName::Close) - .icon_size(IconSize::Small) - .on_click(cx.listener(move |this, _, _, cx| { - this.selected_entry_idx = None; - this.selected_commit_diff = None; - this._commit_diff_task = None; - cx.notify(); - })), - ), - ) .child( v_flex() + .w_full() + .px_3() + .items_center() .gap_0p5() + .child(avatar) .child(Label::new(author_name.clone()).weight(FontWeight::SEMIBOLD)) .child( Label::new(date_string) @@ -1190,14 +1326,20 @@ impl GitGraph { ), ) .children((!ref_names.is_empty()).then(|| { - h_flex().gap_1().flex_wrap().children( - ref_names - .iter() - .map(|name| self.render_badge(name, accent_color)), - ) + h_flex() + .px_3() + .justify_center() + .gap_1() + .flex_wrap() + .children( + ref_names + .iter() + .map(|name| self.render_badge(name, accent_color)), + ) })) .child( v_flex() + .px_3() .gap_1p5() .child( h_flex() @@ -1224,7 +1366,7 @@ impl GitGraph { h_flex() .gap_1() .child( - Icon::new(IconName::Hash) + Icon::new(IconName::FileGit) .size(IconSize::Small) .color(Color::Muted), ) @@ -1286,72 +1428,55 @@ impl GitGraph { ), ) }), - ), - ) - .child( - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .p_3() - .min_w_0() + ) .child( - v_flex() - .gap_2() + div() + .w_full() + .min_w_0() + .border_t_1() + .border_color(cx.theme().colors().border) + .p_3() .child(Label::new(subject).weight(FontWeight::MEDIUM)), ), ) .child( - div() + v_flex() .flex_1() - .overflow_hidden() + .min_h_0() .border_t_1() .border_color(cx.theme().colors().border) - .p_3() .child( - v_flex() - .gap_2() - .child( - Label::new(format!("{} Changed Files", changed_files_count)) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(self.selected_commit_diff.as_ref().map(|diff| { - v_flex().gap_1().children(diff.files.iter().map(|file| { - let file_name: String = file - .path - .file_name() - .map(|n| n.to_string()) - .unwrap_or_default(); - let dir_path: String = file - .path - .parent() - .map(|p| p.as_unix_str().to_string()) - .unwrap_or_default(); - - h_flex() - .gap_1() - .overflow_hidden() - .child( - Icon::new(IconName::File) - .size(IconSize::Small) - .color(Color::Accent), - ) - .child( - Label::new(file_name) - .size(LabelSize::Small) - .single_line(), + div().px_3().pt_3().pb_1().child( + Label::new(format!("{} Changed Files", changed_files_count)) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .child({ + let entries = sorted_file_entries; + let entry_count = entries.len(); + let commit_sha = full_sha.clone(); + let repository = repository.downgrade(); + let workspace = self.workspace.clone(); + uniform_list( + "changed-files-list", + entry_count, + move |range, _window, cx| { + range + .map(|ix| { + entries[ix].render( + ix, + commit_sha.clone(), + repository.clone(), + workspace.clone(), + cx, ) - .when(!dir_path.is_empty(), |this| { - this.child( - Label::new(dir_path) - .size(LabelSize::Small) - .color(Color::Muted) - .single_line(), - ) - }) - })) - })), - ), + }) + .collect() + }, + ) + .flex_1() + }), ) .into_any_element() }