From d562f58e76f2bf9ed519e253a1b2e2eefb8675f1 Mon Sep 17 00:00:00 2001 From: Angelk90 <20476002+Angelk90@users.noreply.github.com> Date: Tue, 11 Mar 2025 04:05:29 +0100 Subject: [PATCH] git_ui: Show more information in the branch picker (#25359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final product: ![CleanShot 2025-02-26 at 9  08 17@2x](https://github.com/user-attachments/assets/e5db1932-b2c6-4b32-ab67-ef0a0d19f022) Release Notes: - Added more information about Git branches to the branch picker. --------- Co-authored-by: Danilo Leal Co-authored-by: Marshall Bowers Co-authored-by: Conrad Irwin --- Cargo.lock | 1 + crates/git/src/repository.rs | 7 + crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/branch_picker.rs | 200 ++++++++++++++++------------- 4 files changed, 120 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07203de787..582104b215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5477,6 +5477,7 @@ dependencies = [ "telemetry", "theme", "time", + "time_format", "ui", "unindent", "util", diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 18ccd5c400..8dd500ca6b 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,4 +1,5 @@ use crate::status::FileStatus; +use crate::SHORT_SHA_LENGTH; use crate::{blame::Blame, status::GitStatus}; use anyhow::{anyhow, Context, Result}; use askpass::{AskPassResult, AskPassSession}; @@ -127,6 +128,12 @@ pub struct CommitDetails { pub committer_name: SharedString, } +impl CommitDetails { + pub fn short_sha(&self) -> SharedString { + self.sha[..SHORT_SHA_LENGTH].to_string().into() + } +} + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Remote { pub name: SharedString, diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index fe1a3bc9f0..2dea431753 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -50,6 +50,7 @@ strum.workspace = true telemetry.workspace = true theme.workspace = true time.workspace = true +time_format.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 3b6ec702b2..c5da2f3077 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Context as _}; -use fuzzy::{StringMatch, StringMatchCandidate}; +use fuzzy::StringMatchCandidate; use git::repository::Branch; use gpui::{ @@ -10,6 +10,8 @@ use gpui::{ use picker::{Picker, PickerDelegate}; use project::git::Repository; use std::sync::Arc; +use time::OffsetDateTime; +use time_format::format_local_timestamp; use ui::{ prelude::*, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, PopoverMenuHandle, Tooltip, }; @@ -63,7 +65,7 @@ pub fn popover( cx: &mut App, ) -> Entity { cx.new(|cx| { - let list = BranchList::new(repository, BranchListStyle::Popover, rems(15.), window, cx); + let list = BranchList::new(repository, BranchListStyle::Popover, rems(20.), window, cx); list.focus_handle(cx).focus(window); list }) @@ -96,10 +98,17 @@ impl BranchList { .map(|repository| repository.read(cx).branches()); cx.spawn_in(window, |this, mut cx| async move { - let all_branches = all_branches_request + let mut all_branches = all_branches_request .context("No active repository")? .await??; + all_branches.sort_by_key(|branch| { + branch + .most_recent_commit + .as_ref() + .map(|commit| 0 - commit.commit_timestamp) + }); + this.update_in(&mut cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { picker.delegate.all_branches = Some(all_branches); @@ -111,7 +120,7 @@ impl BranchList { }) .detach_and_log_err(cx); - let delegate = BranchListDelegate::new(repository.clone(), style, (1.6 * width.0) as usize); + let delegate = BranchListDelegate::new(repository.clone(), style); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { @@ -162,18 +171,9 @@ impl Render for BranchList { } #[derive(Debug, Clone)] -enum BranchEntry { - Branch(StringMatch), - History(String), -} - -impl BranchEntry { - fn name(&self) -> &str { - match self { - Self::Branch(branch) => &branch.string, - Self::History(branch) => &branch, - } - } +struct BranchEntry { + branch: Branch, + positions: Vec, } pub struct BranchListDelegate { @@ -183,17 +183,11 @@ pub struct BranchListDelegate { style: BranchListStyle, selected_index: usize, last_query: String, - /// Max length of branch name before we truncate it and add a trailing `...`. - branch_name_trailoff_after: usize, modifiers: Modifiers, } impl BranchListDelegate { - fn new( - repo: Option>, - style: BranchListStyle, - branch_name_trailoff_after: usize, - ) -> Self { + fn new(repo: Option>, style: BranchListStyle) -> Self { Self { matches: vec![], repo, @@ -201,16 +195,12 @@ impl BranchListDelegate { all_branches: None, selected_index: 0, last_query: Default::default(), - branch_name_trailoff_after, modifiers: Default::default(), } } fn has_exact_match(&self, target: &str) -> bool { - self.matches.iter().any(|mat| match mat { - BranchEntry::Branch(branch) => branch.string == target, - _ => false, - }) + self.matches.iter().any(|entry| entry.branch.name == target) } fn create_branch( @@ -266,37 +256,27 @@ impl PickerDelegate for BranchListDelegate { window: &mut Window, cx: &mut Context>, ) -> Task<()> { - let Some(mut all_branches) = self.all_branches.clone() else { + let Some(all_branches) = self.all_branches.clone() else { return Task::ready(()); }; + const RECENT_BRANCHES_COUNT: usize = 10; cx.spawn_in(window, move |picker, mut cx| async move { - const RECENT_BRANCHES_COUNT: usize = 10; - if query.is_empty() { - if all_branches.len() > RECENT_BRANCHES_COUNT { - // Truncate list of recent branches - // Do a partial sort to show recent-ish branches first. - all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| { - rhs.priority_key().cmp(&lhs.priority_key()) - }); - all_branches.truncate(RECENT_BRANCHES_COUNT); - } - all_branches.sort_unstable_by(|lhs, rhs| { - rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name)) - }); - } - - let candidates = all_branches - .into_iter() - .enumerate() - .map(|(ix, command)| StringMatchCandidate::new(ix, &command.name)) - .collect::>(); - let matches: Vec = if query.is_empty() { - candidates + let matches = if query.is_empty() { + all_branches .into_iter() - .map(|candidate| BranchEntry::History(candidate.string)) + .take(RECENT_BRANCHES_COUNT) + .map(|branch| BranchEntry { + branch, + positions: Vec::new(), + }) .collect() } else { + let candidates = all_branches + .iter() + .enumerate() + .map(|(ix, command)| StringMatchCandidate::new(ix, &command.name.clone())) + .collect::>(); fuzzy::match_strings( &candidates, &query, @@ -308,7 +288,10 @@ impl PickerDelegate for BranchListDelegate { .await .iter() .cloned() - .map(BranchEntry::Branch) + .map(|candidate| BranchEntry { + branch: all_branches[candidate.candidate_id].clone(), + positions: candidate.positions, + }) .collect() }; picker @@ -337,7 +320,7 @@ impl PickerDelegate for BranchListDelegate { return; } - let Some(branch) = self.matches.get(self.selected_index()) else { + let Some(entry) = self.matches.get(self.selected_index()) else { return; }; @@ -349,14 +332,14 @@ impl PickerDelegate for BranchListDelegate { if current_branch .flatten() - .is_some_and(|current_branch| current_branch == branch.name()) + .is_some_and(|current_branch| current_branch == entry.branch.name) { cx.emit(DismissEvent); return; } cx.spawn_in(window, { - let branch = branch.clone(); + let branch = entry.branch.clone(); |picker, mut cx| async move { let branch_change_task = picker.update(&mut cx, |this, cx| { let repo = this @@ -369,16 +352,8 @@ impl PickerDelegate for BranchListDelegate { let cx = cx.to_async(); anyhow::Ok(async move { - match branch { - BranchEntry::Branch(StringMatch { - string: branch_name, - .. - }) - | BranchEntry::History(branch_name) => { - cx.update(|cx| repo.read(cx).change_branch(&branch_name))? - .await? - } - } + cx.update(|cx| repo.read(cx).change_branch(&branch.name))? + .await? }) })??; @@ -398,6 +373,10 @@ impl PickerDelegate for BranchListDelegate { cx.emit(DismissEvent); } + fn render_header(&self, _: &mut Window, _cx: &mut Context>) -> Option { + None + } + fn render_match( &self, ix: usize, @@ -405,9 +384,29 @@ impl PickerDelegate for BranchListDelegate { _window: &mut Window, _cx: &mut Context>, ) -> Option { - let hit = &self.matches[ix]; - let shortened_branch_name = - util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after); + let entry = &self.matches[ix]; + + let (commit_time, commit_message) = entry + .branch + .most_recent_commit + .as_ref() + .map(|commit| { + let commit_message = commit.subject.clone(); + let commit_time = OffsetDateTime::from_unix_timestamp(commit.commit_timestamp) + .unwrap_or_else(|_| OffsetDateTime::now_utc()); + let formatted_time = format_local_timestamp( + commit_time, + OffsetDateTime::now_utc(), + time_format::TimestampFormat::Relative, + ); + (formatted_time, commit_message) + }) + .unwrap_or_else(|| { + ( + "Unknown Date".to_string(), + SharedString::from("No commit message available"), + ) + }); Some( ListItem::new(SharedString::from(format!("vcs-menu-{ix}"))) @@ -418,26 +417,49 @@ impl PickerDelegate for BranchListDelegate { }) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) - .when(matches!(hit, BranchEntry::History(_)), |el| { - el.end_slot( - Icon::new(IconName::HistoryRerun) - .color(Color::Muted) - .size(IconSize::Small), - ) - }) - .map(|el| match hit { - BranchEntry::Branch(branch) => { - let highlights: Vec<_> = branch - .positions - .iter() - .filter(|index| index < &&self.branch_name_trailoff_after) - .copied() - .collect(); - - el.child(HighlightedLabel::new(shortened_branch_name, highlights)) - } - BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)), - }), + .child( + v_flex() + .w_full() + .child( + h_flex() + .w_full() + .flex_shrink() + .overflow_x_hidden() + .gap_2() + .justify_between() + .child( + div().flex_shrink().overflow_x_hidden().child( + HighlightedLabel::new( + entry.branch.name.clone(), + entry.positions.clone(), + ) + .truncate(), + ), + ) + .child( + Label::new(commit_time) + .size(LabelSize::Small) + .color(Color::Muted) + .into_element(), + ), + ) + .when(self.style == BranchListStyle::Modal, |el| { + el.child( + div().max_w_96().child( + Label::new( + commit_message + .split('\n') + .next() + .unwrap_or_default() + .to_string(), + ) + .size(LabelSize::Small) + .truncate() + .color(Color::Muted), + ), + ) + }), + ), ) }