Honor gitignores above worktree root

Antonio Scandurra created

Change summary

crates/project/src/worktree.rs | 123 ++++++++++++++++++++++-------------
1 file changed, 78 insertions(+), 45 deletions(-)

Detailed changes

crates/project/src/worktree.rs 🔗

@@ -2046,24 +2046,41 @@ impl BackgroundScanner {
 
     async fn scan_dirs(&mut self) -> Result<()> {
         let root_char_bag;
+        let root_abs_path;
         let next_entry_id;
         let is_dir;
         {
             let snapshot = self.snapshot.lock();
             root_char_bag = snapshot.root_char_bag;
+            root_abs_path = snapshot.abs_path.clone();
             next_entry_id = snapshot.next_entry_id.clone();
             is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir())
         };
 
+        // Populate ignores above the root.
+        for ancestor in root_abs_path.ancestors().skip(1) {
+            if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
+            {
+                self.snapshot
+                    .lock()
+                    .ignores_by_abs_path
+                    .insert(ancestor.into(), (ignore.into(), 0));
+            }
+        }
+
         if is_dir {
             let path: Arc<Path> = Arc::from(Path::new(""));
-            let abs_path = self.abs_path();
+            let ignore_stack = self
+                .snapshot
+                .lock()
+                .ignore_stack_for_abs_path(&root_abs_path, true);
+
             let (tx, rx) = channel::unbounded();
             self.executor
                 .block(tx.send(ScanJob {
-                    abs_path: abs_path.to_path_buf(),
+                    abs_path: root_abs_path.to_path_buf(),
                     path,
-                    ignore_stack: IgnoreStack::none(),
+                    ignore_stack,
                     scan_queue: tx.clone(),
                 }))
                 .unwrap();
@@ -2313,14 +2330,15 @@ impl BackgroundScanner {
         let mut ignores_to_update = Vec::new();
         let mut ignores_to_delete = Vec::new();
         for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_abs_path {
-            let parent_path = parent_abs_path.strip_prefix(&snapshot.abs_path).unwrap();
-            if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() {
-                ignores_to_update.push(parent_abs_path.clone());
-            }
+            if let Ok(parent_path) = parent_abs_path.strip_prefix(&snapshot.abs_path) {
+                if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() {
+                    ignores_to_update.push(parent_abs_path.clone());
+                }
 
-            let ignore_path = parent_path.join(&*GITIGNORE);
-            if snapshot.entry_for_path(ignore_path).is_none() {
-                ignores_to_delete.push(parent_abs_path.clone());
+                let ignore_path = parent_path.join(&*GITIGNORE);
+                if snapshot.entry_for_path(ignore_path).is_none() {
+                    ignores_to_delete.push(parent_abs_path.clone());
+                }
             }
         }
 
@@ -2410,20 +2428,6 @@ impl BackgroundScanner {
         snapshot.entries_by_path.edit(entries_by_path_edits, &());
         snapshot.entries_by_id.edit(entries_by_id_edits, &());
     }
-
-    async fn build_root_ignore_stack(&self) {
-        // let parent_abs_path = if let Some(path) = self.abs_path().parent() {
-        //     path
-        // } else {
-        //     return IgnoreStack::none()
-        // }
-
-        // let mut cur_path = PathBuf::new();
-        // for component in self.abs_path().components() {
-        // // self.snapshot.lock().ignores.insert(parent_path, (ignore, self.scan_id));
-        //     cur_path.push(compo)
-        // }
-    }
 }
 
 fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
@@ -2797,23 +2801,28 @@ mod tests {
 
     #[gpui::test]
     async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
-        let dir = temp_tree(json!({
-            ".git": {},
-            ".gitignore": "ignored-dir\n",
-            "tracked-dir": {
-                "tracked-file1": "tracked contents",
-            },
-            "ignored-dir": {
-                "ignored-file1": "ignored contents",
+        let parent_dir = temp_tree(json!({
+            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
+            "tree": {
+                ".git": {},
+                ".gitignore": "ignored-dir\n",
+                "tracked-dir": {
+                    "tracked-file1": "",
+                    "ancestor-ignored-file1": "",
+                },
+                "ignored-dir": {
+                    "ignored-file1": ""
+                }
             }
         }));
+        let dir = parent_dir.path().join("tree");
 
         let http_client = FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone());
 
         let tree = Worktree::local(
             client,
-            dir.path(),
+            dir.as_path(),
             true,
             Arc::new(RealFs),
             Default::default(),
@@ -2826,23 +2835,47 @@ mod tests {
         tree.flush_fs_events(&cx).await;
         cx.read(|cx| {
             let tree = tree.read(cx);
-            let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
-            let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
-            assert_eq!(tracked.is_ignored, false);
-            assert_eq!(ignored.is_ignored, true);
+            assert!(
+                !tree
+                    .entry_for_path("tracked-dir/tracked-file1")
+                    .unwrap()
+                    .is_ignored
+            );
+            assert!(
+                tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
+                    .unwrap()
+                    .is_ignored
+            );
+            assert!(
+                tree.entry_for_path("ignored-dir/ignored-file1")
+                    .unwrap()
+                    .is_ignored
+            );
         });
 
-        std::fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
-        std::fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
+        std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap();
+        std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap();
+        std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap();
         tree.flush_fs_events(&cx).await;
         cx.read(|cx| {
             let tree = tree.read(cx);
-            let dot_git = tree.entry_for_path(".git").unwrap();
-            let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
-            let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
-            assert_eq!(tracked.is_ignored, false);
-            assert_eq!(ignored.is_ignored, true);
-            assert_eq!(dot_git.is_ignored, true);
+            assert!(
+                !tree
+                    .entry_for_path("tracked-dir/tracked-file2")
+                    .unwrap()
+                    .is_ignored
+            );
+            assert!(
+                tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
+                    .unwrap()
+                    .is_ignored
+            );
+            assert!(
+                tree.entry_for_path("ignored-dir/ignored-file2")
+                    .unwrap()
+                    .is_ignored
+            );
+            assert!(tree.entry_for_path(".git").unwrap().is_ignored);
         });
     }