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
This commit is contained in:
Anthony Eid authored and GitHub committed 2026-06-02 23:29:55 +00:00
1 parent d14b1f4ed5
commit da41cecb05
1 file changed
+152 -3
+152 -3
View File
@@ -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();