Remove git repos from worktree when deleted on storage

Julia and Mikayla Maki created

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>

Change summary

crates/project/src/git_repository.rs |  2 
crates/project/src/worktree.rs       | 68 +++++++++++++++++++++--------
2 files changed, 50 insertions(+), 20 deletions(-)

Detailed changes

crates/project/src/git_repository.rs 🔗

@@ -56,7 +56,7 @@ impl GitRepository {
         self.last_scan_id = scan_id;
     }
 
-    pub fn with_repo(&mut self, f: Box<dyn FnOnce(&mut git2::Repository)>) {
+    pub fn with_repo<F: FnOnce(&mut git2::Repository)>(&mut self, f: F) {
         let mut git2 = self.libgit_repository.lock();
         f(&mut git2)
     }

crates/project/src/worktree.rs 🔗

@@ -1311,9 +1311,7 @@ impl LocalSnapshot {
         self.git_repositories
             .iter()
             .rev() //git_repository is ordered lexicographically
-            .find(|repo| {
-                repo.is_path_managed_by(&self.abs_path.join(path))
-            })
+            .find(|repo| repo.is_path_managed_by(&self.abs_path.join(path)))
             .map(|repo| repo.clone())
     }
 
@@ -2548,13 +2546,16 @@ impl BackgroundScanner {
     }
 
     async fn update_git_repositories(&self) {
-        let mut snapshot = self.snapshot();
-        let mut git_repositories = mem::take(&mut snapshot.git_repositories);
-        git_repositories.retain(|git_repository| {
-            let dot_git_path = git_repository.content_path().join(&*DOT_GIT);
-            snapshot.entry_for_path(dot_git_path).is_some()
-        });
-        snapshot.git_repositories = git_repositories;
+        let mut snapshot = self.snapshot.lock();
+
+        let new_repos = snapshot
+            .git_repositories
+            .iter()
+            .cloned()
+            .filter(|repo| git2::Repository::open(repo.git_dir_path()).is_ok())
+            .collect();
+
+        snapshot.git_repositories = new_repos;
     }
 
     async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
@@ -3179,30 +3180,59 @@ mod tests {
                 .git_repository_for_file_path("dir1/src/b.txt".as_ref())
                 .unwrap();
 
-            // Need to update the file system for anything involving git
-            // Goal: Make this test pass
-            // Up Next: Invalidating git repos!
-            assert_eq!(repo.content_path(), root.path().join("dir1").canonicalize().unwrap());
-            assert_eq!(repo.git_dir_path(), root.path().join("dir1/.git").canonicalize().unwrap());
+            assert_eq!(
+                repo.content_path(),
+                root.path().join("dir1").canonicalize().unwrap()
+            );
+            assert_eq!(
+                repo.git_dir_path(),
+                root.path().join("dir1/.git").canonicalize().unwrap()
+            );
 
             let repo = tree
                 .git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref())
                 .unwrap();
 
-            assert_eq!(repo.content_path(), root.path().join("dir1/deps/dep1").canonicalize().unwrap());
-            assert_eq!(repo.git_dir_path(), root.path().join("dir1/deps/dep1/.git").canonicalize().unwrap());
+            assert_eq!(
+                repo.content_path(),
+                root.path().join("dir1/deps/dep1").canonicalize().unwrap()
+            );
+            assert_eq!(
+                repo.git_dir_path(),
+                root.path()
+                    .join("dir1/deps/dep1/.git")
+                    .canonicalize()
+                    .unwrap()
+            );
 
             let repo = tree
                 .git_repository_for_git_data("dir1/.git/HEAD".as_ref())
                 .unwrap();
 
-            assert_eq!(repo.content_path(), root.path().join("dir1").canonicalize().unwrap());
-            assert_eq!(repo.git_dir_path(), root.path().join("dir1/.git").canonicalize().unwrap());
+            assert_eq!(
+                repo.content_path(),
+                root.path().join("dir1").canonicalize().unwrap()
+            );
+            assert_eq!(
+                repo.git_dir_path(),
+                root.path().join("dir1/.git").canonicalize().unwrap()
+            );
 
             assert!(tree.does_git_repository_track_file_path(&repo, "dir1/src/b.txt".as_ref()));
             assert!(!tree
                 .does_git_repository_track_file_path(&repo, "dir1/deps/dep1/src/a.txt".as_ref()));
         });
+
+        std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
+        tree.flush_fs_events(cx).await;
+
+        tree.read_with(cx, |tree, _cx| {
+            let tree = tree.as_local().unwrap();
+
+            assert!(tree
+                .git_repository_for_file_path("dir1/src/b.txt".as_ref())
+                .is_none());
+        });
     }
 
     #[gpui::test]