From da41cecb05711ea047d7f7c9925d2b85753c4cb8 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:29:55 -0400 Subject: [PATCH] git: Fix a regression introduced with removing gitlib2 dep (#58359) The regression introduced in #53453 where selecting a remote branch from the branch picker checked out the remote tracking ref directly, leaving the repository in detached HEAD state. Instead of creating a local branch that set the remote branch as it's upstream. The fix was reverting the old checks we did before #53452 was merged to figure out if the branch was valid, remote only, local, or local without the upstream set. Depending on the type of reference it is Zed will check it out, or create/set the branch with remote tracking to its upstream branch. I also added a regression test to prevent this from happening again in the future 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: - N/A --- crates/git/src/repository.rs | 155 ++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 3 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 6746133a5b..93f53e4497 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -2029,11 +2029,48 @@ impl GitRepository for RealGitRepository { } fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { - let git_binary = self.git_binary(); + let git_binary = self.git_binary_in_worktree(); self.executor .spawn(async move { - git_binary.run(&["checkout", &name]).await?; - anyhow::Ok(()) + let git_binary = git_binary?; + let local_ref = format!("refs/heads/{name}"); + if git_binary + .run(&["show-ref", "--verify", "--quiet", &local_ref]) + .await + .is_ok() + { + git_binary.run(&["checkout", &name]).await?; + return anyhow::Ok(()); + } + + let remote_ref = format!("refs/remotes/{name}"); + if git_binary + .run(&["show-ref", "--verify", "--quiet", &remote_ref]) + .await + .is_ok() + { + let (_, branch_name) = + name.split_once('/').context("Unexpected branch format")?; + let local_branch_ref = format!("refs/heads/{branch_name}"); + if git_binary + .run(&["show-ref", "--verify", "--quiet", &local_branch_ref]) + .await + .is_ok() + { + git_binary + .run(&["branch", "--set-upstream-to", &name, branch_name]) + .await?; + } else { + git_binary + .run(&["branch", "--track", branch_name, &name]) + .await?; + } + + git_binary.run(&["checkout", branch_name]).await?; + return anyhow::Ok(()); + } + + anyhow::bail!("Branch '{}' not found", name); }) .boxed() } @@ -3934,6 +3971,118 @@ mod tests { ); } + #[gpui::test] + async fn test_change_branch_creates_local_tracking_branch_from_remote(cx: &mut TestAppContext) { + disable_git_global_config(); + cx.executor().allow_parking(); + + let temp_dir = tempfile::tempdir().unwrap(); + let remote_dir = temp_dir.path().join("remote.git"); + let seed_dir = temp_dir.path().join("seed"); + let clone_dir = temp_dir.path().join("clone"); + + git_command( + temp_dir.path(), + [ + OsString::from("init"), + OsString::from("--bare"), + OsString::from("-b"), + OsString::from("main"), + remote_dir.as_os_str().into(), + ], + ); + git_init_repo(&seed_dir); + fs::write(seed_dir.join("file.txt"), "main").unwrap(); + git_command(&seed_dir, ["add", "file.txt"]); + git_command(&seed_dir, ["commit", "-m", "initial"]); + git_command(&seed_dir, ["switch", "-c", "feature"]); + fs::write(seed_dir.join("feature.txt"), "feature").unwrap(); + git_command(&seed_dir, ["add", "feature.txt"]); + git_command(&seed_dir, ["commit", "-m", "feature"]); + git_command( + &seed_dir, + [ + OsString::from("remote"), + OsString::from("add"), + OsString::from("origin"), + remote_dir.as_os_str().into(), + ], + ); + git_command(&seed_dir, ["push", "-u", "origin", "main"]); + git_command(&seed_dir, ["push", "-u", "origin", "feature"]); + git_command( + temp_dir.path(), + [ + OsString::from("clone"), + remote_dir.as_os_str().into(), + clone_dir.as_os_str().into(), + ], + ); + + let repository = RealGitRepository::new( + &clone_dir.join(".git"), + None, + Some("git".into()), + cx.executor(), + ) + .unwrap(); + let git = repository.git_binary_in_worktree().unwrap(); + assert!( + git.run(&[ + "show-ref", + "--verify", + "--quiet", + "refs/remotes/origin/feature" + ]) + .await + .is_ok() + ); + assert!( + git.run(&["show-ref", "--verify", "--quiet", "refs/heads/feature"]) + .await + .is_err() + ); + + repository + .change_branch("origin/feature".to_string()) + .await + .unwrap(); + + let git = repository.git_binary_in_worktree().unwrap(); + assert_eq!( + git.run(&["branch", "--show-current"]).await.unwrap(), + "feature" + ); + assert_eq!( + git.run(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}",]) + .await + .unwrap(), + "origin/feature" + ); + + git.run(&["checkout", "main"]).await.unwrap(); + git.run(&["branch", "--unset-upstream", "feature"]) + .await + .unwrap(); + + repository + .change_branch("origin/feature".to_string()) + .await + .unwrap(); + + let git = repository.git_binary_in_worktree().unwrap(); + assert_eq!( + git.run(&["branch", "--show-current"]).await.unwrap(), + "feature" + ); + assert_eq!( + git.run(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}",]) + .await + .unwrap(), + "origin/feature" + ); + } + #[gpui::test] fn test_real_git_repository_new_rejects_malformed_git_file(cx: &mut TestAppContext) { disable_git_global_config();