Detailed changes
@@ -235,7 +235,10 @@ async fn test_remote_git_worktrees(
assert_eq!(worktrees.len(), 2);
assert_eq!(worktrees[0].path, PathBuf::from(path!("/project")));
assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
- assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
+ assert_eq!(
+ worktrees[1].ref_name,
+ Some("refs/heads/feature-branch".into())
+ );
assert_eq!(worktrees[1].sha.as_ref(), "abc123");
// Verify from the host side that the worktree was actually created
@@ -287,7 +290,7 @@ async fn test_remote_git_worktrees(
let feature_worktree = worktrees
.iter()
- .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/feature-branch")
+ .find(|worktree| worktree.ref_name == Some("refs/heads/feature-branch".into()))
.expect("should find feature-branch worktree");
assert_eq!(
feature_worktree.path,
@@ -296,7 +299,7 @@ async fn test_remote_git_worktrees(
let bugfix_worktree = worktrees
.iter()
- .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/bugfix-branch")
+ .find(|worktree| worktree.ref_name == Some("refs/heads/bugfix-branch".into()))
.expect("should find bugfix-branch worktree");
assert_eq!(
bugfix_worktree.path,
@@ -396,17 +399,17 @@ async fn test_linked_worktrees_sync(
.with_git_state(Path::new(path!("/project/.git")), true, |state| {
state.worktrees.push(GitWorktree {
path: PathBuf::from(path!("/project")),
- ref_name: "refs/heads/main".into(),
+ ref_name: Some("refs/heads/main".into()),
sha: "aaa111".into(),
});
state.worktrees.push(GitWorktree {
path: PathBuf::from(path!("/project/feature-branch")),
- ref_name: "refs/heads/feature-branch".into(),
+ ref_name: Some("refs/heads/feature-branch".into()),
sha: "bbb222".into(),
});
state.worktrees.push(GitWorktree {
path: PathBuf::from(path!("/project/bugfix-branch")),
- ref_name: "refs/heads/bugfix-branch".into(),
+ ref_name: Some("refs/heads/bugfix-branch".into()),
sha: "ccc333".into(),
});
})
@@ -434,15 +437,18 @@ async fn test_linked_worktrees_sync(
PathBuf::from(path!("/project/feature-branch"))
);
assert_eq!(
- host_linked[0].ref_name.as_ref(),
- "refs/heads/feature-branch"
+ host_linked[0].ref_name,
+ Some("refs/heads/feature-branch".into())
);
assert_eq!(host_linked[0].sha.as_ref(), "bbb222");
assert_eq!(
host_linked[1].path,
PathBuf::from(path!("/project/bugfix-branch"))
);
- assert_eq!(host_linked[1].ref_name.as_ref(), "refs/heads/bugfix-branch");
+ assert_eq!(
+ host_linked[1].ref_name,
+ Some("refs/heads/bugfix-branch".into())
+ );
assert_eq!(host_linked[1].sha.as_ref(), "ccc333");
// Share the project and have client B join.
@@ -472,7 +478,7 @@ async fn test_linked_worktrees_sync(
.with_git_state(Path::new(path!("/project/.git")), true, |state| {
state.worktrees.push(GitWorktree {
path: PathBuf::from(path!("/project/hotfix-branch")),
- ref_name: "refs/heads/hotfix-branch".into(),
+ ref_name: Some("refs/heads/hotfix-branch".into()),
sha: "ddd444".into(),
});
})
@@ -514,7 +520,7 @@ async fn test_linked_worktrees_sync(
.with_git_state(Path::new(path!("/project/.git")), true, |state| {
state
.worktrees
- .retain(|wt| wt.ref_name.as_ref() != "refs/heads/bugfix-branch");
+ .retain(|wt| wt.ref_name != Some("refs/heads/bugfix-branch".into()));
})
.unwrap();
@@ -534,7 +540,7 @@ async fn test_linked_worktrees_sync(
assert!(
host_linked_after_removal
.iter()
- .all(|wt| wt.ref_name.as_ref() != "refs/heads/bugfix-branch"),
+ .all(|wt| wt.ref_name != Some("refs/heads/bugfix-branch".into())),
"bugfix-branch should have been removed"
);
@@ -491,7 +491,10 @@ async fn test_ssh_collaboration_git_worktrees(
.unwrap();
assert_eq!(worktrees.len(), 2);
assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
- assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
+ assert_eq!(
+ worktrees[1].ref_name,
+ Some("refs/heads/feature-branch".into())
+ );
assert_eq!(worktrees[1].sha.as_ref(), "abc123");
let server_worktrees = {
@@ -427,7 +427,7 @@ impl GitRepository for FakeGitRepository {
.unwrap_or_else(|| "refs/heads/main".to_string());
let main_worktree = Worktree {
path: work_dir,
- ref_name: branch_ref.into(),
+ ref_name: Some(branch_ref.into()),
sha: head_sha.into(),
};
let mut all = vec![main_worktree];
@@ -468,7 +468,7 @@ impl GitRepository for FakeGitRepository {
state.refs.insert(ref_name.clone(), sha.clone());
state.worktrees.push(Worktree {
path,
- ref_name: ref_name.into(),
+ ref_name: Some(ref_name.into()),
sha: sha.into(),
});
state.branches.insert(branch_name);
@@ -36,7 +36,10 @@ async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) {
assert_eq!(worktrees.len(), 2);
assert_eq!(worktrees[0].path, PathBuf::from("/project"));
assert_eq!(worktrees[1].path, worktree_1_dir);
- assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
+ assert_eq!(
+ worktrees[1].ref_name,
+ Some("refs/heads/feature-branch".into())
+ );
assert_eq!(worktrees[1].sha.as_ref(), "abc123");
// Directory should exist in FakeFs after create
@@ -212,18 +212,25 @@ impl Branch {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Worktree {
pub path: PathBuf,
- pub ref_name: SharedString,
+ pub ref_name: Option<SharedString>,
// todo(git_worktree) This type should be a Oid
pub sha: SharedString,
}
impl Worktree {
- pub fn branch(&self) -> &str {
- self.ref_name
- .as_ref()
- .strip_prefix("refs/heads/")
- .or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/"))
- .unwrap_or(self.ref_name.as_ref())
+ /// Returns a display name for the worktree, suitable for use in the UI.
+ ///
+ /// If the worktree is attached to a branch, returns the branch name.
+ /// Otherwise, returns the short SHA of the worktree's HEAD commit.
+ pub fn display_name(&self) -> &str {
+ match self.ref_name {
+ Some(ref ref_name) => ref_name
+ .strip_prefix("refs/heads/")
+ .or_else(|| ref_name.strip_prefix("refs/remotes/"))
+ .unwrap_or(ref_name),
+ // Detached HEAD — show the short SHA as a fallback.
+ None => &self.sha[..self.sha.len().min(SHORT_SHA_LENGTH)],
+ }
}
}
@@ -251,12 +258,10 @@ pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree
// Ignore other lines: detached, bare, locked, prunable, etc.
}
- // todo(git_worktree) We should add a test for detach head state
- // a detach head will have ref_name as none so we would skip it
- if let (Some(path), Some(sha), Some(ref_name)) = (path, sha, ref_name) {
+ if let (Some(path), Some(sha)) = (path, sha) {
worktrees.push(Worktree {
path: PathBuf::from(path),
- ref_name: ref_name.into(),
+ ref_name: ref_name.map(Into::into),
sha: sha.into(),
})
}
@@ -3793,7 +3798,7 @@ mod tests {
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123def");
- assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
+ assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
// Multiple worktrees
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3801,23 +3806,30 @@ mod tests {
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 2);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
- assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
+ assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
assert_eq!(result[1].path, PathBuf::from("/home/user/project-wt"));
- assert_eq!(result[1].ref_name.as_ref(), "refs/heads/feature");
+ assert_eq!(result[1].ref_name, Some("refs/heads/feature".into()));
- // Detached HEAD entry (should be skipped since ref_name won't parse)
+ // Detached HEAD entry (included with ref_name: None)
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
worktree /home/user/detached\nHEAD def456\ndetached\n\n";
let result = parse_worktrees_from_str(input);
- assert_eq!(result.len(), 1);
+ assert_eq!(result.len(), 2);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
+ assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+ assert_eq!(result[1].path, PathBuf::from("/home/user/detached"));
+ assert_eq!(result[1].ref_name, None);
+ assert_eq!(result[1].sha.as_ref(), "def456");
- // Bare repo entry (should be skipped)
+ // Bare repo entry (included with ref_name: None)
let input = "worktree /home/user/bare.git\nHEAD abc123\nbare\n\n\
worktree /home/user/project\nHEAD def456\nbranch refs/heads/main\n\n";
let result = parse_worktrees_from_str(input);
- assert_eq!(result.len(), 1);
- assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
+ assert_eq!(result.len(), 2);
+ assert_eq!(result[0].path, PathBuf::from("/home/user/bare.git"));
+ assert_eq!(result[0].ref_name, None);
+ assert_eq!(result[1].path, PathBuf::from("/home/user/project"));
+ assert_eq!(result[1].ref_name, Some("refs/heads/main".into()));
// Extra porcelain lines (locked, prunable) should be ignored
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3826,11 +3838,14 @@ mod tests {
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 3);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
- assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
+ assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
assert_eq!(result[1].path, PathBuf::from("/home/user/locked-wt"));
- assert_eq!(result[1].ref_name.as_ref(), "refs/heads/locked-branch");
+ assert_eq!(result[1].ref_name, Some("refs/heads/locked-branch".into()));
assert_eq!(result[2].path, PathBuf::from("/home/user/prunable-wt"));
- assert_eq!(result[2].ref_name.as_ref(), "refs/heads/prunable-branch");
+ assert_eq!(
+ result[2].ref_name,
+ Some("refs/heads/prunable-branch".into())
+ );
// Leading/trailing whitespace on lines should be tolerated
let input =
@@ -3839,7 +3854,7 @@ mod tests {
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123");
- assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
+ assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
// Windows-style line endings should be handled
let input = "worktree /home/user/project\r\nHEAD abc123\r\nbranch refs/heads/main\r\n\r\n";
@@ -3847,7 +3862,7 @@ mod tests {
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123");
- assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
+ assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
}
#[gpui::test]
@@ -3914,7 +3929,7 @@ mod tests {
let new_worktree = worktrees
.iter()
- .find(|w| w.branch() == "test-branch")
+ .find(|w| w.display_name() == "test-branch")
.expect("should find worktree with test-branch");
assert_eq!(
new_worktree.path.canonicalize().unwrap(),
@@ -3976,7 +3991,7 @@ mod tests {
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 1);
assert!(
- worktrees.iter().all(|w| w.branch() != "to-remove"),
+ worktrees.iter().all(|w| w.display_name() != "to-remove"),
"removed worktree should not appear in list"
);
assert!(!worktree_path.exists());
@@ -4078,7 +4093,7 @@ mod tests {
assert_eq!(worktrees.len(), 2);
let moved_worktree = worktrees
.iter()
- .find(|w| w.branch() == "old-name")
+ .find(|w| w.display_name() == "old-name")
.expect("should find worktree by branch name");
assert_eq!(
moved_worktree.path.canonicalize().unwrap(),
@@ -96,9 +96,12 @@ impl WorktreeList {
});
cx.spawn_in(window, async move |this, cx| {
- let all_worktrees = all_worktrees_request
+ let all_worktrees: Vec<_> = all_worktrees_request
.context("No active repository")?
- .await??;
+ .await??
+ .into_iter()
+ .filter(|worktree| worktree.ref_name.is_some()) // hide worktrees without a branch
+ .collect();
let default_branch = default_branch_request
.context("No active repository")?
@@ -182,7 +185,7 @@ impl WorktreeList {
return;
}
picker.delegate.create_worktree(
- entry.worktree.branch(),
+ entry.worktree.display_name(),
replace_current_window,
Some(default_branch.into()),
window,
@@ -649,7 +652,7 @@ impl PickerDelegate for WorktreeListDelegate {
let candidates = all_worktrees
.iter()
.enumerate()
- .map(|(ix, worktree)| StringMatchCandidate::new(ix, worktree.branch()))
+ .map(|(ix, worktree)| StringMatchCandidate::new(ix, worktree.display_name()))
.collect::<Vec<StringMatchCandidate>>();
fuzzy::match_strings(
&candidates,
@@ -674,13 +677,13 @@ impl PickerDelegate for WorktreeListDelegate {
if !query.is_empty()
&& !matches
.first()
- .is_some_and(|entry| entry.worktree.branch() == query)
+ .is_some_and(|entry| entry.worktree.display_name() == query)
{
let query = query.replace(' ', "-");
matches.push(WorktreeEntry {
worktree: GitWorktree {
path: Default::default(),
- ref_name: format!("refs/heads/{query}").into(),
+ ref_name: Some(format!("refs/heads/{query}").into()),
sha: Default::default(),
},
positions: Vec::new(),
@@ -706,7 +709,7 @@ impl PickerDelegate for WorktreeListDelegate {
return;
};
if entry.is_new {
- self.create_worktree(&entry.worktree.branch(), secondary, None, window, cx);
+ self.create_worktree(&entry.worktree.display_name(), secondary, None, window, cx);
} else {
self.open_worktree(&entry.worktree.path, secondary, window, cx);
}
@@ -737,16 +740,19 @@ impl PickerDelegate for WorktreeListDelegate {
let (branch_name, sublabel) = if entry.is_new {
(
- Label::new(format!("Create Worktree: \"{}\"…", entry.worktree.branch()))
- .truncate()
- .into_any_element(),
+ Label::new(format!(
+ "Create Worktree: \"{}\"…",
+ entry.worktree.display_name()
+ ))
+ .truncate()
+ .into_any_element(),
format!(
"based off {}",
self.base_branch(cx).unwrap_or("the current branch")
),
)
} else {
- let branch = entry.worktree.branch();
+ let branch = entry.worktree.display_name();
let branch_first_line = branch.lines().next().unwrap_or(branch);
let positions: Vec<_> = entry
.positions
@@ -7018,7 +7018,11 @@ fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree {
proto::Worktree {
path: worktree.path.to_string_lossy().to_string(),
- ref_name: worktree.ref_name.to_string(),
+ ref_name: worktree
+ .ref_name
+ .as_ref()
+ .map(|s| s.to_string())
+ .unwrap_or_default(),
sha: worktree.sha.to_string(),
}
}
@@ -7026,7 +7030,7 @@ fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree {
fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree {
git::repository::Worktree {
path: PathBuf::from(proto.path.clone()),
- ref_name: proto.ref_name.clone().into(),
+ ref_name: Some(SharedString::from(&proto.ref_name)),
sha: proto.sha.clone().into(),
}
}
@@ -1287,7 +1287,10 @@ mod git_worktrees {
assert_eq!(worktrees.len(), 2);
assert_eq!(worktrees[0].path, PathBuf::from(path!("/root")));
assert_eq!(worktrees[1].path, worktree_1_directory);
- assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
+ assert_eq!(
+ worktrees[1].ref_name,
+ Some("refs/heads/feature-branch".into())
+ );
assert_eq!(worktrees[1].sha.as_ref(), "abc123");
let worktree_2_directory = worktrees_directory.join("bugfix-branch");
@@ -1316,13 +1319,13 @@ mod git_worktrees {
let worktree_1 = worktrees
.iter()
- .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/feature-branch")
+ .find(|worktree| worktree.ref_name == Some("refs/heads/feature-branch".into()))
.expect("should find feature-branch worktree");
assert_eq!(worktree_1.path, worktree_1_directory);
let worktree_2 = worktrees
.iter()
- .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/bugfix-branch")
+ .find(|worktree| worktree.ref_name == Some("refs/heads/bugfix-branch".into()))
.expect("should find bugfix-branch worktree");
assert_eq!(worktree_2.path, worktree_2_directory);
assert_eq!(worktree_2.sha.as_ref(), "fake-sha");
@@ -4965,7 +4965,7 @@ mod tests {
.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt/rosewood"),
- ref_name: "refs/heads/rosewood".into(),
+ ref_name: Some("refs/heads/rosewood".into()),
sha: "abc".into(),
});
})
@@ -5026,7 +5026,7 @@ mod tests {
.with_git_state(std::path::Path::new("/project/.git"), true, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt/rosewood"),
- ref_name: "refs/heads/rosewood".into(),
+ ref_name: Some("refs/heads/rosewood".into()),
sha: "abc".into(),
});
})
@@ -5130,12 +5130,12 @@ mod tests {
fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-a"),
- ref_name: "refs/heads/feature-a".into(),
+ ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
});
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-b"),
- ref_name: "refs/heads/feature-b".into(),
+ ref_name: Some("refs/heads/feature-b".into()),
sha: "bbb".into(),
});
})
@@ -5232,7 +5232,7 @@ mod tests {
fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-a"),
- ref_name: "refs/heads/feature-a".into(),
+ ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
});
})
@@ -5348,7 +5348,7 @@ mod tests {
fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-a"),
- ref_name: "refs/heads/feature-a".into(),
+ ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
});
})
@@ -5457,7 +5457,7 @@ mod tests {
fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-a"),
- ref_name: "refs/heads/feature-a".into(),
+ ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
});
})
@@ -5563,7 +5563,7 @@ mod tests {
fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-a"),
- ref_name: "refs/heads/feature-a".into(),
+ ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
});
})