Load .gitignore files from the Fs object so we can test this with a FakeFs

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

zed/src/worktree.rs | 133 ++++++++++++++++++++++++++++++++++++----------
1 file changed, 102 insertions(+), 31 deletions(-)

Detailed changes

zed/src/worktree.rs 🔗

@@ -11,7 +11,7 @@ use crate::{
     time::{self, ReplicaId},
     util::{Bias, TryFutureExt},
 };
-use ::ignore::gitignore::Gitignore;
+use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Result};
 use futures::{Stream, StreamExt};
 pub use fuzzy::{match_paths, PathMatch};
@@ -732,12 +732,15 @@ impl LocalWorktree {
                 next_entry_id: Arc::new(next_entry_id),
             };
             if let Some(metadata) = metadata {
-                snapshot.insert_entry(Entry::new(
-                    path.into(),
-                    &metadata,
-                    &snapshot.next_entry_id,
-                    snapshot.root_char_bag,
-                ));
+                snapshot.insert_entry(
+                    Entry::new(
+                        path.into(),
+                        &metadata,
+                        &snapshot.next_entry_id,
+                        snapshot.root_char_bag,
+                    ),
+                    fs.as_ref(),
+                );
             }
 
             let (mut remote_id_tx, remote_id_rx) = watch::channel();
@@ -1156,6 +1159,16 @@ impl LocalWorktree {
     }
 }
 
+fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
+    let contents = smol::block_on(fs.load(&abs_path))?;
+    let parent = abs_path.parent().unwrap_or(Path::new("/"));
+    let mut builder = GitignoreBuilder::new(parent);
+    for line in contents.lines() {
+        builder.add_line(Some(abs_path.into()), line)?;
+    }
+    Ok(builder.build()?)
+}
+
 pub fn refresh_buffer(abs_path: PathBuf, fs: &Arc<dyn Fs>, cx: &mut ModelContext<Buffer>) {
     let fs = fs.clone();
     cx.spawn(|buffer, mut cx| async move {
@@ -1578,16 +1591,24 @@ impl Snapshot {
         self.entry_for_path(path.as_ref()).map(|e| e.inode)
     }
 
-    fn insert_entry(&mut self, mut entry: Entry) -> Entry {
+    fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
+        println!("insert entry {:?}", entry.path);
         if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
-            let (ignore, err) = Gitignore::new(self.abs_path.join(&entry.path));
-            if let Some(err) = err {
-                log::error!("error in ignore file {:?} - {:?}", &entry.path, err);
+            let abs_path = self.abs_path.join(&entry.path);
+            match build_gitignore(&abs_path, fs) {
+                Ok(ignore) => {
+                    let ignore_dir_path = entry.path.parent().unwrap();
+                    self.ignores
+                        .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id));
+                }
+                Err(error) => {
+                    log::error!(
+                        "error loading .gitignore file {:?} - {:?}",
+                        &entry.path,
+                        error
+                    );
+                }
             }
-
-            let ignore_dir_path = entry.path.parent().unwrap();
-            self.ignores
-                .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id));
         }
 
         self.reuse_entry_id(&mut entry);
@@ -2206,13 +2227,16 @@ impl BackgroundScanner {
 
             // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
             if child_name == *GITIGNORE {
-                let (ignore, err) = Gitignore::new(&child_abs_path);
-                if let Some(err) = err {
-                    log::error!("error in ignore file {:?} - {:?}", child_name, err);
+                match build_gitignore(&child_abs_path, self.fs.as_ref()) {
+                    Ok(ignore) => {
+                        let ignore = Arc::new(ignore);
+                        ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
+                        new_ignore = Some(ignore);
+                    }
+                    Err(error) => {
+                        log::error!("error loading .gitignore file {:?} - {:?}", child_name, error);
+                    }
                 }
-                let ignore = Arc::new(ignore);
-                ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
-                new_ignore = Some(ignore);
 
                 // Update ignore status of any child entries we've already processed to reflect the
                 // ignore file in the current directory. Because `.gitignore` starts with a `.`,
@@ -2321,7 +2345,7 @@ impl BackgroundScanner {
                         snapshot.root_char_bag,
                     );
                     fs_entry.is_ignored = ignore_stack.is_all();
-                    snapshot.insert_entry(fs_entry);
+                    snapshot.insert_entry(fs_entry, self.fs.as_ref());
                     if metadata.is_dir {
                         scan_queue_tx
                             .send(ScanJob {
@@ -2490,7 +2514,7 @@ async fn refresh_entry(
         &next_entry_id,
         root_char_bag,
     );
-    Ok(snapshot.lock().insert_entry(entry))
+    Ok(snapshot.lock().insert_entry(entry, fs))
 }
 
 fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
@@ -2774,6 +2798,50 @@ mod tests {
     use std::time::UNIX_EPOCH;
     use std::{env, fmt::Write, os::unix, time::SystemTime};
 
+    #[gpui::test]
+    async fn test_traversal(cx: gpui::TestAppContext) {
+        let fs = FakeFs::new();
+        fs.insert_tree(
+            "/root",
+            json!({
+               ".gitignore": "a/b\n",
+               "a": {
+                   "b": "",
+                   "c": "",
+               }
+            }),
+        )
+        .await;
+
+        let tree = Worktree::open_local(
+            rpc::Client::new(),
+            Arc::from(Path::new("/root")),
+            Arc::new(fs),
+            Default::default(),
+            &mut cx.to_async(),
+        )
+        .await
+        .unwrap();
+        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+            .await;
+
+        tree.read_with(&cx, |tree, cx| {
+            dbg!(tree.entries_by_path.items(&()));
+
+            assert_eq!(
+                tree.entries(false)
+                    .map(|entry| entry.path.as_ref())
+                    .collect::<Vec<_>>(),
+                vec![
+                    Path::new(""),
+                    Path::new(".gitignore"),
+                    Path::new("a"),
+                    Path::new("a/c"),
+                ]
+            );
+        })
+    }
+
     #[gpui::test]
     async fn test_populate_and_search(cx: gpui::TestAppContext) {
         let dir = temp_tree(json!({
@@ -3260,14 +3328,17 @@ mod tests {
             root_char_bag: Default::default(),
             next_entry_id: next_entry_id.clone(),
         };
-        initial_snapshot.insert_entry(Entry::new(
-            Path::new("").into(),
-            &smol::block_on(fs.metadata(root_dir.path()))
-                .unwrap()
-                .unwrap(),
-            &next_entry_id,
-            Default::default(),
-        ));
+        initial_snapshot.insert_entry(
+            Entry::new(
+                Path::new("").into(),
+                &smol::block_on(fs.metadata(root_dir.path()))
+                    .unwrap()
+                    .unwrap(),
+                &next_entry_id,
+                Default::default(),
+            ),
+            fs.as_ref(),
+        );
         let mut scanner = BackgroundScanner::new(
             Arc::new(Mutex::new(initial_snapshot.clone())),
             notify_tx,