Support checking out remote HEAD refs (#57648)
Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments N/A - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Remote `HEAD` refs such as `origin/HEAD` and `upstream/HEAD` are symbolic refs that point to a remote's default branch. These refs can appear in the branch picker, but selecting them shows this error prompt:  This happens because the local branch name is directly derived from the selected ref, resulting in an attempted checkout of a local branch named `HEAD`. This change follows up on feedback from #57588, which just filtered remote `HEAD` refs out of the branch picker to prevent the error prompt. As mentioned in [this comment](https://github.com/zed-industries/zed/pull/57588#issuecomment-4533477336), I agree that the preferred behaviour should be to instead allow checking them out - useful for repos with multiple remotes such as an upstream. For example, if `refs/remotes/origin/HEAD` points to `refs/remotes/origin/main`, selecting `origin/HEAD` now behaves like selecting `origin/main`, switching to the corresponding local `main` branch instead of trying to create or check out a branch named `HEAD`. This does not fetch or pull from the remote; it uses the locally known remote refs. Release Notes: - Fixed selecting remote HEAD refs from the branch picker --------- Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
1 parent
c497b393a1
commit
e7927edccd
1 file changed
+165
-42
+165
-42
@@ -2131,6 +2131,13 @@ impl GitRepository for RealGitRepository {
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
let name = match git_binary.run(&["symbolic-ref", &remote_ref]).await {
|
||||
Ok(resolved) => resolved
|
||||
.strip_prefix("refs/remotes/")
|
||||
.map(str::to_owned)
|
||||
.unwrap_or(name),
|
||||
Err(_) => name,
|
||||
};
|
||||
let (_, branch_name) =
|
||||
name.split_once('/').context("Unexpected branch format")?;
|
||||
let local_branch_ref = format!("refs/heads/{branch_name}");
|
||||
@@ -3934,6 +3941,52 @@ mod tests {
|
||||
git_command(path, ["init", "-b", "main"]);
|
||||
}
|
||||
|
||||
fn clone_remote_repository_with_main_and_feature(temp_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let remote_directory = temp_dir.join("remote.git");
|
||||
let seed_directory = temp_dir.join("seed");
|
||||
let clone_directory = temp_dir.join("clone");
|
||||
|
||||
git_command(
|
||||
temp_dir,
|
||||
[
|
||||
OsString::from("init"),
|
||||
OsString::from("--bare"),
|
||||
OsString::from("-b"),
|
||||
OsString::from("main"),
|
||||
remote_directory.as_os_str().into(),
|
||||
],
|
||||
);
|
||||
git_init_repo(&seed_directory);
|
||||
fs::write(seed_directory.join("file.txt"), "main").unwrap();
|
||||
git_command(&seed_directory, ["add", "file.txt"]);
|
||||
git_command(&seed_directory, ["commit", "-m", "initial"]);
|
||||
git_command(&seed_directory, ["switch", "-c", "feature"]);
|
||||
fs::write(seed_directory.join("feature.txt"), "feature").unwrap();
|
||||
git_command(&seed_directory, ["add", "feature.txt"]);
|
||||
git_command(&seed_directory, ["commit", "-m", "feature"]);
|
||||
git_command(
|
||||
&seed_directory,
|
||||
[
|
||||
OsString::from("remote"),
|
||||
OsString::from("add"),
|
||||
OsString::from("origin"),
|
||||
remote_directory.as_os_str().into(),
|
||||
],
|
||||
);
|
||||
git_command(&seed_directory, ["push", "-u", "origin", "main"]);
|
||||
git_command(&seed_directory, ["push", "-u", "origin", "feature"]);
|
||||
git_command(
|
||||
temp_dir,
|
||||
[
|
||||
OsString::from("clone"),
|
||||
remote_directory.as_os_str().into(),
|
||||
clone_directory.as_os_str().into(),
|
||||
],
|
||||
);
|
||||
|
||||
(remote_directory, clone_directory)
|
||||
}
|
||||
|
||||
fn test_commit_envs() -> HashMap<String, String> {
|
||||
let mut env = checkpoint_author_envs();
|
||||
env.insert("GIT_ASKPASS".to_string(), "false".to_string());
|
||||
@@ -4078,50 +4131,11 @@ mod tests {
|
||||
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 (_remote_directory, clone_directory) =
|
||||
clone_remote_repository_with_main_and_feature(temp_dir.path());
|
||||
|
||||
let repository = RealGitRepository::new(
|
||||
&clone_dir.join(".git"),
|
||||
&clone_directory.join(".git"),
|
||||
None,
|
||||
Some("git".into()),
|
||||
cx.executor(),
|
||||
@@ -4184,6 +4198,115 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_branch_resolves_remote_head_to_tracking_branch(cx: &mut TestAppContext) {
|
||||
disable_git_global_config();
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let (_remote_directory, clone_directory) =
|
||||
clone_remote_repository_with_main_and_feature(temp_dir.path());
|
||||
|
||||
let repository = RealGitRepository::new(
|
||||
&clone_directory.join(".git"),
|
||||
None,
|
||||
Some("git".into()),
|
||||
cx.executor(),
|
||||
)
|
||||
.unwrap();
|
||||
let git = repository.git_binary_in_worktree().unwrap();
|
||||
git.run(&[
|
||||
"symbolic-ref",
|
||||
"refs/remotes/origin/HEAD",
|
||||
"refs/remotes/origin/feature",
|
||||
])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
git.run(&["symbolic-ref", "refs/remotes/origin/HEAD"])
|
||||
.await
|
||||
.unwrap(),
|
||||
"refs/remotes/origin/feature"
|
||||
);
|
||||
assert!(
|
||||
git.run(&["show-ref", "--verify", "--quiet", "refs/heads/feature"])
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
repository
|
||||
.change_branch("origin/HEAD".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]
|
||||
async fn test_change_branch_resolves_non_origin_remote_head(cx: &mut TestAppContext) {
|
||||
disable_git_global_config();
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let (remote_directory, clone_directory) =
|
||||
clone_remote_repository_with_main_and_feature(temp_dir.path());
|
||||
|
||||
git_command(
|
||||
&clone_directory,
|
||||
[
|
||||
OsString::from("remote"),
|
||||
OsString::from("add"),
|
||||
OsString::from("upstream"),
|
||||
remote_directory.as_os_str().into(),
|
||||
],
|
||||
);
|
||||
git_command(&clone_directory, ["fetch", "upstream"]);
|
||||
|
||||
let repository = RealGitRepository::new(
|
||||
&clone_directory.join(".git"),
|
||||
None,
|
||||
Some("git".into()),
|
||||
cx.executor(),
|
||||
)
|
||||
.unwrap();
|
||||
let git = repository.git_binary_in_worktree().unwrap();
|
||||
git.run(&[
|
||||
"symbolic-ref",
|
||||
"refs/remotes/upstream/HEAD",
|
||||
"refs/remotes/upstream/main",
|
||||
])
|
||||
.await
|
||||
.unwrap();
|
||||
git.run(&["checkout", "-b", "scratch"]).await.unwrap();
|
||||
|
||||
repository
|
||||
.change_branch("upstream/HEAD".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let git = repository.git_binary_in_worktree().unwrap();
|
||||
assert_eq!(
|
||||
git.run(&["branch", "--show-current"]).await.unwrap(),
|
||||
"main"
|
||||
);
|
||||
assert_eq!(
|
||||
git.run(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}",])
|
||||
.await
|
||||
.unwrap(),
|
||||
"upstream/main"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_real_git_repository_new_rejects_malformed_git_file(cx: &mut TestAppContext) {
|
||||
disable_git_global_config();
|
||||
|
||||
Reference in new issue
Block a user