From e025ee6a11729985666faf2507739c2522ba9f5a Mon Sep 17 00:00:00 2001 From: Ayush Chandekar Date: Mon, 10 Nov 2025 08:05:29 +0530 Subject: [PATCH] git: Add base branch support to create_branch (#42151) Closes [#41674](https://github.com/zed-industries/zed/issues/41674) Description: Creating a branch from a base requires switching to the base branch first, then creating the new branch and checking out to it, which requires multiple operations. Add base_branch parameter to create_branch to allow a new branch from a base branch in one operation which is synonymous to the command `git switch -c `. Below is the video after solving the issue: (`master` branch is the default branch here, and I create a branch `new-branch-2` based off the `master` branch. I also show the error which used to appear before the fix.) [Screencast from 2025-11-07 05-14-32.webm](https://github.com/user-attachments/assets/d37d1b58-af5f-44e8-b867-2aa5d4ef3d90) Release Notes: - Fixed the branch-picking error by replacing multiple sequential switch operations with just one switch operation. Signed-off-by: ayu-ch --- crates/collab/src/tests/integration_tests.rs | 2 +- .../remote_editing_collaboration_tests.rs | 2 +- crates/fs/src/fake_git_repo.rs | 6 ++- crates/git/src/repository.rs | 29 ++++++++--- crates/git_ui/src/branch_picker.rs | 12 +---- crates/project/src/git_store.rs | 48 +++++++++++-------- .../remote_server/src/remote_editing_tests.rs | 2 +- 7 files changed, 59 insertions(+), 42 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 4fa32b6c9ba55e6962547510f52251f16fc9be81..a4c8dc0e5b7e5eb01f099c11f29a5d651da09303 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7065,7 +7065,7 @@ async fn test_remote_git_branches( // Also try creating a new branch cx_b.update(|cx| { repo_b.update(cx, |repository, _cx| { - repository.create_branch("totally-new-branch".to_string()) + repository.create_branch("totally-new-branch".to_string(), None) }) }) .await diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 883bfa18725b02e4359aba371710c6e96efef73b..e5cc506bbca8b0a4a2fca972df61d373a288702c 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -326,7 +326,7 @@ async fn test_ssh_collaboration_git_branches( // Also try creating a new branch cx_b.update(|cx| { repo_b.update(cx, |repo_b, _cx| { - repo_b.create_branch("totally-new-branch".to_string()) + repo_b.create_branch("totally-new-branch".to_string(), None) }) }) .await diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 4652e0fdb098a8b7ee6bfdb5ac9f3215831afb97..aeaed1d6fc2947e55551026d518da18952cc051a 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -407,7 +407,11 @@ impl GitRepository for FakeGitRepository { }) } - fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { + fn create_branch( + &self, + name: String, + _base_branch: Option, + ) -> BoxFuture<'_, Result<()>> { self.with_state_async(true, move |state| { state.branches.insert(name); Ok(()) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 225ab81abdb3e5299880731951832eee000d8e5c..6fcf285e384f4a03a0f3fe8d2a613a56ace4666e 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -431,7 +431,8 @@ pub trait GitRepository: Send + Sync { fn branches(&self) -> BoxFuture<'_, Result>>; fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>; - fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>; + fn create_branch(&self, name: String, base_branch: Option) + -> BoxFuture<'_, Result<()>>; fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>>; fn worktrees(&self) -> BoxFuture<'_, Result>>; @@ -1358,14 +1359,28 @@ impl GitRepository for RealGitRepository { .boxed() } - fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { - let repo = self.repository.clone(); + fn create_branch( + &self, + name: String, + base_branch: Option, + ) -> BoxFuture<'_, Result<()>> { + let git_binary_path = self.any_git_binary_path.clone(); + let working_directory = self.working_directory(); + let executor = self.executor.clone(); + self.executor .spawn(async move { - let repo = repo.lock(); - let current_commit = repo.head()?.peel_to_commit()?; - repo.branch(&name, ¤t_commit, false)?; - Ok(()) + let mut args = vec!["switch", "-c", &name]; + let base_branch_str; + if let Some(ref base) = base_branch { + base_branch_str = base.clone(); + args.push(&base_branch_str); + } + + GitBinary::new(git_binary_path, working_directory?, executor) + .run(&args) + .await?; + anyhow::Ok(()) }) .boxed() } diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index e10568ff37aaa47924632558228294feca84ac61..3ae9059b2a12f178931a5271b92c5fdf44f319d4 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -241,18 +241,10 @@ impl BranchListDelegate { return; }; let new_branch_name = new_branch_name.to_string().replace(' ', "-"); + let base_branch = from_branch.map(|b| b.to_string()); cx.spawn(async move |_, cx| { - if let Some(based_branch) = from_branch { - repo.update(cx, |repo, _| repo.change_branch(based_branch.to_string()))? - .await??; - } - - repo.update(cx, |repo, _| { - repo.create_branch(new_branch_name.to_string()) - })? - .await??; repo.update(cx, |repo, _| { - repo.change_branch(new_branch_name.to_string()) + repo.create_branch(new_branch_name, base_branch) })? .await??; diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 8767e5e2931288519d78739f67292728b6d5b77d..90d76f51be27c66894519ea22ddcaa19baedc9c4 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -2094,7 +2094,7 @@ impl GitStore { repository_handle .update(&mut cx, |repository_handle, _| { - repository_handle.create_branch(branch_name) + repository_handle.create_branch(branch_name, None) })? .await??; @@ -4747,29 +4747,35 @@ impl Repository { }) } - pub fn create_branch(&mut self, branch_name: String) -> oneshot::Receiver> { + pub fn create_branch( + &mut self, + branch_name: String, + base_branch: Option, + ) -> oneshot::Receiver> { let id = self.id; - self.send_job( - Some(format!("git switch -c {branch_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local { backend, .. } => { - backend.create_branch(branch_name).await - } - RepositoryState::Remote { project_id, client } => { - client - .request(proto::GitCreateBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; + let status_msg = if let Some(ref base) = base_branch { + format!("git switch -c {branch_name} {base}").into() + } else { + format!("git switch -c {branch_name}").into() + }; + self.send_job(Some(status_msg), move |repo, _cx| async move { + match repo { + RepositoryState::Local { backend, .. } => { + backend.create_branch(branch_name, base_branch).await + } + RepositoryState::Remote { project_id, client } => { + client + .request(proto::GitCreateBranch { + project_id: project_id.0, + repository_id: id.to_proto(), + branch_name, + }) + .await?; - Ok(()) - } + Ok(()) } - }, - ) + } + }) } pub fn change_branch(&mut self, branch_name: String) -> oneshot::Receiver> { diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index de0d58e50dbc036723365ca6099efabff1f8449d..98a0aab70bcb4e5590f477f6e6de9aebd512b3c2 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1662,7 +1662,7 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA // Also try creating a new branch cx.update(|cx| { repository.update(cx, |repo, _cx| { - repo.create_branch("totally-new-branch".to_string()) + repo.create_branch("totally-new-branch".to_string(), None) }) }) .await