use std::{ path::{self, Path, PathBuf}, sync::Arc,}; use call::ActiveCall;use client::RECEIVE_TIMEOUT;use collections::HashMap;use git::{ Oid, repository::{ CommitData, GitCommitTemplate, InitialGraphCommitData, RepoPath, Worktree as GitWorktree, }, status::{DiffStat, FileStatus, StatusCode, TrackedStatus},};use git_ui::git_graph::GitGraph;use git_ui::{git_panel::GitPanel, project_diff::ProjectDiff};use gpui::{ AppContext as _, BackgroundExecutor, Entity, IntoElement as _, SharedString, TestAppContext, VisualContext as _, VisualTestContext, point, px, size,};use project::{ ProjectPath, git_store::{CommitDataState, Repository},};use rand::{SeedableRng, rngs::StdRng};use serde_json::json; use util::{path, rel_path::rel_path};use workspace::{MultiWorkspace, Workspace}; use crate::TestServer; #[gpui::test]async fn test_root_repo_common_dir_sync( 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); // Set up a project whose root IS a git repository. 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; executor.run_until_parked(); // Host should see root_repo_common_dir pointing to .git at the root. let host_common_dir = project_a.read_with(cx_a, |project, cx| { let worktree = project.worktrees(cx).next().unwrap(); worktree.read(cx).snapshot().root_repo_common_dir().cloned() }); assert_eq!( host_common_dir.as_deref(), Some(path::Path::new(path!("/project/.git"))), ); // Share the project and have client B join. 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(); // Guest should see the same root_repo_common_dir as the host. let guest_common_dir = project_b.read_with(cx_b, |project, cx| { let worktree = project.worktrees(cx).next().unwrap(); worktree.read(cx).snapshot().root_repo_common_dir().cloned() }); assert_eq!( guest_common_dir, host_common_dir, "guest should see the same root_repo_common_dir as host", );} fn collect_diff_stats<C: gpui::AppContext>( panel: &gpui::Entity<GitPanel>, cx: &C,) -> HashMap<RepoPath, DiffStat> { panel.read_with(cx, |panel, cx| { let Some(repo) = panel.active_repository() else { return HashMap::default(); }; let snapshot = repo.read(cx).snapshot(); let mut stats = HashMap::default(); for entry in snapshot.statuses_by_path.iter() { if let Some(diff_stat) = entry.diff_stat { stats.insert(entry.repo_path.clone(), diff_stat); } } stats })} async fn load_commit_data_batch( repository: &gpui::Entity<Repository>, shas: &[Oid], executor: &BackgroundExecutor, cx: &mut TestAppContext,) -> HashMap<Oid, CommitData> { let states = cx.update(|cx| { shas.iter() .map(|sha| { ( *sha, repository.update(cx, |repository, cx| { repository.fetch_commit_data(*sha, true, cx).clone() }), ) }) .collect::<Vec<_>>() }); executor.run_until_parked(); let mut commit_data = HashMap::default(); for (sha, state) in states { let data = match state { CommitDataState::Loaded(data) => data.as_ref().clone(), CommitDataState::Loading(Some(shared)) => shared.await.unwrap().as_ref().clone(), CommitDataState::Loading(None) => { panic!("fetch_commit_data(..., true) should return an await-result state") } }; commit_data.insert(sha, data); } commit_data} fn branch_list_snapshot( project: &gpui::Entity<project::Project>, cx: &mut TestAppContext,) -> (Option<String>, Vec<String>) { project.read_with(cx, |project, cx| { let repos = project.repositories(cx); assert_eq!(repos.len(), 1, "project should have exactly 1 repository"); let repo = repos.values().next().unwrap(); let snapshot = repo.read(cx).snapshot(); ( snapshot .branch .as_ref() .map(|branch| branch.name().to_string()), snapshot .branch_list .iter() .map(|branch| branch.ref_name.to_string()) .collect(), ) })} fn build_git_graph( project: &Entity<project::Project>, workspace: &Entity<Workspace>, cx: &mut VisualTestContext,) -> Entity<GitGraph> { let (repository_id, git_store) = project.read_with(cx, |project, cx| { let repository = project .active_repository(cx) .expect("project should have an active repository"); (repository.read(cx).id, project.git_store().clone()) }); let workspace = workspace.downgrade(); cx.new_window_entity(|window, cx| { GitGraph::new(repository_id, git_store, workspace, None, window, cx) })} fn render_git_graph(graph: &Entity<GitGraph>, cx: &mut VisualTestContext) { cx.draw(point(px(0.), px(0.)), size(px(1200.), px(800.)), |_, _| { graph.clone().into_any_element() }); cx.run_until_parked();} fn assert_initial_graph_commits_eq( actual: &[Arc<InitialGraphCommitData>], expected: &[Arc<InitialGraphCommitData>],) { assert_eq!(actual.len(), expected.len(), "commit count should match"); for (index, (actual, expected)) in actual.iter().zip(expected).enumerate() { assert_eq!( actual.sha, expected.sha, "sha should match at index {index}" ); assert_eq!( actual.parents, expected.parents, "parents should match at index {index}" ); assert_eq!( actual.ref_names, expected.ref_names, "ref names should match at index {index}" ); }} fn assert_remote_cache_matches_local_cache( local_repository: &gpui::Entity<Repository>, remote_repository: &gpui::Entity<Repository>, cx_local: &mut TestAppContext, cx_remote: &mut TestAppContext,) { let local_cache = cx_local.update(|cx| { local_repository.update(cx, |repository, _| repository.loaded_commit_data_for_test()) }); let remote_cache = cx_remote.update(|cx| { remote_repository.update(cx, |repository, _| repository.loaded_commit_data_for_test()) }); for (sha, remote_commit_data) in &remote_cache { let local_commit_data = local_cache .get(sha) .unwrap_or_else(|| panic!("local cache missing commit data for {sha}")); assert_eq!( local_commit_data.sha, remote_commit_data.sha, "local and remote cache should agree on sha for {sha}" ); assert_eq!( local_commit_data.parents, remote_commit_data.parents, "local and remote cache should agree on parents for {sha}" ); assert_eq!( local_commit_data.author_name, remote_commit_data.author_name, "local and remote cache should agree on author_name for {sha}" ); assert_eq!( local_commit_data.author_email, remote_commit_data.author_email, "local and remote cache should agree on author_email for {sha}" ); assert_eq!( local_commit_data.commit_timestamp, remote_commit_data.commit_timestamp, "local and remote cache should agree on commit_timestamp for {sha}" ); assert_eq!( local_commit_data.subject, remote_commit_data.subject, "local and remote cache should agree on subject for {sha}" ); assert_eq!( local_commit_data.message, remote_commit_data.message, "local and remote cache should agree on message for {sha}" ); }} #[gpui::test]async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.background_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; cx_a.set_name("cx_a"); cx_b.set_name("cx_b"); server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a .fs() .insert_tree( path!("/a"), json!({ ".git": {}, "changed.txt": "after\n", "unchanged.txt": "unchanged\n", "created.txt": "created\n", "secret.pem": "secret-changed\n", }), ) .await; client_a.fs().set_head_and_index_for_repo( Path::new(path!("/a/.git")), &[ ("changed.txt", "before\n".to_string()), ("unchanged.txt", "unchanged\n".to_string()), ("deleted.txt", "deleted\n".to_string()), ("secret.pem", "shh\n".to_string()), ], ); let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), 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(); cx_b.update(editor::init); cx_b.update(git_ui::init); let project_b = client_b.join_remote_project(project_id, cx_b).await; let window_b = cx_b.add_window(|window, cx| { let workspace = cx.new(|cx| { Workspace::new( None, project_b.clone(), client_b.app_state.clone(), window, cx, ) }); MultiWorkspace::new(workspace, window, cx) }); let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b); let workspace_b = window_b .root(cx_b) .unwrap() .read_with(cx_b, |multi_workspace, _| { multi_workspace.workspace().clone() }); cx_b.update(|window, cx| { window .focused(cx) .unwrap() .dispatch_action(&git_ui::project_diff::Diff, window, cx) }); let diff = workspace_b.update(cx_b, |workspace, cx| { workspace.active_item(cx).unwrap().act_as::<ProjectDiff>(cx) }); let diff = diff.unwrap(); cx_b.run_until_parked(); diff.update(cx_b, |diff, cx| { assert_eq!( diff.excerpt_paths(cx), vec![ rel_path("changed.txt").into_arc(), rel_path("deleted.txt").into_arc(), rel_path("created.txt").into_arc() ] ); }); client_a .fs() .insert_tree( path!("/a"), json!({ ".git": {}, "changed.txt": "before\n", "unchanged.txt": "changed\n", "created.txt": "created\n", "secret.pem": "secret-changed\n", }), ) .await; cx_b.run_until_parked(); project_b.update(cx_b, |project, cx| { let project_path = ProjectPath { worktree_id, path: rel_path("unchanged.txt").into(), }; let status = project.project_path_git_status(&project_path, cx); assert_eq!( status.unwrap(), FileStatus::Tracked(TrackedStatus { worktree_status: StatusCode::Modified, index_status: StatusCode::Unmodified, }) ); }); diff.update(cx_b, |diff, cx| { assert_eq!( diff.excerpt_paths(cx), vec![ rel_path("deleted.txt").into_arc(), rel_path("unchanged.txt").into_arc(), rel_path("created.txt").into_arc() ] ); });} #[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( git::repository::CreateWorktreeTarget::NewBranch { branch_name: "feature-branch".to_string(), base_sha: Some("abc123".to_string()), }, worktree_directory.join("feature-branch"), ) }) }) .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, Some("refs/heads/feature-branch".into()) ); assert_eq!(worktrees[1].sha.as_ref(), "abc123"); // Verify from the host side that the worktree was actually created 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( git::repository::CreateWorktreeTarget::NewBranch { branch_name: "bugfix-branch".to_string(), base_sha: None, }, worktree_directory.join("bugfix-branch"), )Lines 1–500 of 1464. Load more
use std::{ path::{self, Path, PathBuf}, sync::Arc, }; use call::ActiveCall; use client::RECEIVE_TIMEOUT; use collections::HashMap; use git::{ Oid, repository::{ CommitData, GitCommitTemplate, InitialGraphCommitData, RepoPath, Worktree as GitWorktree, }, status::{DiffStat, FileStatus, StatusCode, TrackedStatus}, }; use git_ui::git_graph::GitGraph; use git_ui::{git_panel::GitPanel, project_diff::ProjectDiff}; use gpui::{ AppContext as _, BackgroundExecutor, Entity, IntoElement as _, SharedString, TestAppContext, VisualContext as _, VisualTestContext, point, px, size, }; use project::{ ProjectPath, git_store::{CommitDataState, Repository}, }; use rand::{SeedableRng, rngs::StdRng}; use serde_json::json; use util::{path, rel_path::rel_path}; use workspace::{MultiWorkspace, Workspace}; use crate::TestServer; #[gpui::test] async fn test_root_repo_common_dir_sync( 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); // Set up a project whose root IS a git repository. 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; executor.run_until_parked(); // Host should see root_repo_common_dir pointing to .git at the root. let host_common_dir = project_a.read_with(cx_a, |project, cx| { let worktree = project.worktrees(cx).next().unwrap(); worktree.read(cx).snapshot().root_repo_common_dir().cloned() }); assert_eq!( host_common_dir.as_deref(), Some(path::Path::new(path!("/project/.git"))), ); // Share the project and have client B join. 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(); // Guest should see the same root_repo_common_dir as the host. let guest_common_dir = project_b.read_with(cx_b, |project, cx| { let worktree = project.worktrees(cx).next().unwrap(); worktree.read(cx).snapshot().root_repo_common_dir().cloned() }); assert_eq!( guest_common_dir, host_common_dir, "guest should see the same root_repo_common_dir as host", ); } fn collect_diff_stats<C: gpui::AppContext>( panel: &gpui::Entity<GitPanel>, cx: &C, ) -> HashMap<RepoPath, DiffStat> { panel.read_with(cx, |panel, cx| { let Some(repo) = panel.active_repository() else { return HashMap::default(); }; let snapshot = repo.read(cx).snapshot(); let mut stats = HashMap::default(); for entry in snapshot.statuses_by_path.iter() { if let Some(diff_stat) = entry.diff_stat { stats.insert(entry.repo_path.clone(), diff_stat); } } stats }) } async fn load_commit_data_batch( repository: &gpui::Entity<Repository>, shas: &[Oid], executor: &BackgroundExecutor, cx: &mut TestAppContext, ) -> HashMap<Oid, CommitData> { let states = cx.update(|cx| { shas.iter() .map(|sha| { ( *sha, repository.update(cx, |repository, cx| { repository.fetch_commit_data(*sha, true, cx).clone() }), ) }) .collect::<Vec<_>>() }); executor.run_until_parked(); let mut commit_data = HashMap::default(); for (sha, state) in states { let data = match state { CommitDataState::Loaded(data) => data.as_ref().clone(), CommitDataState::Loading(Some(shared)) => shared.await.unwrap().as_ref().clone(), CommitDataState::Loading(None) => { panic!("fetch_commit_data(..., true) should return an await-result state") } }; commit_data.insert(sha, data); } commit_data } fn branch_list_snapshot( project: &gpui::Entity<project::Project>, cx: &mut TestAppContext, ) -> (Option<String>, Vec<String>) { project.read_with(cx, |project, cx| { let repos = project.repositories(cx); assert_eq!(repos.len(), 1, "project should have exactly 1 repository"); let repo = repos.values().next().unwrap(); let snapshot = repo.read(cx).snapshot(); ( snapshot .branch .as_ref() .map(|branch| branch.name().to_string()), snapshot .branch_list .iter() .map(|branch| branch.ref_name.to_string()) .collect(), ) }) } fn build_git_graph( project: &Entity<project::Project>, workspace: &Entity<Workspace>, cx: &mut VisualTestContext, ) -> Entity<GitGraph> { let (repository_id, git_store) = project.read_with(cx, |project, cx| { let repository = project .active_repository(cx) .expect("project should have an active repository"); (repository.read(cx).id, project.git_store().clone()) }); let workspace = workspace.downgrade(); cx.new_window_entity(|window, cx| { GitGraph::new(repository_id, git_store, workspace, None, window, cx) }) } fn render_git_graph(graph: &Entity<GitGraph>, cx: &mut VisualTestContext) { cx.draw(point(px(0.), px(0.)), size(px(1200.), px(800.)), |_, _| { graph.clone().into_any_element() }); cx.run_until_parked(); } fn assert_initial_graph_commits_eq( actual: &[Arc<InitialGraphCommitData>], expected: &[Arc<InitialGraphCommitData>], ) { assert_eq!(actual.len(), expected.len(), "commit count should match"); for (index, (actual, expected)) in actual.iter().zip(expected).enumerate() { assert_eq!( actual.sha, expected.sha, "sha should match at index {index}" ); assert_eq!( actual.parents, expected.parents, "parents should match at index {index}" ); assert_eq!( actual.ref_names, expected.ref_names, "ref names should match at index {index}" ); } } fn assert_remote_cache_matches_local_cache( local_repository: &gpui::Entity<Repository>, remote_repository: &gpui::Entity<Repository>, cx_local: &mut TestAppContext, cx_remote: &mut TestAppContext, ) { let local_cache = cx_local.update(|cx| { local_repository.update(cx, |repository, _| repository.loaded_commit_data_for_test()) }); let remote_cache = cx_remote.update(|cx| { remote_repository.update(cx, |repository, _| repository.loaded_commit_data_for_test()) }); for (sha, remote_commit_data) in &remote_cache { let local_commit_data = local_cache .get(sha) .unwrap_or_else(|| panic!("local cache missing commit data for {sha}")); assert_eq!( local_commit_data.sha, remote_commit_data.sha, "local and remote cache should agree on sha for {sha}" ); assert_eq!( local_commit_data.parents, remote_commit_data.parents, "local and remote cache should agree on parents for {sha}" ); assert_eq!( local_commit_data.author_name, remote_commit_data.author_name, "local and remote cache should agree on author_name for {sha}" ); assert_eq!( local_commit_data.author_email, remote_commit_data.author_email, "local and remote cache should agree on author_email for {sha}" ); assert_eq!( local_commit_data.commit_timestamp, remote_commit_data.commit_timestamp, "local and remote cache should agree on commit_timestamp for {sha}" ); assert_eq!( local_commit_data.subject, remote_commit_data.subject, "local and remote cache should agree on subject for {sha}" ); assert_eq!( local_commit_data.message, remote_commit_data.message, "local and remote cache should agree on message for {sha}" ); } } #[gpui::test] async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.background_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; cx_a.set_name("cx_a"); cx_b.set_name("cx_b"); server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a .fs() .insert_tree( path!("/a"), json!({ ".git": {}, "changed.txt": "after\n", "unchanged.txt": "unchanged\n", "created.txt": "created\n", "secret.pem": "secret-changed\n", }), ) .await; client_a.fs().set_head_and_index_for_repo( Path::new(path!("/a/.git")), &[ ("changed.txt", "before\n".to_string()), ("unchanged.txt", "unchanged\n".to_string()), ("deleted.txt", "deleted\n".to_string()), ("secret.pem", "shh\n".to_string()), ], ); let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), 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(); cx_b.update(editor::init); cx_b.update(git_ui::init); let project_b = client_b.join_remote_project(project_id, cx_b).await; let window_b = cx_b.add_window(|window, cx| { let workspace = cx.new(|cx| { Workspace::new( None, project_b.clone(), client_b.app_state.clone(), window, cx, ) }); MultiWorkspace::new(workspace, window, cx) }); let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b); let workspace_b = window_b .root(cx_b) .unwrap() .read_with(cx_b, |multi_workspace, _| { multi_workspace.workspace().clone() }); cx_b.update(|window, cx| { window .focused(cx) .unwrap() .dispatch_action(&git_ui::project_diff::Diff, window, cx) }); let diff = workspace_b.update(cx_b, |workspace, cx| { workspace.active_item(cx).unwrap().act_as::<ProjectDiff>(cx) }); let diff = diff.unwrap(); cx_b.run_until_parked(); diff.update(cx_b, |diff, cx| { assert_eq!( diff.excerpt_paths(cx), vec![ rel_path("changed.txt").into_arc(), rel_path("deleted.txt").into_arc(), rel_path("created.txt").into_arc() ] ); }); client_a .fs() .insert_tree( path!("/a"), json!({ ".git": {}, "changed.txt": "before\n", "unchanged.txt": "changed\n", "created.txt": "created\n", "secret.pem": "secret-changed\n", }), ) .await; cx_b.run_until_parked(); project_b.update(cx_b, |project, cx| { let project_path = ProjectPath { worktree_id, path: rel_path("unchanged.txt").into(), }; let status = project.project_path_git_status(&project_path, cx); assert_eq!( status.unwrap(), FileStatus::Tracked(TrackedStatus { worktree_status: StatusCode::Modified, index_status: StatusCode::Unmodified, }) ); }); diff.update(cx_b, |diff, cx| { assert_eq!( diff.excerpt_paths(cx), vec![ rel_path("deleted.txt").into_arc(), rel_path("unchanged.txt").into_arc(), rel_path("created.txt").into_arc() ] ); }); } #[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( git::repository::CreateWorktreeTarget::NewBranch { branch_name: "feature-branch".to_string(), base_sha: Some("abc123".to_string()), }, worktree_directory.join("feature-branch"), ) }) }) .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, Some("refs/heads/feature-branch".into()) ); assert_eq!(worktrees[1].sha.as_ref(), "abc123"); // Verify from the host side that the worktree was actually created 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( git::repository::CreateWorktreeTarget::NewBranch { branch_name: "bugfix-branch".to_string(), base_sha: None, }, worktree_directory.join("bugfix-branch"), )