Skip to content

git_ui: Show more information in the branch picker #25359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c4c3659
Show info
Angelk90 Feb 21, 2025
6fb3ae1
Remove white space
Angelk90 Feb 21, 2025
f85c692
Fix format code
Angelk90 Feb 21, 2025
5de724c
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 21, 2025
80298ee
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 22, 2025
2cf2fcd
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 22, 2025
106c796
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 22, 2025
f53dae2
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 24, 2025
949e936
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 24, 2025
85c7075
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 24, 2025
71beb90
Revise the design (and show just SHA number and time)
danilo-leal Feb 25, 2025
3502294
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 25, 2025
27d3980
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 25, 2025
6be0dab
Fix
Angelk90 Feb 25, 2025
f5df1fa
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 25, 2025
97f810b
Add commit message and tidy the UI
danilo-leal Feb 26, 2025
a6f7dfa
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 26, 2025
4835e8e
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 26, 2025
00a5e97
Don't duplicate short SHA length
maxdeviant Feb 26, 2025
212ebef
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 26, 2025
2d92f83
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 26, 2025
0a3d794
Merge branch 'main' into show-branch-info
Angelk90 Feb 28, 2025
37a7f68
Fix
Angelk90 Feb 28, 2025
b48affc
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Feb 28, 2025
4d43797
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Mar 4, 2025
18f6546
Merge branch 'main' into show-branch-info
Angelk90 Mar 5, 2025
656d263
Merge branch 'main' into show-branch-info
Angelk90 Mar 6, 2025
b85d0af
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Mar 7, 2025
bf7f540
Fix
Angelk90 Mar 7, 2025
2fc0cdc
Fix format code
Angelk90 Mar 7, 2025
637bef9
Merge branch 'zed-industries:main' into show-branch-info
Angelk90 Mar 8, 2025
2b2c83e
Merge branch 'main' into show-branch-info
ConradIrwin Mar 11, 2025
226752b
Clarify rendering
ConradIrwin Mar 11, 2025
9b06700
Merge branch 'main' into show-branch-info
ConradIrwin Mar 11, 2025
14de0c0
Fix merge
ConradIrwin Mar 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/git/src/repository.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -119,6 +120,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,
Expand Down
1 change: 1 addition & 0 deletions crates/git_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
200 changes: 111 additions & 89 deletions crates/git_ui/src/branch_picker.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::{anyhow, Context as _};
use fuzzy::{StringMatch, StringMatchCandidate};
use fuzzy::StringMatchCandidate;

use git::repository::Branch;
use gpui::{
Expand All @@ -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,
};
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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);
Expand All @@ -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| {
Expand Down Expand Up @@ -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 {
Expand All @@ -183,34 +183,24 @@ 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,
style,
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(
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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;
};

Expand All @@ -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
Expand All @@ -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?
})
})??;

Expand All @@ -398,16 +373,40 @@ 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,
selected: bool,
_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}")))
Expand All @@ -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),
),
)
}),
),
)
}

Expand Down
Loading