Detailed changes
@@ -437,6 +437,8 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GitCreateRemote>)
.add_request_handler(forward_mutating_project_request::<proto::GitRemoveRemote>)
+ .add_request_handler(forward_read_only_project_request::<proto::GitGetWorktrees>)
+ .add_request_handler(forward_mutating_project_request::<proto::GitCreateWorktree>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context)
@@ -1,9 +1,9 @@
-use std::path::Path;
+use std::path::{Path, PathBuf};
use call::ActiveCall;
use git::status::{FileStatus, StatusCode, TrackedStatus};
use git_ui::project_diff::ProjectDiff;
-use gpui::{AppContext as _, TestAppContext, VisualTestContext};
+use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, VisualTestContext};
use project::ProjectPath;
use serde_json::json;
use util::{path, rel_path::rel_path};
@@ -141,3 +141,142 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
);
});
}
+
+#[gpui::test]
+async fn test_remote_git_worktrees(
+ executor: BackgroundExecutor,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ client_a
+ .fs()
+ .insert_tree(
+ path!("/project"),
+ json!({ ".git": {}, "file.txt": "content" }),
+ )
+ .await;
+
+ let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+ executor.run_until_parked();
+
+ let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
+
+ // Initially only the main worktree (the repo itself) should be present
+ let worktrees = cx_b
+ .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(worktrees.len(), 1);
+ assert_eq!(worktrees[0].path, PathBuf::from(path!("/project")));
+
+ // Client B creates a git worktree via the remote project
+ let worktree_directory = PathBuf::from(path!("/project"));
+ cx_b.update(|cx| {
+ repo_b.update(cx, |repository, _| {
+ repository.create_worktree(
+ "feature-branch".to_string(),
+ worktree_directory.clone(),
+ Some("abc123".to_string()),
+ )
+ })
+ })
+ .await
+ .unwrap()
+ .unwrap();
+
+ executor.run_until_parked();
+
+ // Client B lists worktrees — should see main + the one just created
+ let worktrees = cx_b
+ .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
+ .await
+ .unwrap()
+ .unwrap();
+ 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].sha.as_ref(), "abc123");
+
+ // Verify from the host side that the worktree was actually created
+ let host_worktrees = {
+ let repo_a = cx_a.update(|cx| {
+ project_a
+ .read(cx)
+ .repositories(cx)
+ .values()
+ .next()
+ .unwrap()
+ .clone()
+ });
+ cx_a.update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
+ .await
+ .unwrap()
+ .unwrap()
+ };
+ assert_eq!(host_worktrees.len(), 2);
+ assert_eq!(host_worktrees[0].path, PathBuf::from(path!("/project")));
+ assert_eq!(
+ host_worktrees[1].path,
+ worktree_directory.join("feature-branch")
+ );
+
+ // Client B creates a second git worktree without an explicit commit
+ cx_b.update(|cx| {
+ repo_b.update(cx, |repository, _| {
+ repository.create_worktree(
+ "bugfix-branch".to_string(),
+ worktree_directory.clone(),
+ None,
+ )
+ })
+ })
+ .await
+ .unwrap()
+ .unwrap();
+
+ executor.run_until_parked();
+
+ // Client B lists worktrees — should now have main + two created
+ let worktrees = cx_b
+ .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(worktrees.len(), 3);
+
+ let feature_worktree = worktrees
+ .iter()
+ .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/feature-branch")
+ .expect("should find feature-branch worktree");
+ assert_eq!(
+ feature_worktree.path,
+ worktree_directory.join("feature-branch")
+ );
+
+ let bugfix_worktree = worktrees
+ .iter()
+ .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/bugfix-branch")
+ .expect("should find bugfix-branch worktree");
+ assert_eq!(
+ bugfix_worktree.path,
+ worktree_directory.join("bugfix-branch")
+ );
+ assert_eq!(bugfix_worktree.sha.as_ref(), "fake-sha");
+}
@@ -33,7 +33,7 @@ use settings::{
SettingsStore,
};
use std::{
- path::Path,
+ path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
@@ -396,6 +396,130 @@ async fn test_ssh_collaboration_git_branches(
});
}
+#[gpui::test]
+async fn test_ssh_collaboration_git_worktrees(
+ executor: BackgroundExecutor,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ server_cx: &mut TestAppContext,
+) {
+ cx_a.set_name("a");
+ cx_b.set_name("b");
+ server_cx.set_name("server");
+
+ cx_a.update(|cx| {
+ release_channel::init(semver::Version::new(0, 0, 0), cx);
+ });
+ server_cx.update(|cx| {
+ release_channel::init(semver::Version::new(0, 0, 0), cx);
+ });
+
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+
+ let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
+ let remote_fs = FakeFs::new(server_cx.executor());
+ remote_fs
+ .insert_tree("/project", json!({ ".git": {}, "file.txt": "content" }))
+ .await;
+
+ server_cx.update(HeadlessProject::init);
+ let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
+ let headless_project = server_cx.new(|cx| {
+ HeadlessProject::new(
+ HeadlessAppState {
+ session: server_ssh,
+ fs: remote_fs.clone(),
+ http_client: Arc::new(BlockedHttpClient),
+ node_runtime: NodeRuntime::unavailable(),
+ languages,
+ extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
+ startup_time: std::time::Instant::now(),
+ },
+ false,
+ cx,
+ )
+ });
+
+ let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
+ let (project_a, _) = client_a
+ .build_ssh_project("/project", client_ssh, false, cx_a)
+ .await;
+
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+ executor.run_until_parked();
+
+ let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
+
+ let worktrees = cx_b
+ .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees()))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(worktrees.len(), 1);
+
+ let worktree_directory = PathBuf::from("/project");
+ cx_b.update(|cx| {
+ repo_b.update(cx, |repo, _| {
+ repo.create_worktree(
+ "feature-branch".to_string(),
+ worktree_directory.clone(),
+ Some("abc123".to_string()),
+ )
+ })
+ })
+ .await
+ .unwrap()
+ .unwrap();
+
+ executor.run_until_parked();
+
+ let worktrees = cx_b
+ .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees()))
+ .await
+ .unwrap()
+ .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].sha.as_ref(), "abc123");
+
+ let server_worktrees = {
+ let server_repo = server_cx.update(|cx| {
+ headless_project.update(cx, |headless_project, cx| {
+ headless_project
+ .git_store
+ .read(cx)
+ .repositories()
+ .values()
+ .next()
+ .unwrap()
+ .clone()
+ })
+ });
+ server_cx
+ .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
+ .await
+ .unwrap()
+ .unwrap()
+ };
+ assert_eq!(server_worktrees.len(), 2);
+ assert_eq!(
+ server_worktrees[1].path,
+ worktree_directory.join("feature-branch")
+ );
+}
+
#[gpui::test]
async fn test_ssh_collaboration_formatting_with_prettier(
executor: BackgroundExecutor,
@@ -58,4 +58,4 @@ gpui = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
[features]
-test-support = ["gpui/test-support", "git/test-support"]
+test-support = ["gpui/test-support", "git/test-support", "util/test-support"]
@@ -406,7 +406,31 @@ impl GitRepository for FakeGitRepository {
}
fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
- self.with_state_async(false, |state| Ok(state.worktrees.clone()))
+ 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: branch_ref.into(),
+ sha: head_sha.into(),
+ };
+ let mut all = vec![main_worktree];
+ all.extend(state.worktrees.iter().cloned());
+ Ok(all)
+ })
}
fn create_worktree(
@@ -1012,145 +1036,3 @@ impl GitRepository for FakeGitRepository {
anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
}
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{FakeFs, Fs};
- use gpui::TestAppContext;
- use serde_json::json;
- use std::path::Path;
-
- #[gpui::test]
- async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) {
- let worktree_dir_settings = &["../worktrees", ".git/zed-worktrees", "my-worktrees/"];
-
- for worktree_dir_setting in worktree_dir_settings {
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree("/project", json!({".git": {}, "file.txt": "content"}))
- .await;
- let repo = fs
- .open_repo(Path::new("/project/.git"), None)
- .expect("should open fake repo");
-
- // Initially no worktrees
- let worktrees = repo.worktrees().await.unwrap();
- assert!(worktrees.is_empty());
-
- let expected_dir = git::repository::resolve_worktree_directory(
- Path::new("/project"),
- worktree_dir_setting,
- );
-
- // Create a worktree
- repo.create_worktree(
- "feature-branch".to_string(),
- expected_dir.clone(),
- Some("abc123".to_string()),
- )
- .await
- .unwrap();
-
- // List worktrees — should have one
- let worktrees = repo.worktrees().await.unwrap();
- assert_eq!(worktrees.len(), 1);
- assert_eq!(
- worktrees[0].path,
- expected_dir.join("feature-branch"),
- "failed for worktree_directory setting: {worktree_dir_setting:?}"
- );
- assert_eq!(worktrees[0].ref_name.as_ref(), "refs/heads/feature-branch");
- assert_eq!(worktrees[0].sha.as_ref(), "abc123");
-
- // Directory should exist in FakeFs after create
- assert!(
- fs.is_dir(&expected_dir.join("feature-branch")).await,
- "worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}"
- );
-
- // Create a second worktree (without explicit commit)
- repo.create_worktree("bugfix-branch".to_string(), expected_dir.clone(), None)
- .await
- .unwrap();
-
- let worktrees = repo.worktrees().await.unwrap();
- assert_eq!(worktrees.len(), 2);
- assert!(
- fs.is_dir(&expected_dir.join("bugfix-branch")).await,
- "second worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}"
- );
-
- // Rename the first worktree
- repo.rename_worktree(
- expected_dir.join("feature-branch"),
- expected_dir.join("renamed-branch"),
- )
- .await
- .unwrap();
-
- let worktrees = repo.worktrees().await.unwrap();
- assert_eq!(worktrees.len(), 2);
- assert!(
- worktrees
- .iter()
- .any(|w| w.path == expected_dir.join("renamed-branch")),
- "renamed worktree should exist at new path for setting {worktree_dir_setting:?}"
- );
- assert!(
- worktrees
- .iter()
- .all(|w| w.path != expected_dir.join("feature-branch")),
- "old path should no longer exist for setting {worktree_dir_setting:?}"
- );
-
- // Directory should be moved in FakeFs after rename
- assert!(
- !fs.is_dir(&expected_dir.join("feature-branch")).await,
- "old worktree directory should not exist after rename for setting {worktree_dir_setting:?}"
- );
- assert!(
- fs.is_dir(&expected_dir.join("renamed-branch")).await,
- "new worktree directory should exist after rename for setting {worktree_dir_setting:?}"
- );
-
- // Rename a nonexistent worktree should fail
- let result = repo
- .rename_worktree(PathBuf::from("/nonexistent"), PathBuf::from("/somewhere"))
- .await;
- assert!(result.is_err());
-
- // Remove a worktree
- repo.remove_worktree(expected_dir.join("renamed-branch"), false)
- .await
- .unwrap();
-
- let worktrees = repo.worktrees().await.unwrap();
- assert_eq!(worktrees.len(), 1);
- assert_eq!(worktrees[0].path, expected_dir.join("bugfix-branch"));
-
- // Directory should be removed from FakeFs after remove
- assert!(
- !fs.is_dir(&expected_dir.join("renamed-branch")).await,
- "worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}"
- );
-
- // Remove a nonexistent worktree should fail
- let result = repo
- .remove_worktree(PathBuf::from("/nonexistent"), false)
- .await;
- assert!(result.is_err());
-
- // Remove the last worktree
- repo.remove_worktree(expected_dir.join("bugfix-branch"), false)
- .await
- .unwrap();
-
- let worktrees = repo.worktrees().await.unwrap();
- assert!(worktrees.is_empty());
- assert!(
- !fs.is_dir(&expected_dir.join("bugfix-branch")).await,
- "last worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}"
- );
- }
- }
-}
@@ -1,9 +1,146 @@
use fs::{FakeFs, Fs};
-use gpui::BackgroundExecutor;
+use gpui::{BackgroundExecutor, TestAppContext};
use serde_json::json;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use util::path;
+#[gpui::test]
+async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) {
+ let worktree_dir_settings = &["../worktrees", ".git/zed-worktrees", "my-worktrees/"];
+
+ for worktree_dir_setting in worktree_dir_settings {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree("/project", json!({".git": {}, "file.txt": "content"}))
+ .await;
+ let repo = fs
+ .open_repo(Path::new("/project/.git"), None)
+ .expect("should open fake repo");
+
+ // Initially only the main worktree exists
+ let worktrees = repo.worktrees().await.unwrap();
+ assert_eq!(worktrees.len(), 1);
+ assert_eq!(worktrees[0].path, PathBuf::from("/project"));
+
+ let expected_dir = git::repository::resolve_worktree_directory(
+ Path::new("/project"),
+ worktree_dir_setting,
+ );
+
+ // Create a worktree
+ repo.create_worktree(
+ "feature-branch".to_string(),
+ expected_dir.clone(),
+ Some("abc123".to_string()),
+ )
+ .await
+ .unwrap();
+
+ // List worktrees — should have main + one created
+ let worktrees = repo.worktrees().await.unwrap();
+ assert_eq!(worktrees.len(), 2);
+ assert_eq!(worktrees[0].path, PathBuf::from("/project"));
+ assert_eq!(
+ worktrees[1].path,
+ expected_dir.join("feature-branch"),
+ "failed for worktree_directory setting: {worktree_dir_setting:?}"
+ );
+ assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
+ assert_eq!(worktrees[1].sha.as_ref(), "abc123");
+
+ // Directory should exist in FakeFs after create
+ assert!(
+ fs.is_dir(&expected_dir.join("feature-branch")).await,
+ "worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}"
+ );
+
+ // Create a second worktree (without explicit commit)
+ repo.create_worktree("bugfix-branch".to_string(), expected_dir.clone(), None)
+ .await
+ .unwrap();
+
+ let worktrees = repo.worktrees().await.unwrap();
+ assert_eq!(worktrees.len(), 3);
+ assert!(
+ fs.is_dir(&expected_dir.join("bugfix-branch")).await,
+ "second worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}"
+ );
+
+ // Rename the first worktree
+ repo.rename_worktree(
+ expected_dir.join("feature-branch"),
+ expected_dir.join("renamed-branch"),
+ )
+ .await
+ .unwrap();
+
+ let worktrees = repo.worktrees().await.unwrap();
+ assert_eq!(worktrees.len(), 3);
+ assert!(
+ worktrees
+ .iter()
+ .any(|w| w.path == expected_dir.join("renamed-branch")),
+ "renamed worktree should exist at new path for setting {worktree_dir_setting:?}"
+ );
+ assert!(
+ worktrees
+ .iter()
+ .all(|w| w.path != expected_dir.join("feature-branch")),
+ "old path should no longer exist for setting {worktree_dir_setting:?}"
+ );
+
+ // Directory should be moved in FakeFs after rename
+ assert!(
+ !fs.is_dir(&expected_dir.join("feature-branch")).await,
+ "old worktree directory should not exist after rename for setting {worktree_dir_setting:?}"
+ );
+ assert!(
+ fs.is_dir(&expected_dir.join("renamed-branch")).await,
+ "new worktree directory should exist after rename for setting {worktree_dir_setting:?}"
+ );
+
+ // Rename a nonexistent worktree should fail
+ let result = repo
+ .rename_worktree(PathBuf::from("/nonexistent"), PathBuf::from("/somewhere"))
+ .await;
+ assert!(result.is_err());
+
+ // Remove a worktree
+ repo.remove_worktree(expected_dir.join("renamed-branch"), false)
+ .await
+ .unwrap();
+
+ let worktrees = repo.worktrees().await.unwrap();
+ assert_eq!(worktrees.len(), 2);
+ assert_eq!(worktrees[0].path, PathBuf::from("/project"));
+ assert_eq!(worktrees[1].path, expected_dir.join("bugfix-branch"));
+
+ // Directory should be removed from FakeFs after remove
+ assert!(
+ !fs.is_dir(&expected_dir.join("renamed-branch")).await,
+ "worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}"
+ );
+
+ // Remove a nonexistent worktree should fail
+ let result = repo
+ .remove_worktree(PathBuf::from("/nonexistent"), false)
+ .await;
+ assert!(result.is_err());
+
+ // Remove the last worktree
+ repo.remove_worktree(expected_dir.join("bugfix-branch"), false)
+ .await
+ .unwrap();
+
+ let worktrees = repo.worktrees().await.unwrap();
+ assert_eq!(worktrees.len(), 1);
+ assert_eq!(worktrees[0].path, PathBuf::from("/project"));
+ assert!(
+ !fs.is_dir(&expected_dir.join("bugfix-branch")).await,
+ "last worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}"
+ );
+ }
+}
+
#[gpui::test]
async fn test_checkpoints(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor);
@@ -303,6 +303,7 @@ impl Branch {
pub struct Worktree {
pub path: PathBuf,
pub ref_name: SharedString,
+ // todo(git_worktree) This type should be a Oid
pub sha: SharedString,
}
@@ -340,6 +341,8 @@ 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) {
worktrees.push(Worktree {
path: PathBuf::from(path),
@@ -1174,3 +1174,122 @@ mod git_traversal {
pretty_assertions::assert_eq!(found_statuses, expected_statuses);
}
}
+
+mod git_worktrees {
+ use std::path::PathBuf;
+
+ use fs::FakeFs;
+ use gpui::TestAppContext;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use util::path;
+
+ fn init_test(cx: &mut gpui::TestAppContext) {
+ zlog::init_test();
+
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_git_worktrees_list_and_create(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ path!("/root"),
+ json!({
+ ".git": {},
+ "file.txt": "content",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ let worktrees = cx
+ .update(|cx| repository.update(cx, |repository, _| repository.worktrees()))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(worktrees.len(), 1);
+ assert_eq!(worktrees[0].path, PathBuf::from(path!("/root")));
+
+ let worktree_directory = PathBuf::from(path!("/root"));
+ cx.update(|cx| {
+ repository.update(cx, |repository, _| {
+ repository.create_worktree(
+ "feature-branch".to_string(),
+ worktree_directory.clone(),
+ Some("abc123".to_string()),
+ )
+ })
+ })
+ .await
+ .unwrap()
+ .unwrap();
+
+ cx.executor().run_until_parked();
+
+ let worktrees = cx
+ .update(|cx| repository.update(cx, |repository, _| repository.worktrees()))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(worktrees.len(), 2);
+ assert_eq!(worktrees[0].path, PathBuf::from(path!("/root")));
+ 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].sha.as_ref(), "abc123");
+
+ cx.update(|cx| {
+ repository.update(cx, |repository, _| {
+ repository.create_worktree(
+ "bugfix-branch".to_string(),
+ worktree_directory.clone(),
+ None,
+ )
+ })
+ })
+ .await
+ .unwrap()
+ .unwrap();
+
+ cx.executor().run_until_parked();
+
+ // List worktrees — should now have main + two created
+ let worktrees = cx
+ .update(|cx| repository.update(cx, |repository, _| repository.worktrees()))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(worktrees.len(), 3);
+
+ let feature_worktree = worktrees
+ .iter()
+ .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/feature-branch")
+ .expect("should find feature-branch worktree");
+ assert_eq!(
+ feature_worktree.path,
+ worktree_directory.join("feature-branch")
+ );
+
+ let bugfix_worktree = worktrees
+ .iter()
+ .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/bugfix-branch")
+ .expect("should find bugfix-branch worktree");
+ assert_eq!(
+ bugfix_worktree.path,
+ worktree_directory.join("bugfix-branch")
+ );
+ assert_eq!(bugfix_worktree.sha.as_ref(), "fake-sha");
+ }
+
+ use crate::Project;
+}