Fix git branches in non-active repository (#26148)

Mikayla Maki , Conrad Irwin , and Richard Feldman created

Release Notes:

- Git Beta: Fixed a bug where the branch selector would only show for
the first repository opened.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>

Change summary

crates/collab/src/rpc.rs                                      |   4 
crates/collab/src/tests/integration_tests.rs                  |  38 
crates/collab/src/tests/remote_editing_collaboration_tests.rs |  31 
crates/git_ui/src/branch_picker.rs                            |  95 +
crates/git_ui/src/commit_modal.rs                             |   8 
crates/git_ui/src/git_panel.rs                                |   7 
crates/project/src/git.rs                                     | 144 +++
crates/project/src/project.rs                                 |  17 
crates/project/src/worktree_store.rs                          | 199 ----
crates/proto/proto/zed.proto                                  |  28 
crates/proto/src/proto.rs                                     |  12 
crates/remote_server/src/remote_editing_tests.rs              |  34 
12 files changed, 317 insertions(+), 300 deletions(-)

Detailed changes

crates/collab/src/rpc.rs 🔗

@@ -308,7 +308,7 @@ impl Server {
             .add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
             .add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
-            .add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
+            .add_request_handler(forward_read_only_project_request::<proto::GitGetBranches>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedDiff>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
             .add_request_handler(
@@ -405,6 +405,8 @@ impl Server {
             .add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
             .add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
             .add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
+            .add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
+            .add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
             .add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
             .add_message_handler(update_context)
             .add_request_handler({

crates/collab/src/tests/integration_tests.rs 🔗

@@ -6741,19 +6741,24 @@ async fn test_remote_git_branches(
         .collect::<HashSet<_>>();
 
     let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
+
     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;
 
-    let root_path = ProjectPath::root_path(worktree_id);
-    // Client A sees that a guest has joined.
+    // Client A sees that a guest has joined and the repo has been populated
     executor.run_until_parked();
 
+    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
+
+    let root_path = ProjectPath::root_path(worktree_id);
+
     let branches_b = cx_b
-        .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
+        .update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
         .await
+        .unwrap()
         .unwrap();
 
     let new_branch = branches[2];
@@ -6765,13 +6770,10 @@ async fn test_remote_git_branches(
 
     assert_eq!(branches_b, branches_set);
 
-    cx_b.update(|cx| {
-        project_b.update(cx, |project, cx| {
-            project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
-        })
-    })
-    .await
-    .unwrap();
+    cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
+        .await
+        .unwrap()
+        .unwrap();
 
     executor.run_until_parked();
 
@@ -6789,11 +6791,21 @@ async fn test_remote_git_branches(
 
     // Also try creating a new branch
     cx_b.update(|cx| {
-        project_b.update(cx, |project, cx| {
-            project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
-        })
+        repo_b
+            .read(cx)
+            .create_branch("totally-new-branch".to_string())
+    })
+    .await
+    .unwrap()
+    .unwrap();
+
+    cx_b.update(|cx| {
+        repo_b
+            .read(cx)
+            .change_branch("totally-new-branch".to_string())
     })
     .await
+    .unwrap()
     .unwrap();
 
     executor.run_until_parked();

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -276,11 +276,13 @@ async fn test_ssh_collaboration_git_branches(
     // has some git repositories
     executor.run_until_parked();
 
+    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
     let root_path = ProjectPath::root_path(worktree_id);
 
     let branches_b = cx_b
-        .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
+        .update(|cx| repo_b.read(cx).branches())
         .await
+        .unwrap()
         .unwrap();
 
     let new_branch = branches[2];
@@ -292,13 +294,10 @@ async fn test_ssh_collaboration_git_branches(
 
     assert_eq!(&branches_b, &branches_set);
 
-    cx_b.update(|cx| {
-        project_b.update(cx, |project, cx| {
-            project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
-        })
-    })
-    .await
-    .unwrap();
+    cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
+        .await
+        .unwrap()
+        .unwrap();
 
     executor.run_until_parked();
 
@@ -318,11 +317,21 @@ async fn test_ssh_collaboration_git_branches(
 
     // Also try creating a new branch
     cx_b.update(|cx| {
-        project_b.update(cx, |project, cx| {
-            project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
-        })
+        repo_b
+            .read(cx)
+            .create_branch("totally-new-branch".to_string())
+    })
+    .await
+    .unwrap()
+    .unwrap();
+
+    cx_b.update(|cx| {
+        repo_b
+            .read(cx)
+            .change_branch("totally-new-branch".to_string())
     })
     .await
+    .unwrap()
     .unwrap();
 
     executor.run_until_parked();

crates/git_ui/src/branch_picker.rs 🔗

@@ -1,4 +1,4 @@
-use anyhow::Context as _;
+use anyhow::{anyhow, Context as _};
 use fuzzy::{StringMatch, StringMatchCandidate};
 
 use git::repository::Branch;
@@ -8,7 +8,7 @@ use gpui::{
     Task, Window,
 };
 use picker::{Picker, PickerDelegate};
-use project::{Project, ProjectPath};
+use project::git::Repository;
 use std::sync::Arc;
 use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
 use util::ResultExt;
@@ -28,16 +28,20 @@ pub fn open(
     window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
-    let project = workspace.project().clone();
+    let repository = workspace.project().read(cx).active_repository(cx).clone();
     let style = BranchListStyle::Modal;
     workspace.toggle_modal(window, cx, |window, cx| {
-        BranchList::new(project, style, 34., window, cx)
+        BranchList::new(repository, style, 34., window, cx)
     })
 }
 
-pub fn popover(project: Entity<Project>, window: &mut Window, cx: &mut App) -> Entity<BranchList> {
+pub fn popover(
+    repository: Option<Entity<Repository>>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Entity<BranchList> {
     cx.new(|cx| {
-        let list = BranchList::new(project, BranchListStyle::Popover, 15., window, cx);
+        let list = BranchList::new(repository, BranchListStyle::Popover, 15., window, cx);
         list.focus_handle(cx).focus(window);
         list
     })
@@ -58,22 +62,21 @@ pub struct BranchList {
 
 impl BranchList {
     fn new(
-        project_handle: Entity<Project>,
+        repository: Option<Entity<Repository>>,
         style: BranchListStyle,
         rem_width: f32,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         let popover_handle = PopoverMenuHandle::default();
-        let project = project_handle.read(cx);
-        let all_branches_request = project
-            .visible_worktrees(cx)
-            .next()
-            .map(|worktree| project.branches(ProjectPath::root_path(worktree.read(cx).id()), cx))
-            .context("No worktrees found");
+        let all_branches_request = repository
+            .clone()
+            .map(|repository| repository.read(cx).branches());
 
         cx.spawn_in(window, |this, mut cx| async move {
-            let all_branches = all_branches_request?.await?;
+            let all_branches = all_branches_request
+                .context("No active repository")?
+                .await??;
 
             this.update_in(&mut cx, |this, window, cx| {
                 this.picker.update(cx, |picker, cx| {
@@ -86,7 +89,7 @@ impl BranchList {
         })
         .detach_and_log_err(cx);
 
-        let delegate = BranchListDelegate::new(project_handle.clone(), style, 20);
+        let delegate = BranchListDelegate::new(repository.clone(), style, 20);
         let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 
         let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
@@ -145,7 +148,7 @@ impl BranchEntry {
 pub struct BranchListDelegate {
     matches: Vec<BranchEntry>,
     all_branches: Option<Vec<Branch>>,
-    project: Entity<Project>,
+    repo: Option<Entity<Repository>>,
     style: BranchListStyle,
     selected_index: usize,
     last_query: String,
@@ -155,13 +158,13 @@ pub struct BranchListDelegate {
 
 impl BranchListDelegate {
     fn new(
-        project: Entity<Project>,
+        repo: Option<Entity<Repository>>,
         style: BranchListStyle,
         branch_name_trailoff_after: usize,
     ) -> Self {
         Self {
             matches: vec![],
-            project,
+            repo,
             style,
             all_branches: None,
             selected_index: 0,
@@ -280,14 +283,16 @@ impl PickerDelegate for BranchListDelegate {
             return;
         };
 
-        let current_branch = self.project.update(cx, |project, cx| {
-            project
-                .active_repository(cx)
-                .and_then(|repo| repo.read(cx).current_branch())
-                .map(|branch| branch.name.to_string())
+        let current_branch = self.repo.as_ref().map(|repo| {
+            repo.update(cx, |repo, _| {
+                repo.current_branch().map(|branch| branch.name.clone())
+            })
         });
 
-        if current_branch == Some(branch.name().to_string()) {
+        if current_branch
+            .flatten()
+            .is_some_and(|current_branch| current_branch == branch.name())
+        {
             cx.emit(DismissEvent);
             return;
         }
@@ -296,19 +301,33 @@ impl PickerDelegate for BranchListDelegate {
             let branch = branch.clone();
             |picker, mut cx| async move {
                 let branch_change_task = picker.update(&mut cx, |this, cx| {
-                    let project = this.delegate.project.read(cx);
-                    let branch_to_checkout = match branch {
-                        BranchEntry::Branch(branch) => branch.string,
-                        BranchEntry::History(string) => string,
-                        BranchEntry::NewBranch { name: branch_name } => branch_name,
-                    };
-                    let worktree = project
-                        .visible_worktrees(cx)
-                        .next()
-                        .context("worktree disappeared")?;
-                    let repository = ProjectPath::root_path(worktree.read(cx).id());
-
-                    anyhow::Ok(project.update_or_create_branch(repository, branch_to_checkout, cx))
+                    let repo = this
+                        .delegate
+                        .repo
+                        .as_ref()
+                        .ok_or_else(|| anyhow!("No active repository"))?
+                        .clone();
+
+                    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?
+                            }
+                            BranchEntry::NewBranch { name: branch_name } => {
+                                cx.update(|cx| repo.read(cx).create_branch(branch_name.clone()))?
+                                    .await??;
+                                cx.update(|cx| repo.read(cx).change_branch(branch_name))?
+                                    .await?
+                            }
+                        }
+                    })
                 })??;
 
                 branch_change_task.await?;
@@ -316,7 +335,7 @@ impl PickerDelegate for BranchListDelegate {
                 picker.update(&mut cx, |_, cx| {
                     cx.emit(DismissEvent);
 
-                    Ok::<(), anyhow::Error>(())
+                    anyhow::Ok(())
                 })
             }
         })

crates/git_ui/src/commit_modal.rs 🔗

@@ -4,7 +4,6 @@ use crate::branch_picker::{self, BranchList};
 use crate::git_panel::{commit_message_editor, GitPanel};
 use git::{Commit, ShowCommitEditor};
 use panel::{panel_button, panel_editor_style, panel_filled_button};
-use project::Project;
 use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
 
 use editor::{Editor, EditorElement};
@@ -129,10 +128,9 @@ impl CommitModal {
                 active_index,
             };
 
-            let project = workspace.project().clone();
             workspace.open_panel::<GitPanel>(window, cx);
             workspace.toggle_modal(window, cx, move |window, cx| {
-                CommitModal::new(git_panel, restore_dock_position, project, window, cx)
+                CommitModal::new(git_panel, restore_dock_position, window, cx)
             })
         });
     }
@@ -140,11 +138,11 @@ impl CommitModal {
     fn new(
         git_panel: Entity<GitPanel>,
         restore_dock: RestoreDock,
-        project: Entity<Project>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         let panel = git_panel.read(cx);
+        let active_repository = panel.active_repository.clone();
         let suggested_commit_message = panel.suggest_commit_message();
 
         let commit_editor = git_panel.update(cx, |git_panel, cx| {
@@ -188,7 +186,7 @@ impl CommitModal {
         let properties = ModalContainerProperties::new(window, 50);
 
         Self {
-            branch_list: branch_picker::popover(project.clone(), window, cx),
+            branch_list: branch_picker::popover(active_repository.clone(), window, cx),
             git_panel,
             commit_editor,
             restore_dock,

crates/git_ui/src/git_panel.rs 🔗

@@ -3327,6 +3327,11 @@ impl RenderOnce for PanelRepoFooter {
             .as_ref()
             .map(|panel| panel.read(cx).project.clone());
 
+        let repo = self
+            .git_panel
+            .as_ref()
+            .and_then(|panel| panel.read(cx).active_repository.clone());
+
         let single_repo = project
             .as_ref()
             .map(|project| project.read(cx).all_repositories(cx).len() == 1)
@@ -3366,7 +3371,7 @@ impl RenderOnce for PanelRepoFooter {
             });
 
         let branch_selector = PopoverMenu::new("popover-button")
-            .menu(move |window, cx| Some(branch_picker::popover(project.clone()?, window, cx)))
+            .menu(move |window, cx| Some(branch_picker::popover(repo.clone(), window, cx)))
             .trigger_with_tooltip(
                 branch_selector_button,
                 Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),

crates/project/src/git.rs 🔗

@@ -96,6 +96,9 @@ impl GitStore {
 
     pub fn init(client: &AnyProtoClient) {
         client.add_entity_request_handler(Self::handle_get_remotes);
+        client.add_entity_request_handler(Self::handle_get_branches);
+        client.add_entity_request_handler(Self::handle_change_branch);
+        client.add_entity_request_handler(Self::handle_create_branch);
         client.add_entity_request_handler(Self::handle_push);
         client.add_entity_request_handler(Self::handle_pull);
         client.add_entity_request_handler(Self::handle_fetch);
@@ -459,6 +462,67 @@ impl GitStore {
         })
     }
 
+    async fn handle_get_branches(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitGetBranches>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::GitBranchesResponse> {
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+        let branches = repository_handle
+            .update(&mut cx, |repository_handle, _| repository_handle.branches())?
+            .await??;
+
+        Ok(proto::GitBranchesResponse {
+            branches: branches
+                .into_iter()
+                .map(|branch| worktree::branch_to_proto(&branch))
+                .collect::<Vec<_>>(),
+        })
+    }
+    async fn handle_create_branch(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitCreateBranch>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+        let branch_name = envelope.payload.branch_name;
+
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.create_branch(branch_name)
+            })?
+            .await??;
+
+        Ok(proto::Ack {})
+    }
+
+    async fn handle_change_branch(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitChangeBranch>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+        let branch_name = envelope.payload.branch_name;
+
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.change_branch(branch_name)
+            })?
+            .await??;
+
+        Ok(proto::Ack {})
+    }
+
     async fn handle_show(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::GitShow>,
@@ -1279,4 +1343,84 @@ impl Repository {
             }
         })
     }
+
+    pub fn branches(&self) -> oneshot::Receiver<Result<Vec<Branch>>> {
+        self.send_job(|repo| async move {
+            match repo {
+                GitRepo::Local(git_repository) => git_repository.branches(),
+                GitRepo::Remote {
+                    project_id,
+                    client,
+                    worktree_id,
+                    work_directory_id,
+                } => {
+                    let response = client
+                        .request(proto::GitGetBranches {
+                            project_id: project_id.0,
+                            worktree_id: worktree_id.to_proto(),
+                            work_directory_id: work_directory_id.to_proto(),
+                        })
+                        .await?;
+
+                    let branches = response
+                        .branches
+                        .into_iter()
+                        .map(|branch| worktree::proto_to_branch(&branch))
+                        .collect();
+
+                    Ok(branches)
+                }
+            }
+        })
+    }
+
+    pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
+        self.send_job(|repo| async move {
+            match repo {
+                GitRepo::Local(git_repository) => git_repository.create_branch(&branch_name),
+                GitRepo::Remote {
+                    project_id,
+                    client,
+                    worktree_id,
+                    work_directory_id,
+                } => {
+                    client
+                        .request(proto::GitCreateBranch {
+                            project_id: project_id.0,
+                            worktree_id: worktree_id.to_proto(),
+                            work_directory_id: work_directory_id.to_proto(),
+                            branch_name,
+                        })
+                        .await?;
+
+                    Ok(())
+                }
+            }
+        })
+    }
+
+    pub fn change_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
+        self.send_job(|repo| async move {
+            match repo {
+                GitRepo::Local(git_repository) => git_repository.change_branch(&branch_name),
+                GitRepo::Remote {
+                    project_id,
+                    client,
+                    worktree_id,
+                    work_directory_id,
+                } => {
+                    client
+                        .request(proto::GitChangeBranch {
+                            project_id: project_id.0,
+                            worktree_id: worktree_id.to_proto(),
+                            work_directory_id: work_directory_id.to_proto(),
+                            branch_name,
+                        })
+                        .await?;
+
+                    Ok(())
+                }
+            }
+        })
+    }
 }

crates/project/src/project.rs 🔗

@@ -48,7 +48,7 @@ use image_store::{ImageItemEvent, ImageStoreEvent};
 
 use ::git::{
     blame::Blame,
-    repository::{Branch, GitRepository, RepoPath},
+    repository::{GitRepository, RepoPath},
     status::FileStatus,
 };
 use gpui::{
@@ -3705,21 +3705,6 @@ impl Project {
         worktree.get_local_repo(&root_entry)?.repo().clone().into()
     }
 
-    pub fn branches(&self, project_path: ProjectPath, cx: &App) -> Task<Result<Vec<Branch>>> {
-        self.worktree_store().read(cx).branches(project_path, cx)
-    }
-
-    pub fn update_or_create_branch(
-        &self,
-        repository: ProjectPath,
-        new_branch: String,
-        cx: &App,
-    ) -> Task<Result<()>> {
-        self.worktree_store()
-            .read(cx)
-            .update_or_create_branch(repository, new_branch, cx)
-    }
-
     pub fn blame_buffer(
         &self,
         buffer: &Entity<Buffer>,

crates/project/src/worktree_store.rs 🔗

@@ -27,10 +27,7 @@ use smol::{
 };
 use text::ReplicaId;
 use util::{paths::SanitizedPath, ResultExt};
-use worktree::{
-    branch_to_proto, Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId,
-    WorktreeSettings,
-};
+use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings};
 
 use crate::{search::SearchQuery, ProjectPath};
 
@@ -83,8 +80,6 @@ impl WorktreeStore {
         client.add_entity_request_handler(Self::handle_delete_project_entry);
         client.add_entity_request_handler(Self::handle_expand_project_entry);
         client.add_entity_request_handler(Self::handle_expand_all_for_project_entry);
-        client.add_entity_request_handler(Self::handle_git_branches);
-        client.add_entity_request_handler(Self::handle_update_branch);
     }
 
     pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
@@ -890,150 +885,6 @@ impl WorktreeStore {
         Ok(())
     }
 
-    pub fn branches(
-        &self,
-        project_path: ProjectPath,
-        cx: &App,
-    ) -> Task<Result<Vec<git::repository::Branch>>> {
-        let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
-            return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
-        };
-
-        match worktree.read(cx) {
-            Worktree::Local(local_worktree) => {
-                let branches = util::maybe!({
-                    let worktree_error = |error| {
-                        format!(
-                            "{} for worktree {}",
-                            error,
-                            local_worktree.abs_path().to_string_lossy()
-                        )
-                    };
-
-                    let entry = local_worktree
-                        .git_entry(project_path.path)
-                        .with_context(|| worktree_error("No git entry found"))?;
-
-                    let repo = local_worktree
-                        .get_local_repo(&entry)
-                        .with_context(|| worktree_error("No repository found"))?
-                        .repo()
-                        .clone();
-
-                    repo.branches()
-                });
-
-                Task::ready(branches)
-            }
-            Worktree::Remote(remote_worktree) => {
-                let request = remote_worktree.client().request(proto::GitBranches {
-                    project_id: remote_worktree.project_id(),
-                    repository: Some(proto::ProjectPath {
-                        worktree_id: project_path.worktree_id.to_proto(),
-                        path: project_path.path.to_proto(), // Root path
-                    }),
-                });
-
-                cx.background_spawn(async move {
-                    let response = request.await?;
-
-                    let branches = response
-                        .branches
-                        .into_iter()
-                        .map(|proto_branch| git::repository::Branch {
-                            is_head: proto_branch.is_head,
-                            name: proto_branch.name.into(),
-                            upstream: proto_branch.upstream.map(|upstream| {
-                                git::repository::Upstream {
-                                    ref_name: upstream.ref_name.into(),
-                                    tracking: upstream
-                                        .tracking
-                                        .map(|tracking| {
-                                            git::repository::UpstreamTracking::Tracked(
-                                                git::repository::UpstreamTrackingStatus {
-                                                    ahead: tracking.ahead as u32,
-                                                    behind: tracking.behind as u32,
-                                                },
-                                            )
-                                        })
-                                        .unwrap_or(git::repository::UpstreamTracking::Gone),
-                                }
-                            }),
-                            most_recent_commit: proto_branch.most_recent_commit.map(|commit| {
-                                git::repository::CommitSummary {
-                                    sha: commit.sha.into(),
-                                    subject: commit.subject.into(),
-                                    commit_timestamp: commit.commit_timestamp,
-                                }
-                            }),
-                        })
-                        .collect();
-
-                    Ok(branches)
-                })
-            }
-        }
-    }
-
-    pub fn update_or_create_branch(
-        &self,
-        repository: ProjectPath,
-        new_branch: String,
-        cx: &App,
-    ) -> Task<Result<()>> {
-        let Some(worktree) = self.worktree_for_id(repository.worktree_id, cx) else {
-            return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
-        };
-
-        match worktree.read(cx) {
-            Worktree::Local(local_worktree) => {
-                let result = util::maybe!({
-                    let worktree_error = |error| {
-                        format!(
-                            "{} for worktree {}",
-                            error,
-                            local_worktree.abs_path().to_string_lossy()
-                        )
-                    };
-
-                    let entry = local_worktree
-                        .git_entry(repository.path)
-                        .with_context(|| worktree_error("No git entry found"))?;
-
-                    let repo = local_worktree
-                        .get_local_repo(&entry)
-                        .with_context(|| worktree_error("No repository found"))?
-                        .repo()
-                        .clone();
-
-                    if !repo.branch_exits(&new_branch)? {
-                        repo.create_branch(&new_branch)?;
-                    }
-
-                    repo.change_branch(&new_branch)?;
-                    Ok(())
-                });
-
-                Task::ready(result)
-            }
-            Worktree::Remote(remote_worktree) => {
-                let request = remote_worktree.client().request(proto::UpdateGitBranch {
-                    project_id: remote_worktree.project_id(),
-                    repository: Some(proto::ProjectPath {
-                        worktree_id: repository.worktree_id.to_proto(),
-                        path: repository.path.to_proto(), // Root path
-                    }),
-                    branch_name: new_branch,
-                });
-
-                cx.background_spawn(async move {
-                    request.await?;
-                    Ok(())
-                })
-            }
-        }
-    }
-
     async fn filter_paths(
         fs: &Arc<dyn Fs>,
         mut input: Receiver<MatchingEntry>,
@@ -1130,54 +981,6 @@ impl WorktreeStore {
             .ok_or_else(|| anyhow!("invalid request"))?;
         Worktree::handle_expand_all_for_entry(worktree, envelope.payload, cx).await
     }
-
-    pub async fn handle_git_branches(
-        this: Entity<Self>,
-        branches: TypedEnvelope<proto::GitBranches>,
-        cx: AsyncApp,
-    ) -> Result<proto::GitBranchesResponse> {
-        let project_path = branches
-            .payload
-            .repository
-            .clone()
-            .context("Invalid GitBranches call")?;
-        let project_path = ProjectPath {
-            worktree_id: WorktreeId::from_proto(project_path.worktree_id),
-            path: Arc::<Path>::from_proto(project_path.path),
-        };
-
-        let branches = this
-            .read_with(&cx, |this, cx| this.branches(project_path, cx))?
-            .await?;
-
-        Ok(proto::GitBranchesResponse {
-            branches: branches.iter().map(branch_to_proto).collect(),
-        })
-    }
-
-    pub async fn handle_update_branch(
-        this: Entity<Self>,
-        update_branch: TypedEnvelope<proto::UpdateGitBranch>,
-        cx: AsyncApp,
-    ) -> Result<proto::Ack> {
-        let project_path = update_branch
-            .payload
-            .repository
-            .clone()
-            .context("Invalid GitBranches call")?;
-        let project_path = ProjectPath {
-            worktree_id: WorktreeId::from_proto(project_path.worktree_id),
-            path: Arc::<Path>::from_proto(project_path.path),
-        };
-        let new_branch = update_branch.payload.branch_name;
-
-        this.read_with(&cx, |this, cx| {
-            this.update_or_create_branch(project_path, new_branch, cx)
-        })?
-        .await?;
-
-        Ok(proto::Ack {})
-    }
 }
 
 #[derive(Clone, Debug)]

crates/proto/proto/zed.proto 🔗

@@ -1,3 +1,4 @@
+
 syntax = "proto3";
 package zed.messages;
 
@@ -278,7 +279,6 @@ message Envelope {
         LanguageServerPromptRequest language_server_prompt_request = 268;
         LanguageServerPromptResponse language_server_prompt_response = 269;
 
-        GitBranches git_branches = 270;
         GitBranchesResponse git_branches_response = 271;
 
         UpdateGitBranch update_git_branch = 272;
@@ -332,7 +332,10 @@ message Envelope {
         ApplyCodeActionKind apply_code_action_kind = 309;
         ApplyCodeActionKindResponse apply_code_action_kind_response = 310;
 
-        RemoteMessageResponse remote_message_response = 311; // current max
+        RemoteMessageResponse remote_message_response = 311;
+        GitGetBranches git_get_branches = 312;
+        GitCreateBranch git_create_branch = 313;
+        GitChangeBranch git_change_branch = 314; // current max
     }
 
     reserved 87 to 88;
@@ -348,6 +351,7 @@ message Envelope {
     reserved 221;
     reserved 224 to 229;
     reserved 246;
+    reserved 270;
     reserved 247 to 254;
     reserved 255 to 256;
 }
@@ -2851,3 +2855,23 @@ message RemoteMessageResponse {
     string stdout = 1;
     string stderr = 2;
 }
+
+message GitGetBranches {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    uint64 work_directory_id = 3;
+}
+
+message GitCreateBranch {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    uint64 work_directory_id = 3;
+    string branch_name = 4;
+}
+
+message GitChangeBranch {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    uint64 work_directory_id = 3;
+    string branch_name = 4;
+}

crates/proto/src/proto.rs 🔗

@@ -424,7 +424,7 @@ messages!(
     (FlushBufferedMessages, Foreground),
     (LanguageServerPromptRequest, Foreground),
     (LanguageServerPromptResponse, Foreground),
-    (GitBranches, Background),
+    (GitGetBranches, Background),
     (GitBranchesResponse, Background),
     (UpdateGitBranch, Background),
     (ListToolchains, Foreground),
@@ -452,6 +452,8 @@ messages!(
     (GetRemotesResponse, Background),
     (Pull, Background),
     (RemoteMessageResponse, Background),
+    (GitCreateBranch, Background),
+    (GitChangeBranch, Background),
 );
 
 request_messages!(
@@ -575,7 +577,7 @@ request_messages!(
     (GetPermalinkToLine, GetPermalinkToLineResponse),
     (FlushBufferedMessages, Ack),
     (LanguageServerPromptRequest, LanguageServerPromptResponse),
-    (GitBranches, GitBranchesResponse),
+    (GitGetBranches, GitBranchesResponse),
     (UpdateGitBranch, Ack),
     (ListToolchains, ListToolchainsResponse),
     (ActivateToolchain, Ack),
@@ -594,6 +596,8 @@ request_messages!(
     (Fetch, RemoteMessageResponse),
     (GetRemotes, GetRemotesResponse),
     (Pull, RemoteMessageResponse),
+    (GitCreateBranch, Ack),
+    (GitChangeBranch, Ack),
 );
 
 entity_messages!(
@@ -679,7 +683,7 @@ entity_messages!(
     OpenServerSettings,
     GetPermalinkToLine,
     LanguageServerPromptRequest,
-    GitBranches,
+    GitGetBranches,
     UpdateGitBranch,
     ListToolchains,
     ActivateToolchain,
@@ -695,6 +699,8 @@ entity_messages!(
     Fetch,
     GetRemotes,
     Pull,
+    GitChangeBranch,
+    GitCreateBranch,
 );
 
 entity_messages!(

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -1344,9 +1344,12 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
     // Give the worktree a bit of time to index the file system
     cx.run_until_parked();
 
-    let remote_branches = project
-        .update(cx, |project, cx| project.branches(root_path.clone(), cx))
+    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
+
+    let remote_branches = repository
+        .update(cx, |repository, _| repository.branches())
         .await
+        .unwrap()
         .unwrap();
 
     let new_branch = branches[2];
@@ -1358,13 +1361,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
 
     assert_eq!(&remote_branches, &branches_set);
 
-    cx.update(|cx| {
-        project.update(cx, |project, cx| {
-            project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
-        })
-    })
-    .await
-    .unwrap();
+    cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string()))
+        .await
+        .unwrap()
+        .unwrap();
 
     cx.run_until_parked();
 
@@ -1384,11 +1384,21 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
 
     // Also try creating a new branch
     cx.update(|cx| {
-        project.update(cx, |project, cx| {
-            project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
-        })
+        repository
+            .read(cx)
+            .create_branch("totally-new-branch".to_string())
+    })
+    .await
+    .unwrap()
+    .unwrap();
+
+    cx.update(|cx| {
+        repository
+            .read(cx)
+            .change_branch("totally-new-branch".to_string())
     })
     .await
+    .unwrap()
     .unwrap();
 
     cx.run_until_parked();