git_ui: Show more information in the branch picker (#25359)

Angelk90 , Danilo Leal , Marshall Bowers , and Conrad Irwin created

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 <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

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(-)

Detailed changes

Cargo.lock 🔗

@@ -5477,6 +5477,7 @@ dependencies = [
  "telemetry",
  "theme",
  "time",
+ "time_format",
  "ui",
  "unindent",
  "util",

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,

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

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<BranchList> {
     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<usize>,
 }
 
 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<Entity<Repository>>,
-        style: BranchListStyle,
-        branch_name_trailoff_after: usize,
-    ) -> Self {
+    fn new(repo: Option<Entity<Repository>>, 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<Picker<Self>>,
     ) -> 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::<Vec<StringMatchCandidate>>();
-            let matches: Vec<BranchEntry> = 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::<Vec<StringMatchCandidate>>();
                 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<Picker<Self>>) -> Option<AnyElement> {
+        None
+    }
+
     fn render_match(
         &self,
         ix: usize,
@@ -405,9 +384,29 @@ impl PickerDelegate for BranchListDelegate {
         _window: &mut Window,
         _cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        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),
+                                ),
+                            )
+                        }),
+                ),
         )
     }