Clean up worktree setup in tests (#52934)

Release Notes:

- N/A

---------

Co-authored-by: Eric Holk <eric@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-04-01 23:37:12 +00:00
1 parent a0028ae0fa
commit 51bad89a6c
7 files changed
+499 -495

No files matched your search

+44 -42
View File
@@ -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();
@@ -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
+179 -86
View File
@@ -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()
+120 -6
View File
@@ -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,
+1 -1
View File
@@ -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,
+7 -5
View File
@@ -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
}
+142 -349
View File
@@ -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",
},
},
},
".git": {},
"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",
"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",
},
},
},
".git": {},
"src": {},
}),
)
.await;
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));
@@ -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",
},
},
},
".git": {},
"src": {},
}),
)
.await;
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));
@@ -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",
},
},
},
".git": {},
"src": {},
}),
)
.await;
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));
@@ -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",
},
},
},
".git": {},
"src": {},
}),
)
.await;
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));
@@ -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",
},
},
},
".git": {},
"src": {},
}),
)
.await;
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));
@@ -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 =