cherry-pick: #54575 to preview (#54577)

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 is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- git: Fix remote branch picker
This commit is contained in:
Anthony Eid 2026-04-22 20:58:10 -04:00 committed by GitHub
parent 4ec3a6db1b
commit a10a5f3a21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 156 additions and 0 deletions

View file

@ -886,6 +886,7 @@ impl Database {
current_merge_conflicts,
branch_summary,
head_commit_details,
branch_list: Vec::new(),
scan_id: db_repository_entry.scan_id as u64,
is_last_update: true,
merge_message: db_repository_entry.merge_message,

View file

@ -790,6 +790,7 @@ impl Database {
current_merge_conflicts,
branch_summary,
head_commit_details,
branch_list: Vec::new(),
project_id: project_id.to_proto(),
id: db_repository.id as u64,
abs_path: db_repository.abs_path.clone(),

View file

@ -91,6 +91,29 @@ fn collect_diff_stats<C: gpui::AppContext>(
})
}
fn branch_list_snapshot(
project: &gpui::Entity<project::Project>,
cx: &mut TestAppContext,
) -> (Option<String>, Vec<String>) {
project.read_with(cx, |project, cx| {
let repos = project.repositories(cx);
assert_eq!(repos.len(), 1, "project should have exactly 1 repository");
let repo = repos.values().next().unwrap();
let snapshot = repo.read(cx).snapshot();
(
snapshot
.branch
.as_ref()
.map(|branch| branch.name().to_string()),
snapshot
.branch_list
.iter()
.map(|branch| branch.ref_name.to_string())
.collect(),
)
})
}
#[gpui::test]
async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.background_executor.clone()).await;
@ -480,6 +503,95 @@ async fn test_remote_git_head_sha(
assert_eq!(remote_head_sha.unwrap(), local_head_sha);
}
#[gpui::test]
async fn test_branch_list_sync(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs()
.insert_tree(
path!("/project"),
json!({ ".git": {}, "file.txt": "content" }),
)
.await;
client_a.fs().insert_branches(
Path::new(path!("/project/.git")),
&["main", "feature-1", "feature-2"],
);
let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
executor.run_until_parked();
let host_snapshot = branch_list_snapshot(&project_a, cx_a);
assert_eq!(host_snapshot.0.as_deref(), Some("main"));
assert_eq!(
host_snapshot.1,
vec![
"refs/heads/feature-1".to_string(),
"refs/heads/feature-2".to_string(),
"refs/heads/main".to_string(),
]
);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
executor.run_until_parked();
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
cx_b.update(|cx| {
repo_b.update(cx, |repository, _cx| {
repository.create_branch("totally-new-branch".to_string(), None)
})
})
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b.update(cx, |repository, _cx| {
repository.change_branch("totally-new-branch".to_string())
})
})
.await
.unwrap()
.unwrap();
executor.run_until_parked();
let host_snapshot_after_update = branch_list_snapshot(&project_a, cx_a);
assert_eq!(
host_snapshot_after_update.0.as_deref(),
Some("totally-new-branch")
);
assert_eq!(
host_snapshot_after_update.1,
vec![
"refs/heads/feature-1".to_string(),
"refs/heads/feature-2".to_string(),
"refs/heads/main".to_string(),
"refs/heads/totally-new-branch".to_string(),
]
);
let guest_snapshot_after_update = branch_list_snapshot(&project_b, cx_b);
assert_eq!(guest_snapshot_after_update, host_snapshot_after_update);
}
#[gpui::test]
async fn test_linked_worktrees_sync(
executor: BackgroundExecutor,

View file

@ -3856,6 +3856,7 @@ impl RepositorySnapshot {
fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
proto::UpdateRepository {
branch_summary: self.branch.as_ref().map(branch_to_proto),
branch_list: self.branch_list.iter().map(branch_to_proto).collect(),
head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
updated_statuses: self
.statuses_by_path
@ -3941,6 +3942,7 @@ impl RepositorySnapshot {
proto::UpdateRepository {
branch_summary: self.branch.as_ref().map(branch_to_proto),
branch_list: self.branch_list.iter().map(branch_to_proto).collect(),
head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
updated_statuses,
removed_statuses,
@ -6785,6 +6787,15 @@ impl Repository {
self.snapshot.branch = new_branch;
self.snapshot.head_commit = new_head_commit;
if update.is_last_update {
let new_branch_list: Arc<[Branch]> =
update.branch_list.iter().map(proto_to_branch).collect();
if *self.snapshot.branch_list != *new_branch_list {
cx.emit(RepositoryEvent::BranchListChanged);
}
self.snapshot.branch_list = new_branch_list;
}
// We don't store any merge head state for downstream projects; the upstream
// will track it and we will just get the updated conflicts
let new_merge_heads = TreeMap::from_ordered_entries(

View file

@ -127,6 +127,7 @@ message UpdateRepository {
optional string remote_origin_url = 15;
optional string original_repo_abs_path = 16;
repeated Worktree linked_worktrees = 17;
repeated Branch branch_list = 18;
}
message RemoveRepository {

View file

@ -917,6 +917,7 @@ pub fn split_repository_update(
) -> impl Iterator<Item = UpdateRepository> {
let mut updated_statuses_iter = mem::take(&mut update.updated_statuses).into_iter().fuse();
let mut removed_statuses_iter = mem::take(&mut update.removed_statuses).into_iter().fuse();
let branch_list = mem::take(&mut update.branch_list);
std::iter::from_fn({
let update = update.clone();
move || {
@ -934,6 +935,7 @@ pub fn split_repository_update(
Some(UpdateRepository {
updated_statuses,
removed_statuses,
branch_list: Vec::new(),
is_last_update: false,
..update.clone()
})
@ -942,6 +944,7 @@ pub fn split_repository_update(
.chain([UpdateRepository {
updated_statuses: Vec::new(),
removed_statuses: Vec::new(),
branch_list,
is_last_update: true,
..update
}])
@ -999,4 +1002,31 @@ mod tests {
};
assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
}
#[test]
fn test_split_repository_update_keeps_branch_list_on_final_chunk() {
let update = UpdateRepository {
updated_statuses: vec![
StatusEntry::default(),
StatusEntry::default(),
StatusEntry::default(),
],
branch_list: vec![Branch {
ref_name: "refs/heads/main".into(),
..Default::default()
}],
..Default::default()
};
let chunks = split_repository_update(update).collect::<Vec<_>>();
assert_eq!(chunks.len(), 3);
assert!(chunks[0].branch_list.is_empty());
assert!(chunks[1].branch_list.is_empty());
assert_eq!(chunks[2].branch_list.len(), 1);
assert_eq!(chunks[2].branch_list[0].ref_name, "refs/heads/main");
assert!(!chunks[0].is_last_update);
assert!(!chunks[1].is_last_update);
assert!(chunks[2].is_last_update);
}
}