diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e3b5b0be7e651eb95daefddaf1e2e9c8e34e0c58..764f070f0b8add0baf27f603be4cb122b8024273 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -10,7 +10,7 @@ use editor::{ ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; -use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions}; +use fs::{repository::GitStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; use futures::StreamExt as _; use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, @@ -2690,6 +2690,117 @@ async fn test_git_branch_name( }); } +#[gpui::test] +async fn test_git_status_sync( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".git": {}, + "a.txt": "a", + "b.txt": "b", + }), + ) + .await; + + const A_TXT: &'static str = "a.txt"; + const B_TXT: &'static str = "b.txt"; + + client_a + .fs + .as_fake() + .set_status_for_repo( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitStatus::Added), + (&Path::new(B_TXT), GitStatus::Added), + ], + ) + .await; + + let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| { + call.share_project(project_local.clone(), cx) + }) + .await + .unwrap(); + + let project_remote = client_b.build_remote_project(project_id, cx_b).await; + + // Wait for it to catch up to the new status + deterministic.run_until_parked(); + + #[track_caller] + fn assert_status(file: &impl AsRef, status: Option, project: &Project, cx: &AppContext) { + let file = file.as_ref(); + let worktrees = project.visible_worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree = worktrees[0].clone(); + let snapshot = worktree.read(cx).snapshot(); + let root_entry = snapshot.root_git_entry().unwrap(); + assert_eq!(root_entry.status_for(&snapshot, file), status); + } + + // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + project_remote.read_with(cx_b, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + + client_a + .fs + .as_fake() + .set_status_for_repo( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitStatus::Modified), + (&Path::new(B_TXT), GitStatus::Modified), + ], + ) + .await; + + // Wait for buffer_local_a to receive it + deterministic.run_until_parked(); + + // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + project_remote.read_with(cx_b, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); + + // And synchronization while joining + let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; + project_remote_c.read_with(cx_c, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitStatus::Added), project, cx); + }); +} + #[gpui::test(iterations = 10)] async fn test_fs_operations( deterministic: Arc, diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 945ffaea16a66e754db72bfa8db23dc56f48c424..efc24553c42b47d25bc05c08dde709b1c82af4db 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,7 @@ use git2::Repository as LibGitRepository; use lazy_static::lazy_static; use parking_lot::Mutex; use regex::Regex; -use repository::GitRepository; +use repository::{GitRepository, GitStatus}; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; @@ -654,6 +654,19 @@ impl FakeFs { }); } + pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitStatus)]) { + self.with_git_state(dot_git, |state| { + state.git_statuses.clear(); + state.git_statuses.extend( + statuses + .iter() + .map(|(path, content)| { + ((**path).into(), content.clone()) + }), + ); + }); + } + pub fn paths(&self) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 626fbf9e12a9db07fa8cc7f123b5ad951403296c..7fa20bddcb085427f0bda0340330b63c0c16af26 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -102,6 +102,7 @@ pub struct FakeGitRepository { #[derive(Debug, Clone, Default)] pub struct FakeGitRepositoryState { pub index_contents: HashMap, + pub git_statuses: HashMap, pub branch_name: Option, } @@ -126,11 +127,17 @@ impl GitRepository for FakeGitRepository { } fn statuses(&self) -> Option> { - todo!() + let state = self.state.lock(); + let mut map = TreeMap::default(); + for (repo_path, status) in state.git_statuses.iter() { + map.insert(repo_path.to_owned(), status.to_owned()); + } + Some(map) } - fn file_status(&self, _: &RepoPath) -> Option { - todo!() + fn file_status(&self, path: &RepoPath) -> Option { + let state = self.state.lock(); + state.git_statuses.get(path).cloned() } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 66cef5131b3c42e3c62e3fe3500fc25b81a811aa..82c719f31ecccf4c719542ce426e3b49c3c2c8fd 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -178,6 +178,9 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), + // TODO: Status + removed_statuses: Default::default(), + updated_statuses: Default::default(), } } } @@ -1855,6 +1858,7 @@ impl LocalSnapshot { let scan_id = self.scan_id; let repo_lock = repo.lock(); + self.repository_entries.insert( work_directory, RepositoryEntry { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 220ef22fb729a7842165b9781a18a0dad363e991..abe02f42bbe26ce26451b958563d83fe8afd294d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -986,8 +986,22 @@ message Entry { message RepositoryEntry { uint64 work_directory_id = 1; optional string branch = 2; + repeated uint64 removed_statuses = 3; + repeated StatusEntry updated_statuses = 4; } +message StatusEntry { + uint64 entry_id = 1; + GitStatus status = 2; +} + +enum GitStatus { + Added = 0; + Modified = 1; + Conflict = 2; +} + + message BufferState { uint64 id = 1; optional File file = 2;