diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index 0cbd2a97dabcb832e6b25298341272be783e4024..cc1b748675d421ae92316d490df243f6d79bbc4f 100644 --- a/crates/collab/tests/integration/git_tests.rs +++ b/crates/collab/tests/integration/git_tests.rs @@ -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" ); diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index 59ba868de745b1364f7dd9cff45030b08e24d6a2..4c4f37489608be0313921be13cd9b09d5bf77c6d 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -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 = { diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index f94f8c1f62b07ea66961828d289123ad59b60978..9c218c8e53f9a2135ee09fadc78f627e3960da54 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -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); diff --git a/crates/fs/tests/integration/fake_git_repo.rs b/crates/fs/tests/integration/fake_git_repo.rs index cc68b77ee862bf09b1d02bfaf719544a52ac29c3..e327f92e996bfa0e89cc60a0a9c0d919bec8bc47 100644 --- a/crates/fs/tests/integration/fake_git_repo.rs +++ b/crates/fs/tests/integration/fake_git_repo.rs @@ -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 diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index e868fb22343fd43d88c5432f11e7bf1f6e6ab728..32904aa9a9001187193c91a055a5e0393221514d 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -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, // 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>(raw_worktrees: T) -> 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::>(); 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 diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index fef9c0eeb39db1f04ac6a4318697bc1492126c09..85278dae6eecf09e70343976823c2285e1d24f39 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -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(), } } diff --git a/crates/project/tests/integration/git_store.rs b/crates/project/tests/integration/git_store.rs index 5476ed4c5b2edcf326a28fc39abdf6b1057cc59e..02f752b28b24a8135e2cba9307a5eacdc16f0fa3 100644 --- a/crates/project/tests/integration/git_store.rs +++ b/crates/project/tests/integration/git_store.rs @@ -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"); diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index ced8850cbb2df8b2d68b9967eb445f9490e66821..dc687b01bdf298835497a18f09a9946769a0c193 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -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(), }); })