From bcd78afabfba579074af137c6f4d4f4c5232dd6c Mon Sep 17 00:00:00 2001
From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com>
Date: Wed, 28 Jan 2026 15:03:25 +0100
Subject: [PATCH] git_ui: Align repo and branch pickers (#47752)
When working in a workspace with multiple repositories, the git panel
provides a repository picker to switch between them. However, there was
no visual indication of which repositories have uncommitted changes:
users had to either select each repository individually or check the
project panel where modified directories are highlighted.
This change adds git status icons to the repository picker, allowing
users to see at a glance which repositories contain changes (modified,
added, deleted, or conflicted files). The icons use the same visual
language already established for file status throughout the git panel.
Additionally, the repository picker now matches the branch picker's
styling for visual consistency:
- Added "Repositories" header
- Aligned popover width and positioning
- Added scrollbar
- Added check icon next to currently selected repo
- Added selected branch under repo list item
- Sort by display name is now case insensitive
Before:
After:
Branch picker for style reference:
Release Notes:
- Git: Improved the project picker in the panel by also displaying the
GIt status icon on them, to clearly indicate which repos have changes.
---------
Co-authored-by: Danilo Leal
---
crates/git_ui/src/git_panel.rs | 23 +++--
crates/git_ui/src/repository_selector.rs | 103 ++++++++++++++++++++---
2 files changed, 107 insertions(+), 19 deletions(-)
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
index 92fae951a5e46a81bce4bd1b57e59dea936d6509..0bd1b46a5e909ff296a1fb359d3b586586cbc8f7 100644
--- a/crates/git_ui/src/git_panel.rs
+++ b/crates/git_ui/src/git_panel.rs
@@ -35,7 +35,7 @@ use git::{
StashApply, StashPop, TrashUntrackedFiles, UnstageAll,
};
use gpui::{
- Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Entity,
+ Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Empty, Entity,
EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point,
PromptLevel, ScrollStrategy, Subscription, Task, UniformListScrollHandle, WeakEntity, actions,
anchored, deferred, point, size, uniform_list,
@@ -5771,22 +5771,33 @@ impl RenderOnce for PanelRepoFooter {
let repo_selector_trigger = Button::new("repo-selector", truncated_repo_name)
.size(ButtonSize::None)
- .label_size(LabelSize::Small)
- .color(Color::Muted);
+ .label_size(LabelSize::Small);
let repo_selector = PopoverMenu::new("repository-switcher")
.menu({
let project = project;
move |window, cx| {
let project = project.clone()?;
- Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx)))
+ Some(cx.new(|cx| RepositorySelector::new(project, rems(20.), window, cx)))
}
})
.trigger_with_tooltip(
- repo_selector_trigger.disabled(single_repo).truncate(true),
- Tooltip::text("Switch Active Repository"),
+ repo_selector_trigger
+ .when(single_repo, |this| this.disabled(true).color(Color::Muted))
+ .truncate(true),
+ move |_, cx| {
+ if single_repo {
+ cx.new(|_| Empty).into()
+ } else {
+ Tooltip::simple("Switch Active Repository", cx)
+ }
+ },
)
.anchor(Corner::BottomLeft)
+ .offset(gpui::Point {
+ x: px(0.0),
+ y: px(-2.0),
+ })
.into_any_element();
let branch_selector_button = Button::new("branch-selector", truncated_branch_name)
diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs
index 5e60bebc4279df4bbf90a685ccffa957803253f7..463540de90ce204fba6af5b5d17a6a9e8f831fb8 100644
--- a/crates/git_ui/src/repository_selector.rs
+++ b/crates/git_ui/src/repository_selector.rs
@@ -1,3 +1,5 @@
+use crate::git_status_icon;
+use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
use itertools::Itertools;
use picker::{Picker, PickerDelegate, PickerEditorPosition};
@@ -38,7 +40,12 @@ impl RepositorySelector {
let repository_entries = git_store.update(cx, |git_store, _cx| {
let mut repos: Vec<_> = git_store.repositories().values().cloned().collect();
- repos.sort_by_key(|a| a.read(_cx).display_name());
+ repos.sort_by(|a, b| {
+ a.read(_cx)
+ .display_name()
+ .to_lowercase()
+ .cmp(&b.read(_cx).display_name().to_lowercase())
+ });
repos
});
@@ -51,17 +58,24 @@ impl RepositorySelector {
.cmp(&b.read(cx).display_name().len())
});
+ let active_repository = git_store.read(cx).active_repository();
+ let selected_index = active_repository
+ .as_ref()
+ .and_then(|active| filtered_repositories.iter().position(|repo| repo == active))
+ .unwrap_or(0);
let delegate = RepositorySelectorDelegate {
repository_selector: cx.entity().downgrade(),
repository_entries,
filtered_repositories,
- selected_index: 0,
+ active_repository,
+ selected_index,
};
let picker = cx.new(|cx| {
Picker::uniform_list(delegate, window, cx)
.widest_item(widest_item_ix)
.max_height(Some(rems(20.).into()))
+ .show_scrollbar(true)
});
RepositorySelector { picker, width }
@@ -122,6 +136,7 @@ pub struct RepositorySelectorDelegate {
repository_selector: WeakEntity,
repository_entries: Vec>,
filtered_repositories: Vec>,
+ active_repository: Option>,
selected_index: usize,
}
@@ -129,7 +144,15 @@ impl RepositorySelectorDelegate {
pub fn update_repository_entries(&mut self, all_repositories: Vec>) {
self.repository_entries = all_repositories.clone();
self.filtered_repositories = all_repositories;
- self.selected_index = 0;
+ self.selected_index = self
+ .active_repository
+ .as_ref()
+ .and_then(|active| {
+ self.filtered_repositories
+ .iter()
+ .position(|repo| repo == active)
+ })
+ .unwrap_or(0);
}
}
@@ -193,9 +216,20 @@ impl PickerDelegate for RepositorySelectorDelegate {
this.update_in(cx, |this, window, cx| {
let mut sorted_repositories = filtered_repositories;
- sorted_repositories.sort_by_key(|a| a.read(cx).display_name());
+ sorted_repositories.sort_by(|a, b| {
+ a.read(cx)
+ .display_name()
+ .to_lowercase()
+ .cmp(&b.read(cx).display_name().to_lowercase())
+ });
+ let selected_index = this
+ .delegate
+ .active_repository
+ .as_ref()
+ .and_then(|active| sorted_repositories.iter().position(|repo| repo == active))
+ .unwrap_or(0);
this.delegate.filtered_repositories = sorted_repositories;
- this.delegate.set_selected_index(0, window, cx);
+ this.delegate.set_selected_index(selected_index, window, cx);
cx.notify();
})
.ok();
@@ -226,13 +260,56 @@ impl PickerDelegate for RepositorySelectorDelegate {
cx: &mut Context>,
) -> Option {
let repo_info = self.filtered_repositories.get(ix)?;
- let display_name = repo_info.read(cx).display_name();
- Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .toggle_state(selected)
- .child(Label::new(display_name)),
- )
+ let repo = repo_info.read(cx);
+ let display_name = repo.display_name();
+ let summary = repo.status_summary();
+ let is_active = self
+ .active_repository
+ .as_ref()
+ .is_some_and(|active| active == repo_info);
+
+ let mut item = ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(
+ h_flex()
+ .gap_1()
+ .child(Label::new(display_name))
+ .when(is_active, |this| {
+ this.child(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Accent),
+ )
+ }),
+ );
+
+ if summary.count > 0 {
+ let status = if summary.conflict > 0 {
+ FileStatus::Unmerged(UnmergedStatus {
+ first_head: UnmergedStatusCode::Updated,
+ second_head: UnmergedStatusCode::Updated,
+ })
+ } else if summary.worktree.deleted > 0 || summary.index.deleted > 0 {
+ FileStatus::Tracked(TrackedStatus {
+ index_status: StatusCode::Deleted,
+ worktree_status: StatusCode::Unmodified,
+ })
+ } else if summary.worktree.modified > 0 || summary.index.modified > 0 {
+ FileStatus::Tracked(TrackedStatus {
+ index_status: StatusCode::Modified,
+ worktree_status: StatusCode::Unmodified,
+ })
+ } else {
+ FileStatus::Tracked(TrackedStatus {
+ index_status: StatusCode::Added,
+ worktree_status: StatusCode::Unmodified,
+ })
+ };
+ item = item.end_slot(div().pr_2().child(git_status_icon(status)));
+ }
+
+ Some(item)
}
}