Clean up worktree setup in tests (#52934)

Max Brunsfeld , Eric Holk , and Mikayla Maki created

Release Notes:

- N/A

---------

Co-authored-by: Eric Holk <eric@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/collab/tests/integration/git_tests.rs                          |  86 
crates/collab/tests/integration/remote_editing_collaboration_tests.rs |  12 
crates/fs/src/fake_git_repo.rs                                        | 265 
crates/fs/src/fs.rs                                                   | 126 
crates/remote/src/transport/docker.rs                                 |   2 
crates/sidebar/src/project_group_builder.rs                           |  12 
crates/sidebar/src/sidebar_tests.rs                                   | 491 
7 files changed, 499 insertions(+), 495 deletions(-)

Detailed changes

crates/collab/tests/integration/git_tests.rs 🔗

@@ -394,29 +394,29 @@ async fn test_linked_worktrees_sync(
         )
         .await;
 
-    client_a
-        .fs()
-        .with_git_state(Path::new(path!("/project/.git")), true, |state| {
-            state.worktrees.push(GitWorktree {
-                path: PathBuf::from(path!("/project")),
-                ref_name: Some("refs/heads/main".into()),
-                sha: "aaa111".into(),
-                is_main: false,
-            });
-            state.worktrees.push(GitWorktree {
-                path: PathBuf::from(path!("/project/feature-branch")),
-                ref_name: Some("refs/heads/feature-branch".into()),
-                sha: "bbb222".into(),
-                is_main: false,
-            });
-            state.worktrees.push(GitWorktree {
-                path: PathBuf::from(path!("/project/bugfix-branch")),
-                ref_name: Some("refs/heads/bugfix-branch".into()),
-                sha: "ccc333".into(),
-                is_main: false,
-            });
-        })
-        .unwrap();
+    let fs = client_a.fs();
+    fs.add_linked_worktree_for_repo(
+        Path::new(path!("/project/.git")),
+        true,
+        GitWorktree {
+            path: PathBuf::from(path!("/worktrees/feature-branch")),
+            ref_name: Some("refs/heads/feature-branch".into()),
+            sha: "bbb222".into(),
+            is_main: false,
+        },
+    )
+    .await;
+    fs.add_linked_worktree_for_repo(
+        Path::new(path!("/project/.git")),
+        true,
+        GitWorktree {
+            path: PathBuf::from(path!("/worktrees/bugfix-branch")),
+            ref_name: Some("refs/heads/bugfix-branch".into()),
+            sha: "ccc333".into(),
+            is_main: false,
+        },
+    )
+    .await;
 
     let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
 
@@ -437,22 +437,22 @@ async fn test_linked_worktrees_sync(
     );
     assert_eq!(
         host_linked[0].path,
-        PathBuf::from(path!("/project/feature-branch"))
+        PathBuf::from(path!("/worktrees/bugfix-branch"))
     );
     assert_eq!(
         host_linked[0].ref_name,
-        Some("refs/heads/feature-branch".into())
+        Some("refs/heads/bugfix-branch".into())
     );
-    assert_eq!(host_linked[0].sha.as_ref(), "bbb222");
+    assert_eq!(host_linked[0].sha.as_ref(), "ccc333");
     assert_eq!(
         host_linked[1].path,
-        PathBuf::from(path!("/project/bugfix-branch"))
+        PathBuf::from(path!("/worktrees/feature-branch"))
     );
     assert_eq!(
         host_linked[1].ref_name,
-        Some("refs/heads/bugfix-branch".into())
+        Some("refs/heads/feature-branch".into())
     );
-    assert_eq!(host_linked[1].sha.as_ref(), "ccc333");
+    assert_eq!(host_linked[1].sha.as_ref(), "bbb222");
 
     // Share the project and have client B join.
     let project_id = active_call_a
@@ -478,15 +478,17 @@ async fn test_linked_worktrees_sync(
     // Now mutate: add a third linked worktree on the host side.
     client_a
         .fs()
-        .with_git_state(Path::new(path!("/project/.git")), true, |state| {
-            state.worktrees.push(GitWorktree {
-                path: PathBuf::from(path!("/project/hotfix-branch")),
+        .add_linked_worktree_for_repo(
+            Path::new(path!("/project/.git")),
+            true,
+            GitWorktree {
+                path: PathBuf::from(path!("/worktrees/hotfix-branch")),
                 ref_name: Some("refs/heads/hotfix-branch".into()),
                 sha: "ddd444".into(),
                 is_main: false,
-            });
-        })
-        .unwrap();
+            },
+        )
+        .await;
 
     // Wait for the host to re-scan and propagate the update.
     executor.run_until_parked();
@@ -504,7 +506,7 @@ async fn test_linked_worktrees_sync(
     );
     assert_eq!(
         host_linked_updated[2].path,
-        PathBuf::from(path!("/project/hotfix-branch"))
+        PathBuf::from(path!("/worktrees/hotfix-branch"))
     );
 
     // Verify the guest also received the update.
@@ -521,12 +523,12 @@ async fn test_linked_worktrees_sync(
     // Now mutate: remove one linked worktree from the host side.
     client_a
         .fs()
-        .with_git_state(Path::new(path!("/project/.git")), true, |state| {
-            state
-                .worktrees
-                .retain(|wt| wt.ref_name != Some("refs/heads/bugfix-branch".into()));
-        })
-        .unwrap();
+        .remove_worktree_for_repo(
+            Path::new(path!("/project/.git")),
+            true,
+            "refs/heads/bugfix-branch",
+        )
+        .await;
 
     executor.run_until_parked();
 

crates/collab/tests/integration/remote_editing_collaboration_tests.rs 🔗

@@ -469,7 +469,7 @@ async fn test_ssh_collaboration_git_worktrees(
         .unwrap();
     assert_eq!(worktrees.len(), 1);
 
-    let worktree_directory = PathBuf::from("/project");
+    let worktree_directory = PathBuf::from("/worktrees");
     cx_b.update(|cx| {
         repo_b.update(cx, |repo, _| {
             repo.create_worktree(
@@ -536,8 +536,8 @@ async fn test_ssh_collaboration_git_worktrees(
     cx_a.update(|cx| {
         repo_a.update(cx, |repository, _| {
             repository.rename_worktree(
-                PathBuf::from("/project/feature-branch"),
-                PathBuf::from("/project/renamed-branch"),
+                PathBuf::from("/worktrees/feature-branch"),
+                PathBuf::from("/worktrees/renamed-branch"),
             )
         })
     })
@@ -559,7 +559,7 @@ async fn test_ssh_collaboration_git_worktrees(
     );
     assert_eq!(
         host_worktrees[1].path,
-        PathBuf::from("/project/renamed-branch")
+        PathBuf::from("/worktrees/renamed-branch")
     );
 
     let server_worktrees = {
@@ -588,13 +588,13 @@ async fn test_ssh_collaboration_git_worktrees(
     );
     assert_eq!(
         server_worktrees[1].path,
-        PathBuf::from("/project/renamed-branch")
+        PathBuf::from("/worktrees/renamed-branch")
     );
 
     // Host (client A) removes the renamed worktree via SSH
     cx_a.update(|cx| {
         repo_a.update(cx, |repository, _| {
-            repository.remove_worktree(PathBuf::from("/project/renamed-branch"), false)
+            repository.remove_worktree(PathBuf::from("/worktrees/renamed-branch"), false)
         })
     })
     .await

crates/fs/src/fake_git_repo.rs 🔗

@@ -53,7 +53,6 @@ pub struct FakeGitRepositoryState {
     pub simulated_create_worktree_error: Option<String>,
     pub refs: HashMap<String, String>,
     pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
-    pub worktrees: Vec<Worktree>,
 }
 
 impl FakeGitRepositoryState {
@@ -73,7 +72,6 @@ impl FakeGitRepositoryState {
             oids: Default::default(),
             remotes: HashMap::default(),
             graph_commits: Vec::new(),
-            worktrees: Vec::new(),
         }
     }
 }
@@ -409,32 +407,78 @@ impl GitRepository for FakeGitRepository {
     }
 
     fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
-        let dot_git_path = self.dot_git_path.clone();
-        self.with_state_async(false, move |state| {
-            let work_dir = dot_git_path
-                .parent()
-                .map(PathBuf::from)
-                .unwrap_or(dot_git_path);
-            let head_sha = state
-                .refs
-                .get("HEAD")
-                .cloned()
-                .unwrap_or_else(|| "0000000".to_string());
-            let branch_ref = state
-                .current_branch_name
-                .as_ref()
-                .map(|name| format!("refs/heads/{name}"))
-                .unwrap_or_else(|| "refs/heads/main".to_string());
-            let main_worktree = Worktree {
-                path: work_dir,
-                ref_name: Some(branch_ref.into()),
-                sha: head_sha.into(),
-                is_main: true,
-            };
+        let fs = self.fs.clone();
+        let common_dir_path = self.common_dir_path.clone();
+        let executor = self.executor.clone();
+
+        async move {
+            executor.simulate_random_delay().await;
+
+            let (main_worktree, refs) = fs.with_git_state(&common_dir_path, false, |state| {
+                let work_dir = common_dir_path
+                    .parent()
+                    .map(PathBuf::from)
+                    .unwrap_or_else(|| common_dir_path.clone());
+                let head_sha = state
+                    .refs
+                    .get("HEAD")
+                    .cloned()
+                    .unwrap_or_else(|| "0000000".to_string());
+                let branch_ref = state
+                    .current_branch_name
+                    .as_ref()
+                    .map(|name| format!("refs/heads/{name}"))
+                    .unwrap_or_else(|| "refs/heads/main".to_string());
+                let main_wt = Worktree {
+                    path: work_dir,
+                    ref_name: Some(branch_ref.into()),
+                    sha: head_sha.into(),
+                    is_main: true,
+                };
+                (main_wt, state.refs.clone())
+            })?;
+
             let mut all = vec![main_worktree];
-            all.extend(state.worktrees.iter().cloned());
+
+            let worktrees_dir = common_dir_path.join("worktrees");
+            if let Ok(mut entries) = fs.read_dir(&worktrees_dir).await {
+                use futures::StreamExt;
+                while let Some(Ok(entry_path)) = entries.next().await {
+                    let head_content = match fs.load(&entry_path.join("HEAD")).await {
+                        Ok(content) => content,
+                        Err(_) => continue,
+                    };
+                    let gitdir_content = match fs.load(&entry_path.join("gitdir")).await {
+                        Ok(content) => content,
+                        Err(_) => continue,
+                    };
+
+                    let ref_name = head_content
+                        .strip_prefix("ref: ")
+                        .map(|s| s.trim().to_string());
+                    let sha = ref_name
+                        .as_ref()
+                        .and_then(|r| refs.get(r))
+                        .cloned()
+                        .unwrap_or_else(|| head_content.trim().to_string());
+
+                    let worktree_path = PathBuf::from(gitdir_content.trim())
+                        .parent()
+                        .map(PathBuf::from)
+                        .unwrap_or_default();
+
+                    all.push(Worktree {
+                        path: worktree_path,
+                        ref_name: ref_name.map(Into::into),
+                        sha: sha.into(),
+                        is_main: false,
+                    });
+                }
+            }
+
             Ok(all)
-        })
+        }
+        .boxed()
     }
 
     fn create_worktree(
@@ -446,36 +490,58 @@ impl GitRepository for FakeGitRepository {
         let fs = self.fs.clone();
         let executor = self.executor.clone();
         let dot_git_path = self.dot_git_path.clone();
+        let common_dir_path = self.common_dir_path.clone();
         async move {
             executor.simulate_random_delay().await;
-            // Check for simulated error before any side effects
+            // Check for simulated error and duplicate branch before any side effects.
             fs.with_git_state(&dot_git_path, false, |state| {
                 if let Some(message) = &state.simulated_create_worktree_error {
                     anyhow::bail!("{message}");
                 }
+                if state.branches.contains(&branch_name) {
+                    bail!("a branch named '{}' already exists", branch_name);
+                }
                 Ok(())
             })??;
-            // Create directory before updating state so state is never
-            // inconsistent with the filesystem
+
+            // Create the worktree checkout directory.
             fs.create_dir(&path).await?;
-            fs.with_git_state(&dot_git_path, true, {
-                let path = path.clone();
-                move |state| {
-                    if state.branches.contains(&branch_name) {
-                        bail!("a branch named '{}' already exists", branch_name);
-                    }
-                    let ref_name = format!("refs/heads/{branch_name}");
-                    let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
-                    state.refs.insert(ref_name.clone(), sha.clone());
-                    state.worktrees.push(Worktree {
-                        path,
-                        ref_name: Some(ref_name.into()),
-                        sha: sha.into(),
-                        is_main: false,
-                    });
-                    state.branches.insert(branch_name);
-                    Ok::<(), anyhow::Error>(())
-                }
+
+            // Create .git/worktrees/<name>/ directory with HEAD, commondir, gitdir.
+            let ref_name = format!("refs/heads/{branch_name}");
+            let worktrees_entry_dir = common_dir_path.join("worktrees").join(&branch_name);
+            fs.create_dir(&worktrees_entry_dir).await?;
+
+            fs.write_file_internal(
+                worktrees_entry_dir.join("HEAD"),
+                format!("ref: {ref_name}").into_bytes(),
+                false,
+            )?;
+            fs.write_file_internal(
+                worktrees_entry_dir.join("commondir"),
+                common_dir_path.to_string_lossy().into_owned().into_bytes(),
+                false,
+            )?;
+            let worktree_dot_git = path.join(".git");
+            fs.write_file_internal(
+                worktrees_entry_dir.join("gitdir"),
+                worktree_dot_git.to_string_lossy().into_owned().into_bytes(),
+                false,
+            )?;
+
+            // Create .git file in the worktree checkout.
+            fs.write_file_internal(
+                &worktree_dot_git,
+                format!("gitdir: {}", worktrees_entry_dir.display()).into_bytes(),
+                false,
+            )?;
+
+            // Update git state: add ref and branch.
+            let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
+            fs.with_git_state(&dot_git_path, true, move |state| {
+                state.refs.insert(ref_name, sha);
+                state.branches.insert(branch_name);
+                Ok::<(), anyhow::Error>(())
             })??;
             Ok(())
         }
@@ -485,20 +551,23 @@ impl GitRepository for FakeGitRepository {
     fn remove_worktree(&self, path: PathBuf, _force: bool) -> BoxFuture<'_, Result<()>> {
         let fs = self.fs.clone();
         let executor = self.executor.clone();
-        let dot_git_path = self.dot_git_path.clone();
+        let common_dir_path = self.common_dir_path.clone();
         async move {
             executor.simulate_random_delay().await;
-            // Validate the worktree exists in state before touching the filesystem
-            fs.with_git_state(&dot_git_path, false, {
-                let path = path.clone();
-                move |state| {
-                    if !state.worktrees.iter().any(|w| w.path == path) {
-                        bail!("no worktree found at path: {}", path.display());
-                    }
-                    Ok(())
-                }
-            })??;
-            // Now remove the directory
+
+            // Read the worktree's .git file to find its entry directory.
+            let dot_git_file = path.join(".git");
+            let content = fs
+                .load(&dot_git_file)
+                .await
+                .with_context(|| format!("no worktree found at path: {}", path.display()))?;
+            let gitdir = content
+                .strip_prefix("gitdir:")
+                .context("invalid .git file in worktree")?
+                .trim();
+            let worktree_entry_dir = PathBuf::from(gitdir);
+
+            // Remove the worktree checkout directory.
             fs.remove_dir(
                 &path,
                 RemoveOptions {
@@ -507,11 +576,21 @@ impl GitRepository for FakeGitRepository {
                 },
             )
             .await?;
-            // Update state
-            fs.with_git_state(&dot_git_path, true, move |state| {
-                state.worktrees.retain(|worktree| worktree.path != path);
-                Ok::<(), anyhow::Error>(())
-            })??;
+
+            // Remove the .git/worktrees/<name>/ directory.
+            fs.remove_dir(
+                &worktree_entry_dir,
+                RemoveOptions {
+                    recursive: true,
+                    ignore_if_not_exists: false,
+                },
+            )
+            .await?;
+
+            // Emit a git event on the main .git directory so the scanner
+            // notices the change.
+            fs.with_git_state(&common_dir_path, true, |_| {})?;
+
             Ok(())
         }
         .boxed()
@@ -520,20 +599,23 @@ impl GitRepository for FakeGitRepository {
     fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
         let fs = self.fs.clone();
         let executor = self.executor.clone();
-        let dot_git_path = self.dot_git_path.clone();
+        let common_dir_path = self.common_dir_path.clone();
         async move {
             executor.simulate_random_delay().await;
-            // Validate the worktree exists in state before touching the filesystem
-            fs.with_git_state(&dot_git_path, false, {
-                let old_path = old_path.clone();
-                move |state| {
-                    if !state.worktrees.iter().any(|w| w.path == old_path) {
-                        bail!("no worktree found at path: {}", old_path.display());
-                    }
-                    Ok(())
-                }
-            })??;
-            // Now move the directory
+
+            // Read the worktree's .git file to find its entry directory.
+            let dot_git_file = old_path.join(".git");
+            let content = fs
+                .load(&dot_git_file)
+                .await
+                .with_context(|| format!("no worktree found at path: {}", old_path.display()))?;
+            let gitdir = content
+                .strip_prefix("gitdir:")
+                .context("invalid .git file in worktree")?
+                .trim();
+            let worktree_entry_dir = PathBuf::from(gitdir);
+
+            // Move the worktree checkout directory.
             fs.rename(
                 &old_path,
                 &new_path,
@@ -544,16 +626,27 @@ impl GitRepository for FakeGitRepository {
                 },
             )
             .await?;
-            // Update state
-            fs.with_git_state(&dot_git_path, true, move |state| {
-                let worktree = state
-                    .worktrees
-                    .iter_mut()
-                    .find(|worktree| worktree.path == old_path)
-                    .expect("worktree was validated above");
-                worktree.path = new_path;
-                Ok::<(), anyhow::Error>(())
-            })??;
+
+            // Update the gitdir file in .git/worktrees/<name>/ to point to the
+            // new location.
+            let new_dot_git = new_path.join(".git");
+            fs.write_file_internal(
+                worktree_entry_dir.join("gitdir"),
+                new_dot_git.to_string_lossy().into_owned().into_bytes(),
+                false,
+            )?;
+
+            // Update the .git file in the moved worktree checkout.
+            fs.write_file_internal(
+                &new_dot_git,
+                format!("gitdir: {}", worktree_entry_dir.display()).into_bytes(),
+                false,
+            )?;
+
+            // Emit a git event on the main .git directory so the scanner
+            // notices the change.
+            fs.with_git_state(&common_dir_path, true, |_| {})?;
+
             Ok(())
         }
         .boxed()

crates/fs/src/fs.rs 🔗

@@ -57,7 +57,7 @@ use collections::{BTreeMap, btree_map};
 use fake_git_repo::FakeGitRepositoryState;
 #[cfg(feature = "test-support")]
 use git::{
-    repository::{InitialGraphCommitData, RepoPath, repo_path},
+    repository::{InitialGraphCommitData, RepoPath, Worktree, repo_path},
     status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
 };
 #[cfg(feature = "test-support")]
@@ -1892,11 +1892,15 @@ impl FakeFs {
                 anyhow::bail!("gitfile points to a non-directory")
             };
             let common_dir = if let Some(child) = entries.get("commondir") {
-                Path::new(
-                    std::str::from_utf8(child.file_content("commondir".as_ref())?)
-                        .context("commondir content")?,
-                )
-                .to_owned()
+                let raw = std::str::from_utf8(child.file_content("commondir".as_ref())?)
+                    .context("commondir content")?
+                    .trim();
+                let raw_path = Path::new(raw);
+                if raw_path.is_relative() {
+                    normalize_path(&canonical_path.join(raw_path))
+                } else {
+                    raw_path.to_owned()
+                }
             } else {
                 canonical_path.clone()
             };
@@ -1960,6 +1964,116 @@ impl FakeFs {
         .unwrap();
     }
 
+    pub async fn add_linked_worktree_for_repo(
+        &self,
+        dot_git: &Path,
+        emit_git_event: bool,
+        worktree: Worktree,
+    ) {
+        let ref_name = worktree
+            .ref_name
+            .as_ref()
+            .expect("linked worktree must have a ref_name");
+        let branch_name = ref_name
+            .strip_prefix("refs/heads/")
+            .unwrap_or(ref_name.as_ref());
+
+        // Create ref in git state.
+        self.with_git_state(dot_git, false, |state| {
+            state
+                .refs
+                .insert(ref_name.to_string(), worktree.sha.to_string());
+        })
+        .unwrap();
+
+        // Create .git/worktrees/<name>/ directory with HEAD, commondir, and gitdir.
+        let worktrees_entry_dir = dot_git.join("worktrees").join(branch_name);
+        self.create_dir(&worktrees_entry_dir).await.unwrap();
+
+        self.write_file_internal(
+            worktrees_entry_dir.join("HEAD"),
+            format!("ref: {ref_name}").into_bytes(),
+            false,
+        )
+        .unwrap();
+
+        self.write_file_internal(
+            worktrees_entry_dir.join("commondir"),
+            dot_git.to_string_lossy().into_owned().into_bytes(),
+            false,
+        )
+        .unwrap();
+
+        let worktree_dot_git = worktree.path.join(".git");
+        self.write_file_internal(
+            worktrees_entry_dir.join("gitdir"),
+            worktree_dot_git.to_string_lossy().into_owned().into_bytes(),
+            false,
+        )
+        .unwrap();
+
+        // Create the worktree checkout directory with a .git file pointing back.
+        self.create_dir(&worktree.path).await.unwrap();
+
+        self.write_file_internal(
+            &worktree_dot_git,
+            format!("gitdir: {}", worktrees_entry_dir.display()).into_bytes(),
+            false,
+        )
+        .unwrap();
+
+        if emit_git_event {
+            self.with_git_state(dot_git, true, |_| {}).unwrap();
+        }
+    }
+
+    pub async fn remove_worktree_for_repo(
+        &self,
+        dot_git: &Path,
+        emit_git_event: bool,
+        ref_name: &str,
+    ) {
+        let branch_name = ref_name.strip_prefix("refs/heads/").unwrap_or(ref_name);
+        let worktrees_entry_dir = dot_git.join("worktrees").join(branch_name);
+
+        // Read gitdir to find the worktree checkout path.
+        let gitdir_content = self
+            .load_internal(worktrees_entry_dir.join("gitdir"))
+            .await
+            .unwrap();
+        let gitdir_str = String::from_utf8(gitdir_content).unwrap();
+        let worktree_path = PathBuf::from(gitdir_str.trim())
+            .parent()
+            .map(PathBuf::from)
+            .unwrap_or_default();
+
+        // Remove the worktree checkout directory.
+        self.remove_dir(
+            &worktree_path,
+            RemoveOptions {
+                recursive: true,
+                ignore_if_not_exists: true,
+            },
+        )
+        .await
+        .unwrap();
+
+        // Remove the .git/worktrees/<name>/ directory.
+        self.remove_dir(
+            &worktrees_entry_dir,
+            RemoveOptions {
+                recursive: true,
+                ignore_if_not_exists: false,
+            },
+        )
+        .await
+        .unwrap();
+
+        if emit_git_event {
+            self.with_git_state(dot_git, true, |_| {}).unwrap();
+        }
+    }
+
     pub fn set_unmerged_paths_for_repo(
         &self,
         dot_git: &Path,

crates/remote/src/transport/docker.rs 🔗

@@ -30,7 +30,7 @@ use crate::{
     transport::parse_platform,
 };
 
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct DockerConnectionOptions {
     pub name: String,
     pub container_id: String,

crates/sidebar/src/project_group_builder.rs 🔗

@@ -250,15 +250,17 @@ mod tests {
             }),
         )
         .await;
-        fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-            state.worktrees.push(git::repository::Worktree {
+        fs.add_linked_worktree_for_repo(
+            std::path::Path::new("/project/.git"),
+            false,
+            git::repository::Worktree {
                 path: std::path::PathBuf::from("/wt/feature-a"),
                 ref_name: Some("refs/heads/feature-a".into()),
                 sha: "abc".into(),
                 is_main: false,
-            });
-        })
-        .expect("git state should be set");
+            },
+        )
+        .await;
         fs
     }
 

crates/sidebar/src/sidebar_tests.rs 🔗

@@ -12,7 +12,10 @@ use gpui::TestAppContext;
 use pretty_assertions::assert_eq;
 use project::AgentId;
 use settings::SettingsStore;
-use std::{path::PathBuf, sync::Arc};
+use std::{
+    path::{Path, PathBuf},
+    sync::Arc,
+};
 use util::path_list::PathList;
 
 fn init_test(cx: &mut TestAppContext) {
@@ -2435,38 +2438,24 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
     // Worktree checkout pointing back to the main repo.
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
-            "src": {},
-        }),
-    )
-    .await;
-
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -2573,15 +2562,17 @@ async fn test_search_matches_worktree_name(cx: &mut TestAppContext) {
     let (project, fs) = init_test_project_with_git("/project", cx).await;
 
     fs.as_fake()
-        .with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-            state.worktrees.push(git::repository::Worktree {
+        .add_linked_worktree_for_repo(
+            Path::new("/project/.git"),
+            false,
+            git::repository::Worktree {
                 path: std::path::PathBuf::from("/wt/rosewood"),
                 ref_name: Some("refs/heads/rosewood".into()),
                 sha: "abc".into(),
                 is_main: false,
-            });
-        })
-        .unwrap();
+            },
+        )
+        .await;
 
     project
         .update(cx, |project, cx| project.git_scans_complete(cx))
@@ -2635,15 +2626,17 @@ async fn test_git_worktree_added_live_updates_sidebar(cx: &mut TestAppContext) {
 
     // Now add the worktree to the git state and trigger a rescan.
     fs.as_fake()
-        .with_git_state(std::path::Path::new("/project/.git"), true, |state| {
-            state.worktrees.push(git::repository::Worktree {
+        .add_linked_worktree_for_repo(
+            Path::new("/project/.git"),
+            true,
+            git::repository::Worktree {
                 path: std::path::PathBuf::from("/wt/rosewood"),
                 ref_name: Some("refs/heads/rosewood".into()),
                 sha: "abc".into(),
                 is_main: false,
-            });
-        })
-        .unwrap();
+            },
+        )
+        .await;
 
     cx.run_until_parked();
 
@@ -2667,16 +2660,6 @@ async fn test_two_worktree_workspaces_absorbed_when_main_added(cx: &mut TestAppC
         "/project",
         serde_json::json!({
             ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                    "feature-b": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-b",
-                    },
-                },
             },
             "src": {},
         }),
@@ -2684,20 +2667,26 @@ async fn test_two_worktree_workspaces_absorbed_when_main_added(cx: &mut TestAppC
     .await;
 
     // Two worktree checkouts whose .git files point back to the main repo.
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
-            "src": {},
-        }),
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
+            path: std::path::PathBuf::from("/wt-feature-a"),
+            ref_name: Some("refs/heads/feature-a".into()),
+            sha: "aaa".into(),
+            is_main: false,
+        },
     )
     .await;
-    fs.insert_tree(
-        "/wt-feature-b",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-b",
-            "src": {},
-        }),
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
+            path: std::path::PathBuf::from("/wt-feature-b"),
+            ref_name: Some("refs/heads/feature-b".into()),
+            sha: "bbb".into(),
+            is_main: false,
+        },
     )
     .await;
 
@@ -2735,24 +2724,6 @@ async fn test_two_worktree_workspaces_absorbed_when_main_added(cx: &mut TestAppC
         ]
     );
 
-    // Configure the main repo to list both worktrees before opening
-    // it so the initial git scan picks them up.
-    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: Some("refs/heads/feature-a".into()),
-            sha: "aaa".into(),
-            is_main: false,
-        });
-        state.worktrees.push(git::repository::Worktree {
-            path: std::path::PathBuf::from("/wt-feature-b"),
-            ref_name: Some("refs/heads/feature-b".into()),
-            sha: "bbb".into(),
-            is_main: false,
-        });
-    })
-    .unwrap();
-
     let main_project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
     main_project
         .update(cx, |p, cx| p.git_scans_complete(cx))
