WIP: Move statuses to entries

Mikayla Maki and julia created

co-authored-by: julia <julia@zed.dev>

Change summary

crates/project/src/worktree.rs | 60 ++++++++++++++++++++++++------------
crates/rpc/src/proto.rs        |  5 +++
2 files changed, 45 insertions(+), 20 deletions(-)

Detailed changes

crates/project/src/worktree.rs 🔗

@@ -1474,8 +1474,9 @@ impl Snapshot {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn status_for_file(&self, path: impl Into<PathBuf>) -> Option<GitFileStatus> {
+        let path = path.into();
         self.entries_by_path
-            .get(&PathKey(Arc::from(path.into())), &())
+            .get(&PathKey(Arc::from(path)), &())
             .and_then(|entry| entry.git_status)
     }
 
@@ -1929,25 +1930,25 @@ impl LocalSnapshot {
         if self.git_repositories.get(&work_dir_id).is_none() {
             let repo = fs.open_repo(abs_path.as_path())?;
             let work_directory = RepositoryWorkDirectory(work_dir.clone());
-            let scan_id = self.scan_id;
 
             let repo_lock = repo.lock();
 
             self.repository_entries.insert(
-                work_directory,
+                work_directory.clone(),
                 RepositoryEntry {
                     work_directory: work_dir_id.into(),
                     branch: repo_lock.branch_name().map(Into::into),
-                    // TODO: statuses
-                    // statuses: repo_lock.statuses().unwrap_or_default(),
                 },
             );
+
+            self.scan_statuses(repo_lock.deref(), &work_directory);
+
             drop(repo_lock);
 
             self.git_repositories.insert(
                 work_dir_id,
                 LocalRepositoryEntry {
-                    git_dir_scan_id: scan_id,
+                    git_dir_scan_id: 0,
                     repo_ptr: repo,
                     git_dir_path: parent_path.clone(),
                 },
@@ -1957,6 +1958,23 @@ impl LocalSnapshot {
         Some(())
     }
 
+    fn scan_statuses(&mut self, repo_ptr: &dyn GitRepository, work_directory: &RepositoryWorkDirectory) {
+        let statuses = repo_ptr.statuses().unwrap_or_default();
+        for (repo_path, status) in statuses.iter() {
+            let Some(entry) = self.entry_for_path(&work_directory.0.join(repo_path)) else {
+                continue;
+            };
+
+            let mut entry = entry.clone();
+            entry.git_status = Some(*status);
+
+            // TODO statuses
+            // Bubble
+
+            self.entries_by_path.insert_or_replace(entry, &());
+        }
+    }
+
     fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
         let mut inodes = TreeSet::default();
         for ancestor in path.ancestors().skip(1) {
@@ -3114,8 +3132,15 @@ impl BackgroundScanner {
                 if repo.git_dir_scan_id == scan_id {
                     return None;
                 }
+
                 (*entry_id, repo.repo_ptr.to_owned())
             };
+            /*
+            1. Populate dir, initializes the git repo
+            2. Sometimes, we get a file event inside the .git repo, before it's initializaed
+            In both cases, we should end up with an initialized repo and a full status scan
+
+            */
 
             let work_dir = snapshot
                 .entry_for_id(entry_id)
@@ -3136,20 +3161,8 @@ impl BackgroundScanner {
                     entry.branch = branch.map(Into::into);
                 });
 
-            let statuses = repo.statuses().unwrap_or_default();
-            for (repo_path, status) in statuses.iter() {
-                let Some(entry) = snapshot.entry_for_path(&work_dir.0.join(repo_path)) else {
-                    continue;
-                };
-
-                let mut entry = entry.clone();
-                entry.git_status = Some(*status);
-
-                // TODO statuses
-                // Bubble
 
-                snapshot.entries_by_path.insert_or_replace(entry, &());
-            }
+            snapshot.scan_statuses(repo.deref(), &work_dir);
         } else {
             if snapshot
                 .entry_for_path(&path)
@@ -4945,7 +4958,7 @@ mod tests {
 
         // TODO: Stream statuses UPDATE THIS TO CHECK BUBBLIBG BEHAVIOR
         #[gpui::test]
-        async fn test_git_status(cx: &mut TestAppContext) {
+        async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
             const IGNORE_RULE: &'static str = "**/target";
 
             let root = temp_tree(json!({
@@ -4982,6 +4995,7 @@ mod tests {
             cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
                 .await;
 
+
             const A_TXT: &'static str = "a.txt";
             const B_TXT: &'static str = "b.txt";
             const E_TXT: &'static str = "c/d/e.txt";
@@ -5001,6 +5015,7 @@ mod tests {
             std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
 
             tree.flush_fs_events(cx).await;
+            deterministic.run_until_parked();
 
             // Check that the right git state is observed on startup
             tree.read_with(cx, |tree, _cx| {
@@ -5027,6 +5042,7 @@ mod tests {
             git_add(Path::new(B_TXT), &repo);
             git_commit("Committing modified and added", &repo);
             tree.flush_fs_events(cx).await;
+            deterministic.run_until_parked();
 
             // Check that repo only changes are tracked
             tree.read_with(cx, |tree, _cx| {
@@ -5044,6 +5060,7 @@ mod tests {
             std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
             std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
             tree.flush_fs_events(cx).await;
+            deterministic.run_until_parked();
 
             // Check that more complex repo changes are tracked
             tree.read_with(cx, |tree, _cx| {
@@ -5076,6 +5093,7 @@ mod tests {
             git_commit("Committing modified git ignore", &repo);
 
             tree.flush_fs_events(cx).await;
+            deterministic.run_until_parked();
 
             let mut renamed_dir_name = "first_directory/second_directory";
             const RENAMED_FILE: &'static str = "rf.txt";
@@ -5088,6 +5106,7 @@ mod tests {
             .unwrap();
 
             tree.flush_fs_events(cx).await;
+            deterministic.run_until_parked();
 
             tree.read_with(cx, |tree, _cx| {
                 let snapshot = tree.snapshot();
@@ -5107,6 +5126,7 @@ mod tests {
             .unwrap();
 
             tree.flush_fs_events(cx).await;
+            deterministic.run_until_parked();
 
             tree.read_with(cx, |tree, _cx| {
                 let snapshot = tree.snapshot();

crates/rpc/src/proto.rs 🔗

@@ -497,6 +497,11 @@ pub fn split_worktree_update(
         .map(|repo| (repo.work_directory_id, repo))
         .collect::<HashMap<_, _>>();
 
+    // Maintain a list of inflight repositories
+    // Every time we send a repository's work directory, stick it in the list of in-flight repositories
+    // Every go of the loop, drain each in-flight repository's statuses
+    // Until we have no more data
+
     iter::from_fn(move || {
         if done_files {
             return None;