multiworkspace: Don't destroy workspace when git worktree is detached head (#51977)

Eric Holk created

When a git worktree is in a detached HEAD state (e.g. after `git
checkout --detach`), the workspace for that worktree would disappear
from the sidebar UI, and if the workspace was currently open your UI
would just disappear.

This happened because `parse_worktrees_from_str` silently dropped any
worktree entries without a `branch` line in the porcelain output, which
cascaded through the `linked_worktrees` list and caused
`prune_stale_worktree_workspaces` to remove the workspace.

This PR:
- Makes `Worktree::ref_name` an `Option<SharedString>` so detached
worktrees can be represented
- Renames `Worktree::branch()` to `Worktree::display_name()`, which
returns the branch name when available or the short SHA as a fallback
(mirroring how the title bar already handles detached HEAD for
repositories)
- Updates `parse_worktrees_from_str` to include detached and bare
worktree entries instead of dropping them
- Filters detached worktrees out of the worktree picker UI, preserving
the existing product decision to only show branch-based worktrees there

## 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

Change summary

crates/collab/tests/integration/git_tests.rs                          | 30 
crates/collab/tests/integration/remote_editing_collaboration_tests.rs |  5 
crates/fs/src/fake_git_repo.rs                                        |  4 
crates/fs/tests/integration/fake_git_repo.rs                          |  5 
crates/git/src/repository.rs                                          | 69 
crates/git_ui/src/worktree_picker.rs                                  | 28 
crates/project/src/git_store.rs                                       |  8 
crates/project/tests/integration/git_store.rs                         |  9 
crates/sidebar/src/sidebar.rs                                         | 16 
9 files changed, 107 insertions(+), 67 deletions(-)

Detailed changes

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"
     );
 

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 = {

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);

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

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<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(),

crates/git_ui/src/worktree_picker.rs 🔗

@@ -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

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(),
     }
 }

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");

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(),
             });
         })