git: Add base branch support to create_branch (#42151)

Ayush Chandekar created

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 <new-branch> <base-branch>`.

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 <ayu.chandekar@gmail.com>

Change summary

crates/collab/src/tests/integration_tests.rs                  |  2 
crates/collab/src/tests/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 ++--
crates/remote_server/src/remote_editing_tests.rs              |  2 
7 files changed, 59 insertions(+), 42 deletions(-)

Detailed changes

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

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

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<String>,
+    ) -> BoxFuture<'_, Result<()>> {
         self.with_state_async(true, move |state| {
             state.branches.insert(name);
             Ok(())

crates/git/src/repository.rs 🔗

@@ -431,7 +431,8 @@ pub trait GitRepository: Send + Sync {
     fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>>;
 
     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<String>)
+    -> BoxFuture<'_, Result<()>>;
     fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>>;
 
     fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>>;
@@ -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<String>,
+    ) -> 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, &current_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()
     }

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??;
 

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<Result<()>> {
+    pub fn create_branch(
+        &mut self,
+        branch_name: String,
+        base_branch: Option<String>,
+    ) -> oneshot::Receiver<Result<()>> {
         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<Result<()>> {

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