branch_picker: Add remote icons and fix edge cases (#61215)
# Objective Make the branch picker easier to scan and ensure branch creation and remote icons behave consistently across All, Local, and Remote filters. ## Solution - Group local and remote branches under their own headers in the All filter. - Place the create-branch option above branch sections when there is no exact branch-name match. - Resolve each remote's hosting provider from its URL and show the provider icon for branches from that remote. - Include remote URLs in remote-workspace repository responses so arbitrary remote names work without hardcoding `origin` or `upstream`. ## Edge Cases Fixed - An exact branch can exist outside the active Local or Remote filter. The picker now checks all branches before offering to create a duplicate branch. - A non-exact search can return fuzzy branch matches and a create action. The create action now stays at the top, before the Local and Remote section headers. - When a filtered search has no branch matches, the create action is shown without incorrectly placing it under a Local Branches or Remote Branches header. - Repositories can use any remote name, not only `origin` and `upstream`. Provider icons are resolved independently for every configured remote. - Remotes hosted on an unknown or unsupported forge use the generic server icon instead of implying a desktop or a specific provider. - Remote-workspace repositories carry the same remote-name-to-URL data as local repositories, so their branch icons follow the same provider-resolution path. ## Testing - `cargo check -p git_ui --no-default-features` - Branch picker test suite (17 tests) - `git diff --check` Tested on macOS. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase <img width="560" height="522" alt="Screenshot 2026-07-17 at 11 19 27 PM" src="https://github.com/user-attachments/assets/28d57315-b859-4a8d-86ca-53f9fb513bf5" /> The updated branch picker groups local and remote branches, keeps the create-branch action above those sections, and shows forge-specific icons for remote branches. --- Release Notes: - Improved branch picker filtering, grouping, branch creation suggestions, and remote-provider icons.
This commit is contained in:
1 parent
ced39bbd9f
commit
fdedc0c2b7
3 files changed
+372
-173
No files matched your search
+338
-173
@@ -2,8 +2,9 @@ use anyhow::Context as _;
|
||||
use editor::Editor;
|
||||
use fuzzy_nucleo::StringMatchCandidate;
|
||||
|
||||
use collections::HashSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use git::repository::{Branch, delete_branch_flag};
|
||||
use git::{GitHostingProviderRegistry, parse_git_remote_url};
|
||||
use gpui::http_client::Url;
|
||||
use gpui::{
|
||||
Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global,
|
||||
@@ -277,6 +278,9 @@ impl BranchList {
|
||||
let default_branch_request = repository.clone().map(|repository| {
|
||||
repository.update(cx, |repository, _| repository.default_branch(false))
|
||||
});
|
||||
let remote_urls_request = repository
|
||||
.clone()
|
||||
.map(|repository| repository.update(cx, |repository, _| repository.remote_urls()));
|
||||
|
||||
let mut delegate = BranchListDelegate::new(
|
||||
workspace,
|
||||
@@ -289,7 +293,7 @@ impl BranchList {
|
||||
delegate.branch_list_error = branch_list_error;
|
||||
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::uniform_list(delegate, window, cx)
|
||||
Picker::list(delegate, window, cx)
|
||||
.initial_width(width)
|
||||
.show_scrollbar(true)
|
||||
.when(embedded, |picker| picker.embedded())
|
||||
@@ -350,6 +354,21 @@ impl BranchList {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let remote_urls = remote_urls_request
|
||||
.context("No active repository")?
|
||||
.await??;
|
||||
let remote_provider_icons = cx.update(|cx| remote_provider_icons(&remote_urls, cx));
|
||||
this.update(cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.remote_provider_icons = remote_provider_icons;
|
||||
cx.notify();
|
||||
});
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
picker,
|
||||
picker_focus_handle,
|
||||
@@ -655,6 +674,7 @@ pub struct BranchListDelegate {
|
||||
restore_selected_branch: Option<SharedString>,
|
||||
show_footer: bool,
|
||||
hovered_delete_index: Option<usize>,
|
||||
remote_provider_icons: HashMap<String, IconName>,
|
||||
}
|
||||
|
||||
enum BranchSelectionBehavior {
|
||||
@@ -895,15 +915,25 @@ fn sort_branch_entries(
|
||||
matches: &mut [Entry],
|
||||
branch_selection_context: Option<&BranchSelectionContext>,
|
||||
) {
|
||||
let selected_branch_is_remote = branch_selection_context
|
||||
.and_then(|context| context.selected_branch.as_ref())
|
||||
.and_then(|selected_branch| {
|
||||
matches.iter().find_map(|entry| {
|
||||
let branch = entry.as_branch()?;
|
||||
branch_matches_ref(branch, selected_branch).then(|| branch.is_remote())
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
matches.sort_by_key(|entry| {
|
||||
let Some(branch) = entry.as_branch() else {
|
||||
return (4, false);
|
||||
return (true, 4);
|
||||
};
|
||||
|
||||
let priority = branch_selection_context
|
||||
.map(|context| context.priority(branch))
|
||||
.unwrap_or(0);
|
||||
(priority, branch.is_remote())
|
||||
(branch.is_remote() != selected_branch_is_remote, priority)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -982,6 +1012,7 @@ impl BranchListDelegate {
|
||||
restore_selected_branch,
|
||||
show_footer: false,
|
||||
hovered_delete_index: None,
|
||||
remote_provider_icons: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1180,8 +1211,28 @@ impl BranchListDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_provider_icons(
|
||||
remote_urls: &HashMap<String, String>,
|
||||
cx: &App,
|
||||
) -> HashMap<String, IconName> {
|
||||
let Some(provider_registry) = GitHostingProviderRegistry::try_global(cx) else {
|
||||
return HashMap::default();
|
||||
};
|
||||
|
||||
remote_urls
|
||||
.iter()
|
||||
.filter_map(|(remote_name, remote_url)| {
|
||||
let (provider, _) = parse_git_remote_url(provider_registry.clone(), remote_url)?;
|
||||
Some((
|
||||
remote_name.clone(),
|
||||
crate::get_provider_icon(&provider.name()),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl PickerDelegate for BranchListDelegate {
|
||||
type ListItem = ListItem;
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"branch picker"
|
||||
@@ -1304,7 +1355,12 @@ impl PickerDelegate for BranchListDelegate {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<AnyElement> {
|
||||
if self.branch_filter == BranchFilter::All {
|
||||
if self.branch_filter == BranchFilter::All
|
||||
|| !self
|
||||
.matches
|
||||
.first()
|
||||
.is_some_and(|entry| entry.as_branch().is_some())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1364,10 +1420,10 @@ impl PickerDelegate for BranchListDelegate {
|
||||
|
||||
let mut matches: Vec<Entry> = if query.is_empty() {
|
||||
let mut matches: Vec<Entry> = all_branches
|
||||
.into_iter()
|
||||
.iter()
|
||||
.filter(|branch| branch_matches_filter(branch))
|
||||
.map(|branch| Entry::Branch {
|
||||
branch,
|
||||
branch: branch.clone(),
|
||||
positions: Vec::new(),
|
||||
})
|
||||
.collect();
|
||||
@@ -1426,7 +1482,7 @@ impl PickerDelegate for BranchListDelegate {
|
||||
|
||||
if !picker.delegate.is_select_only()
|
||||
&& !query.is_empty()
|
||||
&& !matches.first().is_some_and(|entry| entry.name() == query)
|
||||
&& !all_branches.iter().any(|branch| branch.name() == query)
|
||||
{
|
||||
let query = normalize_branch_name(&query);
|
||||
let is_url = query.trim_start_matches("git@").parse::<Url>().is_ok();
|
||||
@@ -1618,7 +1674,11 @@ impl PickerDelegate for BranchListDelegate {
|
||||
if is_checked_branch {
|
||||
IconName::Check
|
||||
} else if branch.is_remote() {
|
||||
IconName::Screen
|
||||
branch
|
||||
.remote_name()
|
||||
.and_then(|remote_name| self.remote_provider_icons.get(remote_name))
|
||||
.copied()
|
||||
.unwrap_or(IconName::Server)
|
||||
} else {
|
||||
IconName::GitBranch
|
||||
}
|
||||
@@ -1714,172 +1774,195 @@ impl PickerDelegate for BranchListDelegate {
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
Some(
|
||||
ListItem::new(format!("vcs-menu-{ix}"))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2p5()
|
||||
.flex_grow_1()
|
||||
.child(
|
||||
Icon::new(entry_icon)
|
||||
.color(if is_checked_branch {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("info_container")
|
||||
.w_full()
|
||||
.child(entry_title)
|
||||
.child({
|
||||
let message = match entry {
|
||||
Entry::NewUrl { url } => format!("Based off {url}"),
|
||||
Entry::NewRemoteName { url, .. } => {
|
||||
format!("Based off {url}")
|
||||
let list_item = ListItem::new(format!("vcs-menu-{ix}"))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2p5()
|
||||
.flex_grow_1()
|
||||
.child(
|
||||
Icon::new(entry_icon)
|
||||
.color(if is_checked_branch {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("info_container")
|
||||
.w_full()
|
||||
.child(entry_title)
|
||||
.child({
|
||||
let message = match entry {
|
||||
Entry::NewUrl { url } => format!("Based off {url}"),
|
||||
Entry::NewRemoteName { url, .. } => {
|
||||
format!("Based off {url}")
|
||||
}
|
||||
Entry::NewBranch { .. } => {
|
||||
if let Some(current_branch) =
|
||||
self.repo.as_ref().and_then(|repo| {
|
||||
repo.read(cx).branch.as_ref().map(|b| b.name())
|
||||
})
|
||||
{
|
||||
format!("Based off {}", current_branch)
|
||||
} else {
|
||||
"Based off the current branch".to_string()
|
||||
}
|
||||
Entry::NewBranch { .. } => {
|
||||
if let Some(current_branch) =
|
||||
self.repo.as_ref().and_then(|repo| {
|
||||
repo.read(cx).branch.as_ref().map(|b| b.name())
|
||||
})
|
||||
{
|
||||
format!("Based off {}", current_branch)
|
||||
} else {
|
||||
"Based off the current branch".to_string()
|
||||
}
|
||||
}
|
||||
Entry::Branch { .. } => String::new(),
|
||||
}
|
||||
Entry::Branch { .. } => String::new(),
|
||||
};
|
||||
|
||||
if matches!(entry, Entry::Branch { .. }) {
|
||||
let show_author_name = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.branch_picker
|
||||
.show_author_name;
|
||||
let has_author = show_author_name && author_name.is_some();
|
||||
let has_commit = commit_time.is_some();
|
||||
let author_for_meta =
|
||||
if show_author_name { author_name } else { None };
|
||||
|
||||
let dot = || {
|
||||
Label::new("•")
|
||||
.alpha(0.5)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
};
|
||||
|
||||
if matches!(entry, Entry::Branch { .. }) {
|
||||
let show_author_name = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.branch_picker
|
||||
.show_author_name;
|
||||
let has_author = show_author_name && author_name.is_some();
|
||||
let has_commit = commit_time.is_some();
|
||||
let author_for_meta =
|
||||
if show_author_name { author_name } else { None };
|
||||
|
||||
let dot = || {
|
||||
Label::new("•")
|
||||
.alpha(0.5)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.gap_1p5()
|
||||
.when_some(author_for_meta, |this, author| {
|
||||
this.child(
|
||||
Label::new(author)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when_some(commit_time, |this, time| {
|
||||
this.when(has_author, |this| this.child(dot()))
|
||||
.child(
|
||||
Label::new(time)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when_some(subject, |this, subj| {
|
||||
this.when(has_commit, |this| this.child(dot()))
|
||||
.child(
|
||||
Label::new(subj.to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.flex_1(),
|
||||
)
|
||||
})
|
||||
.when(!has_commit, |this| {
|
||||
this.child(
|
||||
Label::new("No commits found")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(message)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
entry.as_branch().map(|b| b.name().to_string()),
|
||||
|this, branch_name| {
|
||||
let absolute_time = absolute_time.clone();
|
||||
this.tooltip({
|
||||
let is_head = is_head_branch;
|
||||
let is_checked = is_checked_branch;
|
||||
let is_select_only = self.is_select_only();
|
||||
Tooltip::element(move |_, _| {
|
||||
v_flex()
|
||||
.child(Label::new(branch_name.clone()))
|
||||
.when(is_select_only && is_checked, |this| {
|
||||
this.child(
|
||||
Label::new("Selected Branch")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when(is_head, |this| {
|
||||
this.child(
|
||||
Label::new("Current Branch")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when_some(
|
||||
absolute_time.clone(),
|
||||
|this, time| {
|
||||
this.child(
|
||||
Label::new(time)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
h_flex()
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.gap_1p5()
|
||||
.when_some(author_for_meta, |this, author| {
|
||||
this.child(
|
||||
Label::new(author)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(
|
||||
!self.is_select_only() && !is_new_items && !is_head_branch,
|
||||
|this| {
|
||||
this.end_slot(deleted_branch_icon(ix))
|
||||
.show_end_slot_on_hover()
|
||||
},
|
||||
)
|
||||
.when_some(
|
||||
if is_new_items {
|
||||
create_from_default_button
|
||||
.when_some(commit_time, |this, time| {
|
||||
this.when(has_author, |this| this.child(dot())).child(
|
||||
Label::new(time)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when_some(subject, |this, subj| {
|
||||
this.when(has_commit, |this| this.child(dot())).child(
|
||||
Label::new(subj.to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.flex_1(),
|
||||
)
|
||||
})
|
||||
.when(!has_commit, |this| {
|
||||
this.child(
|
||||
Label::new("No commits found")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(message)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
entry.as_branch().map(|b| b.name().to_string()),
|
||||
|this, branch_name| {
|
||||
let absolute_time = absolute_time.clone();
|
||||
this.tooltip({
|
||||
let is_head = is_head_branch;
|
||||
let is_checked = is_checked_branch;
|
||||
let is_select_only = self.is_select_only();
|
||||
Tooltip::element(move |_, _| {
|
||||
v_flex()
|
||||
.child(Label::new(branch_name.clone()))
|
||||
.when(is_select_only && is_checked, |this| {
|
||||
this.child(
|
||||
Label::new("Selected Branch")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when(is_head, |this| {
|
||||
this.child(
|
||||
Label::new("Current Branch")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when_some(absolute_time.clone(), |this, time| {
|
||||
this.child(
|
||||
Label::new(time)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(
|
||||
!self.is_select_only() && !is_new_items && !is_head_branch,
|
||||
|this| {
|
||||
this.end_slot(deleted_branch_icon(ix))
|
||||
.show_end_slot_on_hover()
|
||||
},
|
||||
)
|
||||
.when_some(
|
||||
if is_new_items {
|
||||
create_from_default_button
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|this, create_from_default_button| {
|
||||
this.end_slot(create_from_default_button)
|
||||
.show_end_slot_on_hover()
|
||||
},
|
||||
);
|
||||
|
||||
let section_header = (self.branch_filter == BranchFilter::All)
|
||||
.then(|| entry.as_branch())
|
||||
.flatten()
|
||||
.and_then(|branch| {
|
||||
let starts_section = ix == 0
|
||||
|| self.matches[ix - 1]
|
||||
.as_branch()
|
||||
.is_none_or(|previous_branch| {
|
||||
previous_branch.is_remote() != branch.is_remote()
|
||||
});
|
||||
starts_section.then(|| {
|
||||
if branch.is_remote() {
|
||||
"Remote Branches"
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|this, create_from_default_button| {
|
||||
this.end_slot(create_from_default_button)
|
||||
.show_end_slot_on_hover()
|
||||
},
|
||||
),
|
||||
"Local Branches"
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.when_some(section_header, |this, section_header| {
|
||||
this.pt_1p5()
|
||||
.child(ListSubHeader::new(section_header).inset(true))
|
||||
})
|
||||
.child(list_item)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2140,6 +2223,7 @@ mod tests {
|
||||
),
|
||||
create_test_branch("main", false, Some("origin"), Some(1000)),
|
||||
create_test_branch("feature", false, Some("origin"), Some(900)),
|
||||
create_test_branch("topic", false, Some("origin"), Some(850)),
|
||||
create_test_branch("main", false, Some("fork"), Some(800)),
|
||||
]);
|
||||
|
||||
@@ -2183,11 +2267,24 @@ mod tests {
|
||||
let ordered_branch_names = entries.iter().map(Entry::name).collect::<Vec<_>>();
|
||||
assert_eq!(ordered_branch_names.first(), Some(&"origin/main"));
|
||||
assert!(
|
||||
ordered_branch_names.iter().position(|name| *name == "main")
|
||||
entries
|
||||
.windows(2)
|
||||
.filter(|entries| {
|
||||
entries[0].as_branch().map(Branch::is_remote)
|
||||
!= entries[1].as_branch().map(Branch::is_remote)
|
||||
})
|
||||
.count()
|
||||
<= 1,
|
||||
"local and remote branches should each form a contiguous section"
|
||||
);
|
||||
assert!(
|
||||
ordered_branch_names
|
||||
.iter()
|
||||
.position(|name| *name == "origin/topic")
|
||||
< ordered_branch_names
|
||||
.iter()
|
||||
.position(|name| *name == "fork/main"),
|
||||
"branches on the active branch's remote should be prioritized"
|
||||
"branches on the active branch's remote should be prioritized within their section"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2317,6 +2414,74 @@ mod tests {
|
||||
assert!(last_match.is_new_branch());
|
||||
})
|
||||
});
|
||||
|
||||
for branch_filter in [BranchFilter::Local, BranchFilter::Remote] {
|
||||
branch_list
|
||||
.update_in(cx, |branch_list, window, cx| {
|
||||
branch_list.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.branch_filter = branch_filter;
|
||||
picker
|
||||
.delegate
|
||||
.update_matches(String::from("missing-branch"), window, cx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
branch_list.update_in(cx, |branch_list, window, cx| {
|
||||
branch_list.picker.update(cx, |picker, cx| {
|
||||
assert!(
|
||||
picker.delegate.render_header(window, cx).is_none(),
|
||||
"create-only results should not be shown under a branch section header"
|
||||
);
|
||||
assert!(matches!(
|
||||
picker.delegate.matches.as_slice(),
|
||||
[Entry::NewBranch { .. }]
|
||||
));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
branch_list
|
||||
.update_in(cx, |branch_list, window, cx| {
|
||||
branch_list.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.branch_filter = BranchFilter::Remote;
|
||||
picker
|
||||
.delegate
|
||||
.update_matches(String::from("feature-ui"), window, cx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
branch_list.update(cx, |branch_list, cx| {
|
||||
branch_list.picker.update(cx, |picker, _cx| {
|
||||
assert!(
|
||||
picker.delegate.matches.is_empty(),
|
||||
"an exact branch hidden by the active filter should not be offered for creation"
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
branch_list
|
||||
.update_in(cx, |branch_list, window, cx| {
|
||||
branch_list.picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.update_matches(String::from("feature-u"), window, cx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
branch_list.update(cx, |branch_list, cx| {
|
||||
branch_list.picker.update(cx, |picker, _cx| {
|
||||
assert!(matches!(
|
||||
picker.delegate.matches.as_slice(),
|
||||
[Entry::NewBranch { .. }]
|
||||
));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async fn update_branch_list_matches_with_empty_query(
|
||||
|
||||
@@ -3150,12 +3150,18 @@ impl GitStore {
|
||||
repository_handle.get_remotes(branch_name, is_push)
|
||||
})
|
||||
.await??;
|
||||
let remote_urls = repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.remote_urls()
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(proto::GetRemotesResponse {
|
||||
remotes: remotes
|
||||
.into_iter()
|
||||
.map(|remotes| proto::get_remotes_response::Remote {
|
||||
name: remotes.name.to_string(),
|
||||
url: remote_urls.get(remotes.name.as_ref()).cloned(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
@@ -7949,6 +7955,33 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote_urls(&mut self) -> oneshot::Receiver<Result<HashMap<String, String>>> {
|
||||
let id = self.id;
|
||||
self.send_job("remote_urls", None, move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
||||
Ok(backend.remote_urls().await)
|
||||
}
|
||||
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
||||
let response = client
|
||||
.request(proto::GetRemotes {
|
||||
project_id: project_id.0,
|
||||
repository_id: id.to_proto(),
|
||||
branch_name: None,
|
||||
is_push: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(response
|
||||
.remotes
|
||||
.into_iter()
|
||||
.filter_map(|remote| Some((remote.name, remote.url?)))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn branches(&mut self) -> oneshot::Receiver<Result<BranchesScanResult>> {
|
||||
let id = self.id;
|
||||
self.send_job("branches", None, move |repo, _| async move {
|
||||
|
||||
@@ -459,6 +459,7 @@ message GetRemotesResponse {
|
||||
|
||||
message Remote {
|
||||
string name = 1;
|
||||
optional string url = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user