@@ -2788,54 +2759,33 @@ async fn test_threadless_workspace_shows_new_thread_with_worktree_chip(cx: &mut
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                    "feature-b": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-b",
-                    },
-                },
-            },
-            "src": {},
-        }),
-    )
-    .await;
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
-            "src": {},
-        }),
-    )
-    .await;
-    fs.insert_tree(
-        "/wt-feature-b",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-b",
+            ".git": {},
             "src": {},
         }),
     )
     .await;
-
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-        state.worktrees.push(git::repository::Worktree {
+        },
+    )
+    .await;
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-b"),
             ref_name: Some("refs/heads/feature-b".into()),
             sha: "bbb".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -2884,18 +2834,7 @@ async fn test_multi_worktree_thread_shows_multiple_chips(cx: &mut TestAppContext
     fs.insert_tree(
         "/project_a",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "olivetti": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/olivetti",
-                    },
-                    "selectric": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/selectric",
-                    },
-                },
-            },
+            ".git": {},
             "src": {},
         }),
     )
@@ -2903,56 +2842,28 @@ async fn test_multi_worktree_thread_shows_multiple_chips(cx: &mut TestAppContext
     fs.insert_tree(
         "/project_b",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "olivetti": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/olivetti",
-                    },
-                    "selectric": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/selectric",
-                    },
-                },
-            },
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
     // Worktree checkouts.
