worktree: Prevent background scanner from trying to scan file worktrees (#39277)

Lukas Wirth created

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/fs/src/fs.rs                 | 10 +++-
crates/project/src/project_tests.rs |  2 
crates/worktree/src/worktree.rs     | 67 ++++++++++++++++++------------
3 files changed, 47 insertions(+), 32 deletions(-)

Detailed changes

crates/fs/src/fs.rs 🔗

@@ -791,11 +791,15 @@ impl Fs for RealFs {
         let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
 
         // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
-        if watcher.add(path).is_err()
+        if let Err(e) = watcher.add(path)
             && let Some(parent) = path.parent()
-            && let Err(e) = watcher.add(parent)
+            && let Err(parent_e) = watcher.add(parent)
         {
-            log::warn!("Failed to watch: {e}");
+            log::warn!(
+                "Failed to watch {} and its parent directory {}:\n{e}\n{parent_e}",
+                path.display(),
+                parent.display()
+            );
         }
 
         // Check if path is a symlink and follow the target parent

crates/project/src/project_tests.rs 🔗

@@ -1380,7 +1380,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
 
     cx.executor().run_until_parked();
     assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
-    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 5);
+    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
 
     let mut new_watched_paths = fs.watched_paths();
     new_watched_paths.retain(|path| {

crates/worktree/src/worktree.rs 🔗

@@ -158,6 +158,7 @@ pub struct RemoteWorktree {
 #[derive(Clone)]
 pub struct Snapshot {
     id: WorktreeId,
+    /// The absolute path of the worktree root.
     abs_path: Arc<SanitizedPath>,
     path_style: PathStyle,
     root_name: Arc<RelPath>,
@@ -235,7 +236,7 @@ pub struct LocalSnapshot {
     /// All of the git repositories in the worktree, indexed by the project entry
     /// id of their parent directory.
     git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
-    /// The file handle of the root dir
+    /// The file handle of the worktree root. `None` if the worktree is a directory.
     /// (so we can find it after it's been moved)
     root_file_handle: Option<Arc<dyn fs::FileHandle>>,
 }
@@ -370,11 +371,19 @@ impl Worktree {
             true
         });
 
-        let root_file_handle = fs
-            .open_handle(&abs_path)
-            .await
-            .context("failed to open local worktree root")
-            .log_err();
+        let root_file_handle = if metadata.as_ref().is_some() {
+            fs.open_handle(&abs_path)
+                .await
+                .with_context(|| {
+                    format!(
+                        "failed to open local worktree root at {}",
+                        abs_path.display()
+                    )
+                })
+                .log_err()
+        } else {
+            None
+        };
 
         cx.new(move |cx: &mut Context<Worktree>| {
             let mut snapshot = LocalSnapshot {
@@ -3572,25 +3581,25 @@ impl BackgroundScanner {
 
         log::trace!("containing git repository: {containing_git_repository:?}");
 
-        let global_gitignore_path = paths::global_gitignore_path();
-        self.state.lock().snapshot.global_gitignore =
-            if let Some(global_gitignore_path) = global_gitignore_path.as_ref() {
-                build_gitignore(global_gitignore_path, self.fs.as_ref())
+        let mut global_gitignore_events =
+            if let Some(global_gitignore_path) = &paths::global_gitignore_path() {
+                self.state.lock().snapshot.global_gitignore =
+                    if self.fs.is_file(&global_gitignore_path).await {
+                        build_gitignore(global_gitignore_path, self.fs.as_ref())
+                            .await
+                            .ok()
+                            .map(Arc::new)
+                    } else {
+                        None
+                    };
+                self.fs
+                    .watch(global_gitignore_path, FS_WATCH_LATENCY)
                     .await
-                    .ok()
-                    .map(Arc::new)
+                    .0
             } else {
-                None
+                self.state.lock().snapshot.global_gitignore = None;
+                Box::pin(futures::stream::empty())
             };
-        let mut global_gitignore_events = if let Some(global_gitignore_path) = global_gitignore_path
-        {
-            self.fs
-                .watch(&global_gitignore_path, FS_WATCH_LATENCY)
-                .await
-                .0
-        } else {
-            Box::pin(futures::stream::empty())
-        };
 
         let (scan_job_tx, scan_job_rx) = channel::unbounded();
         {
@@ -3606,12 +3615,14 @@ impl BackgroundScanner {
                     root_entry.is_ignored = true;
                     state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
                 }
-                state.enqueue_scan_dir(
-                    root_abs_path.as_path().into(),
-                    &root_entry,
-                    &scan_job_tx,
-                    self.fs.as_ref(),
-                );
+                if root_entry.is_dir() {
+                    state.enqueue_scan_dir(
+                        root_abs_path.as_path().into(),
+                        &root_entry,
+                        &scan_job_tx,
+                        self.fs.as_ref(),
+                    );
+                }
             }
         };