WIP: integrate status with collab

Mikayla Maki created

Change summary

crates/collab/src/tests/integration_tests.rs | 113 +++++++++++++++++++++
crates/fs/src/fs.rs                          |  15 ++
crates/fs/src/repository.rs                  |  13 +
crates/project/src/worktree.rs               |   4 
crates/rpc/proto/zed.proto                   |  14 ++
5 files changed, 154 insertions(+), 5 deletions(-)

Detailed changes

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<Deterministic>,
+    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<Path>, status: Option<GitStatus>, project: &Project, cx: &AppContext) {
+        let file = file.as_ref();
+        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
+        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<Deterministic>,

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<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();

crates/fs/src/repository.rs 🔗

@@ -102,6 +102,7 @@ pub struct FakeGitRepository {
 #[derive(Debug, Clone, Default)]
 pub struct FakeGitRepositoryState {
     pub index_contents: HashMap<PathBuf, String>,
+    pub git_statuses: HashMap<RepoPath, GitStatus>,
     pub branch_name: Option<String>,
 }
 
@@ -126,11 +127,17 @@ impl GitRepository for FakeGitRepository {
     }
 
     fn statuses(&self) -> Option<TreeMap<RepoPath, GitStatus>> {
-        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<GitStatus> {
-        todo!()
+    fn file_status(&self, path: &RepoPath) -> Option<GitStatus> {
+        let state = self.state.lock();
+        state.git_statuses.get(path).cloned()
     }
 }
 

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 {

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;