-    for (repo, branch) in &[
-        ("project_a", "olivetti"),
-        ("project_a", "selectric"),
-        ("project_b", "olivetti"),
-        ("project_b", "selectric"),
-    ] {
-        let worktree_path = format!("/worktrees/{repo}/{branch}/{repo}");
-        let gitdir = format!("gitdir: /{repo}/.git/worktrees/{branch}");
-        fs.insert_tree(
-            &worktree_path,
-            serde_json::json!({
-                ".git": gitdir,
-                "src": {},
-            }),
-        )
-        .await;
-    }
-
-    // Register linked worktrees.
     for repo in &["project_a", "project_b"] {
         let git_path = format!("/{repo}/.git");
-        fs.with_git_state(std::path::Path::new(&git_path), false, |state| {
-            for branch in &["olivetti", "selectric"] {
-                state.worktrees.push(git::repository::Worktree {
+        for branch in &["olivetti", "selectric"] {
+            fs.add_linked_worktree_for_repo(
+                Path::new(&git_path),
+                false,
+                git::repository::Worktree {
                     path: std::path::PathBuf::from(format!("/worktrees/{repo}/{branch}/{repo}")),
                     ref_name: Some(format!("refs/heads/{branch}").into()),
                     sha: "aaa".into(),
                     is_main: false,
-                });
-            }
-        })
-        .unwrap();
+                },
+            )
+            .await;
+        }
     }
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
@@ -3005,14 +2916,7 @@ async fn test_same_named_worktree_chips_are_deduplicated(cx: &mut TestAppContext
     fs.insert_tree(
         "/project_a",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "olivetti": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/olivetti",
-                    },
-                },
-            },
+            ".git": {},
             "src": {},
         }),
     )
