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

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>
This commit is contained in:
Angelk90 2025-03-11 04:05:29 +01:00 committed by GitHub
parent 94e4aa626d
commit d562f58e76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 120 additions and 89 deletions

1
Cargo.lock generated
View file

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

View file

@ -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,

View file

@ -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

View file

@ -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),
),
)
}),
),
)
}