Omit `.git` worktree indexing (#9281)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/9174

Release Notes:

- Fixed panics when `.git` was opened as a Zed worktree
([9174](https://github.com/zed-industries/zed/issues/9174))

Change summary

crates/project_core/src/worktree.rs       | 29 ++++++++++++++----
crates/project_core/src/worktree_tests.rs | 37 +++++++++++++++++++++++++
2 files changed, 59 insertions(+), 7 deletions(-)

Detailed changes

crates/project_core/src/worktree.rs 🔗

@@ -2663,13 +2663,28 @@ impl BackgroundScannerState {
         Arc<Mutex<dyn GitRepository>>,
         TreeMap<RepoPath, GitFileStatus>,
     )> {
-        log::info!("build git repository {:?}", dot_git_path);
-
-        let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
-
-        // Guard against repositories inside the repository metadata
-        if work_dir_path.iter().any(|component| component == *DOT_GIT) {
-            return None;
+        let work_dir_path: Arc<Path> = match dot_git_path.parent() {
+            Some(parent_dir) => {
+                // Guard against repositories inside the repository metadata
+                if parent_dir.iter().any(|component| component == *DOT_GIT) {
+                    log::info!(
+                        "not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}"
+                    );
+                    return None;
+                };
+                log::info!(
+                    "building git repository, `.git` path in the worktree: {dot_git_path:?}"
+                );
+                parent_dir.into()
+            }
+            None => {
+                // `dot_git_path.parent().is_none()` means `.git` directory is the opened worktree itself,
+                // no files inside that directory are tracked by git, so no need to build the repo around it
+                log::info!(
+                    "not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}"
+                );
+                return None;
+            }
         };
 
         let work_dir_id = self

crates/project_core/src/worktree_tests.rs 🔗

@@ -1199,6 +1199,43 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
+    init_test(cx);
+    cx.executor().allow_parking();
+    let dir = temp_tree(json!({
+        ".git": {
+            "HEAD": "ref: refs/heads/main\n",
+            "foo": "foo contents",
+        },
+    }));
+    let dot_git_worktree_dir = dir.path().join(".git");
+
+    let tree = Worktree::local(
+        build_client(cx),
+        dot_git_worktree_dir.clone(),
+        true,
+        Arc::new(RealFs),
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    tree.flush_fs_events(cx).await;
+    tree.read_with(cx, |tree, _| {
+        check_worktree_entries(tree, &[], &["HEAD", "foo"], &[])
+    });
+
+    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
+        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
+    tree.flush_fs_events(cx).await;
+    tree.read_with(cx, |tree, _| {
+        check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[])
+    });
+}
+
 #[gpui::test(iterations = 30)]
 async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
     init_test(cx);