@@ -3020,41 +2924,25 @@ async fn test_same_named_worktree_chips_are_deduplicated(cx: &mut TestAppContext
     fs.insert_tree(
         "/project_b",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "olivetti": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/olivetti",
-                    },
-                },
-            },
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
     for repo in &["project_a", "project_b"] {
-        let worktree_path = format!("/worktrees/{repo}/olivetti/{repo}");
-        let gitdir = format!("gitdir: /{repo}/.git/worktrees/olivetti");
-        fs.insert_tree(
-            &worktree_path,
-            serde_json::json!({
-                ".git": gitdir,
-                "src": {},
-            }),
-        )
-        .await;
-
         let git_path = format!("/{repo}/.git");
-        fs.with_git_state(std::path::Path::new(&git_path), false, |state| {
-            state.worktrees.push(git::repository::Worktree {
+        fs.add_linked_worktree_for_repo(
+            Path::new(&git_path),
+            false,
+            git::repository::Worktree {
                 path: std::path::PathBuf::from(format!("/worktrees/{repo}/olivetti/{repo}")),
                 ref_name: Some("refs/heads/olivetti".into()),
                 sha: "aaa".into(),
                 is_main: false,
-            });
-        })
-        .unwrap();
+            },
+        )
+        .await;
     }
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
@@ -3114,38 +3002,24 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
     // Worktree checkout pointing back to the main repo.
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
-            "src": {},
-        }),
-    )
-    .await;
-
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -3231,37 +3105,23 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
-            "src": {},
-        }),
-    )
-    .await;
-
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -3338,37 +3198,23 @@ async fn test_clicking_worktree_thread_opens_workspace_when_none_exists(cx: &mut
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
-            "src": {},
-        }),
-    )
-    .await;
-
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -3444,37 +3290,23 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
-            "src": {},
-        }),
-    )
-    .await;
-
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -3595,37 +3427,23 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
-            "src": {},
-        }),
-    )
-    .await;
-
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -4190,37 +4008,23 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
-            "src": {},
-        }),
-    )
-    .await;
-
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
+            ".git": {},
             "src": {},
         }),
     )
     .await;
 
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -4354,22 +4158,7 @@ async fn test_linked_worktree_threads_not_duplicated_across_groups(cx: &mut Test
     fs.insert_tree(
         "/project",
         serde_json::json!({
-            ".git": {
-                "worktrees": {
-                    "feature-a": {
-                        "commondir": "../../",
-                        "HEAD": "ref: refs/heads/feature-a",
-                    },
-                },
-            },
-            "src": {},
-        }),
-    )
-    .await;
-    fs.insert_tree(
-        "/wt-feature-a",
-        serde_json::json!({
-            ".git": "gitdir: /project/.git/worktrees/feature-a",
+            ".git": {},
             "src": {},
         }),
     )
@@ -4384,15 +4173,17 @@ async fn test_linked_worktree_threads_not_duplicated_across_groups(cx: &mut Test
     .await;
 
     // Register the linked worktree in the main repo.
-    fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
-        state.worktrees.push(git::repository::Worktree {
+    fs.add_linked_worktree_for_repo(
+        Path::new("/project/.git"),
+        false,
+        git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
             is_main: false,
-        });
-    })
-    .unwrap();
+        },
+    )
+    .await;
 
     cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
 
@@ -5191,15 +4982,17 @@ mod property_test {
                 let worktree_pathbuf = std::path::PathBuf::from(&worktree_path);
                 state
                     .fs
-                    .with_git_state(dot_git_path, false, |git_state| {
-                        git_state.worktrees.push(git::repository::Worktree {
+                    .add_linked_worktree_for_repo(
+                        dot_git_path,
+                        false,
+                        git::repository::Worktree {
                             path: worktree_pathbuf,
                             ref_name: Some(format!("refs/heads/{}", worktree_name).into()),
                             sha: "aaa".into(),
                             is_main: false,
-                        });
-                    })
-                    .unwrap();
+                        },
+                    )
+                    .await;
 
                 // Re-scan the main workspace's project so it discovers the new worktree.
                 let main_workspace =