Start work on handling changes to gitignore files

Max Brunsfeld created

* Use the published ignore crate
* Store ignore objects on snapshot, and use them to compute
  files' ignored status dynamically, instead of storing the
  ignored status on the file.

Change summary

Cargo.lock                |  50 ++--------
zed/Cargo.toml            |   2 
zed/src/worktree.rs       | 187 +++++++++++++++++++++-------------------
zed/src/worktree/fuzzy.rs |  25 +++--
4 files changed, 126 insertions(+), 138 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -448,16 +448,6 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
-[[package]]
-name = "crossbeam-channel"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
-dependencies = [
- "crossbeam-utils 0.7.2",
- "maybe-uninit",
-]
-
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.0"
@@ -465,7 +455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.2",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -475,18 +465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.2",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
-dependencies = [
- "autocfg",
- "cfg-if 0.1.10",
- "lazy_static",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -891,8 +870,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
 [[package]]
 name = "globset"
-version = "0.4.4"
-source = "git+https://github.com/zed-industries/ripgrep?rev=1d152118f35b3e3590216709b86277062d79b8a0#1d152118f35b3e3590216709b86277062d79b8a0"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
 dependencies = [
  "aho-corasick",
  "bstr",
@@ -967,11 +947,11 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
 [[package]]
 name = "ignore"
-version = "0.4.11"
-source = "git+https://github.com/zed-industries/ripgrep?rev=1d152118f35b3e3590216709b86277062d79b8a0#1d152118f35b3e3590216709b86277062d79b8a0"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
 dependencies = [
- "crossbeam-channel 0.4.4",
- "crossbeam-utils 0.7.2",
+ "crossbeam-utils",
  "globset",
  "lazy_static",
  "log",
@@ -1095,12 +1075,6 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
 
-[[package]]
-name = "maybe-uninit"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
-
 [[package]]
 name = "memchr"
 version = "2.3.4"
@@ -1616,7 +1590,7 @@ dependencies = [
  "base64",
  "blake2b_simd",
  "constant_time_eq",
- "crossbeam-utils 0.8.2",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -1711,7 +1685,7 @@ dependencies = [
 name = "scoped-pool"
 version = "0.0.1"
 dependencies = [
- "crossbeam-channel 0.5.0",
+ "crossbeam-channel",
 ]
 
 [[package]]
@@ -2246,7 +2220,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "arrayvec",
- "crossbeam-channel 0.5.0",
+ "crossbeam-channel",
  "ctor",
  "dirs",
  "easy-parallel",

zed/Cargo.toml 🔗

@@ -22,7 +22,7 @@ easy-parallel = "3.1.0"
 fsevent = {path = "../fsevent"}
 futures-core = "0.3"
 gpui = {path = "../gpui"}
-ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"}
+ignore = "0.4"
 lazy_static = "1.4.0"
 libc = "0.2"
 log = "0.4"

zed/src/worktree.rs 🔗

@@ -9,7 +9,7 @@ use anyhow::{anyhow, Context, Result};
 use fuzzy::PathEntry;
 pub use fuzzy::{match_paths, PathMatch};
 use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task};
-use ignore::dir::{Ignore, IgnoreBuilder};
+use ignore::gitignore::Gitignore;
 use parking_lot::Mutex;
 use postage::{
     prelude::{Sink, Stream},
@@ -29,6 +29,8 @@ use std::{
     time::Duration,
 };
 
+const GITIGNORE: &'static str = ".gitignore";
+
 #[derive(Clone, Debug)]
 enum ScanState {
     Idle,
@@ -58,6 +60,7 @@ impl Worktree {
             id,
             path: path.into(),
             root_inode: None,
+            ignores: Default::default(),
             entries: Default::default(),
         };
         let (event_stream, event_stream_handle) =
@@ -229,6 +232,7 @@ pub struct Snapshot {
     id: usize,
     path: Arc<Path>,
     root_inode: Option<u64>,
+    ignores: HashMap<u64, Gitignore>,
     entries: SumTree<Entry>,
 }
 
@@ -268,6 +272,48 @@ impl Snapshot {
         })
     }
 
+    pub fn is_path_ignored(&self, path: impl AsRef<Path>) -> Result<bool> {
+        if let Some(inode) = self.inode_for_path(path.as_ref()) {
+            self.is_inode_ignored(inode)
+        } else {
+            Ok(false)
+        }
+    }
+
+    pub fn is_inode_ignored(&self, mut inode: u64) -> Result<bool> {
+        let mut components = Vec::new();
+        let mut relative_path = PathBuf::new();
+        let mut entry = self
+            .entries
+            .get(&inode)
+            .ok_or_else(|| anyhow!("entry does not exist in worktree"))?;
+        while let Some(parent) = entry.parent() {
+            let parent_entry = self.entries.get(&parent).unwrap();
+            if let Entry::Dir { children, .. } = parent_entry {
+                let (_, child_name) = children
+                    .iter()
+                    .find(|(child_inode, _)| *child_inode == inode)
+                    .unwrap();
+                components.push(child_name.as_ref());
+                inode = parent;
+
+                if let Some(ignore) = self.ignores.get(&inode) {
+                    relative_path.clear();
+                    relative_path.extend(components.iter().rev());
+                    match ignore.matched_path_or_any_parents(&relative_path, entry.is_dir()) {
+                        ignore::Match::Whitelist(_) => return Ok(false),
+                        ignore::Match::Ignore(_) => return Ok(true),
+                        ignore::Match::None => {}
+                    }
+                }
+            } else {
+                unreachable!();
+            }
+            entry = parent_entry;
+        }
+        Ok(false)
+    }
+
     pub fn path_for_inode(&self, mut inode: u64, include_root: bool) -> Result<PathBuf> {
         let mut components = Vec::new();
         let mut entry = self
@@ -311,13 +357,18 @@ impl Snapshot {
 
         // Insert the entry in its new parent with the correct name.
         if let Some(new_parent_inode) = entry.parent() {
+            let name = name.unwrap();
+            if name == GITIGNORE {
+                self.insert_ignore_file(new_parent_inode);
+            }
+
             let mut new_parent = self.entries.get(&new_parent_inode).unwrap().clone();
             if let Entry::Dir { children, .. } = &mut new_parent {
                 *children = children
                     .iter()
                     .filter(|(inode, _)| *inode != entry.inode())
                     .cloned()
-                    .chain(Some((entry.inode(), name.unwrap().into())))
+                    .chain(Some((entry.inode(), name.into())))
                     .collect::<Vec<_>>()
                     .into();
             } else {
@@ -346,6 +397,10 @@ impl Snapshot {
         let mut new_children = Vec::new();
         let mut old_children = HashMap::<u64, HashSet<u64>>::new();
         for (name, child) in children.into_iter() {
+            if *name == *GITIGNORE {
+                self.insert_ignore_file(parent_inode);
+            }
+
             new_children.push((child.inode(), name.into()));
             if let Some(old_child) = self.entries.get(&child.inode()) {
                 if let Some(old_parent_inode) = old_child.parent() {
@@ -389,6 +444,11 @@ impl Snapshot {
             self.clear_descendants(entry.inode(), &mut edits);
 
             if let Some(parent_inode) = entry.parent() {
+                if let Some(file_name) = path.file_name() {
+                    if file_name == GITIGNORE {
+                        self.remove_ignore_file(parent_inode);
+                    }
+                }
                 let parent = self.entries.get(&parent_inode).unwrap().clone();
                 self.remove_children(parent, &mut edits, |inode| inode == entry.inode());
             }
@@ -402,12 +462,19 @@ impl Snapshot {
     fn clear_descendants(&mut self, inode: u64, edits: &mut Vec<Edit<Entry>>) {
         let mut stack = vec![inode];
         while let Some(inode) = stack.pop() {
+            let mut has_gitignore = false;
             if let Entry::Dir { children, .. } = self.entries.get(&inode).unwrap() {
-                for (child_inode, _) in children.iter() {
+                for (child_inode, child_name) in children.iter() {
+                    if **child_name == *GITIGNORE {
+                        has_gitignore = true;
+                    }
                     edits.push(Edit::Remove(*child_inode));
                     stack.push(*child_inode);
                 }
             }
+            if has_gitignore {
+                self.remove_ignore_file(inode);
+            }
         }
     }
 
@@ -430,6 +497,22 @@ impl Snapshot {
         edits.push(Edit::Insert(parent));
     }
 
+    fn insert_ignore_file(&mut self, dir_inode: u64) {
+        let mut path = self.path.to_path_buf();
+        path.push(self.path_for_inode(dir_inode, false).unwrap());
+        path.push(GITIGNORE);
+        let (ignore, err) = Gitignore::new(&path);
+        if let Some(err) = err {
+            log::info!("error in ignore file {:?} - {:?}", path, err);
+        }
+
+        self.ignores.insert(dir_inode, ignore);
+    }
+
+    fn remove_ignore_file(&mut self, dir_inode: u64) {
+        self.ignores.remove(&dir_inode);
+    }
+
     fn fmt_entry(
         &self,
         f: &mut fmt::Formatter<'_>,
@@ -500,7 +583,6 @@ pub enum Entry {
         inode: u64,
         parent: Option<u64>,
         is_symlink: bool,
-        is_ignored: bool,
         children: Arc<[(u64, Arc<OsStr>)]>,
         pending: bool,
     },
@@ -508,7 +590,6 @@ pub enum Entry {
         inode: u64,
         parent: Option<u64>,
         is_symlink: bool,
-        is_ignored: bool,
         path: PathEntry,
     },
 }
@@ -647,19 +728,11 @@ impl BackgroundScanner {
         let name = Arc::from(path.file_name().unwrap_or(OsStr::new("/")));
         let relative_path = PathBuf::from(&name);
 
-        let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap();
-        if metadata.is_dir() {
-            ignore = ignore.add_child(&path).unwrap();
-        }
-        let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore();
-
         if metadata.file_type().is_dir() {
-            let is_ignored = is_ignored || name.as_ref() == ".git";
             let dir_entry = Entry::Dir {
                 parent: None,
                 inode,
                 is_symlink,
-                is_ignored,
                 children: Arc::from([]),
                 pending: true,
             };
@@ -676,7 +749,6 @@ impl BackgroundScanner {
                 inode,
                 path: path.clone(),
                 relative_path,
-                ignore: Some(ignore),
                 scan_queue: tx.clone(),
             }))
             .unwrap();
@@ -704,10 +776,9 @@ impl BackgroundScanner {
                 None,
                 Entry::File {
                     parent: None,
-                    path: PathEntry::new(inode, &relative_path, is_ignored),
+                    path: PathEntry::new(inode, &relative_path),
                     inode,
                     is_symlink,
-                    is_ignored,
                 },
             );
             snapshot.root_inode = Some(inode);
@@ -732,25 +803,12 @@ impl BackgroundScanner {
             let child_path = job.path.join(child_name.as_ref());
 
             if child_metadata.is_dir() {
-                let mut is_ignored = true;
-                let mut ignore = None;
-
-                if let Some(parent_ignore) = job.ignore.as_ref() {
-                    let child_ignore = parent_ignore.add_child(&child_path).unwrap();
-                    is_ignored = child_ignore.matched(&child_path, true).is_ignore()
-                        || child_name.as_ref() == ".git";
-                    if !is_ignored {
-                        ignore = Some(child_ignore);
-                    }
-                }
-
                 new_entries.push((
                     child_name,
                     Entry::Dir {
                         parent: Some(job.inode),
                         inode: child_inode,
                         is_symlink: child_is_symlink,
-                        is_ignored,
                         children: Arc::from([]),
                         pending: true,
                     },
@@ -759,22 +817,16 @@ impl BackgroundScanner {
                     inode: child_inode,
                     path: Arc::from(child_path),
                     relative_path: child_relative_path,
-                    ignore,
                     scan_queue: scan_queue.clone(),
                 });
             } else {
-                let is_ignored = job
-                    .ignore
-                    .as_ref()
-                    .map_or(true, |i| i.matched(&child_path, false).is_ignore());
                 new_entries.push((
                     child_name,
                     Entry::File {
                         parent: Some(job.inode),
-                        path: PathEntry::new(child_inode, &child_relative_path, is_ignored),
+                        path: PathEntry::new(child_inode, &child_relative_path),
                         inode: child_inode,
                         is_symlink: child_is_symlink,
-                        is_ignored,
                     },
                 ));
             };
@@ -815,7 +867,7 @@ impl BackgroundScanner {
             snapshot.remove_path(&relative_path);
 
             match self.fs_entry_for_path(&root_path, &path) {
-                Ok(Some((fs_entry, ignore))) => {
+                Ok(Some(fs_entry)) => {
                     let is_dir = fs_entry.is_dir();
                     let inode = fs_entry.inode();
 
@@ -829,7 +881,6 @@ impl BackgroundScanner {
                                     .root_name()
                                     .map_or(PathBuf::new(), PathBuf::from)
                                     .join(relative_path),
-                                ignore: Some(ignore),
                                 scan_queue: scan_queue_tx.clone(),
                             }))
                             .unwrap();
@@ -862,7 +913,7 @@ impl BackgroundScanner {
         true
     }
 
-    fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result<Option<(Entry, Ignore)>> {
+    fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result<Option<Entry>> {
         let metadata = match fs::metadata(&path) {
             Err(err) => {
                 return match (err.kind(), err.raw_os_error()) {
@@ -874,11 +925,6 @@ impl BackgroundScanner {
             Ok(metadata) => metadata,
         };
 
-        let mut ignore = IgnoreBuilder::new().build().add_parents(&path).unwrap();
-        if metadata.is_dir() {
-            ignore = ignore.add_child(&path).unwrap();
-        }
-        let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore();
         let inode = metadata.ino();
         let is_symlink = fs::symlink_metadata(&path)
             .context("failed to read symlink metadata")?
@@ -899,7 +945,6 @@ impl BackgroundScanner {
                 inode,
                 parent,
                 is_symlink,
-                is_ignored,
                 pending: true,
                 children: Arc::from([]),
             }
@@ -908,18 +953,16 @@ impl BackgroundScanner {
                 inode,
                 parent,
                 is_symlink,
-                is_ignored,
                 path: PathEntry::new(
                     inode,
                     root_path
                         .parent()
                         .map_or(path, |parent| path.strip_prefix(parent).unwrap()),
-                    is_ignored,
                 ),
             }
         };
 
-        Ok(Some((entry, ignore)))
+        Ok(Some(entry))
     }
 }
 
@@ -927,7 +970,6 @@ struct ScanJob {
     inode: u64,
     path: Arc<Path>,
     relative_path: PathBuf,
-    ignore: Option<Ignore>,
     scan_queue: crossbeam_channel::Sender<io::Result<ScanJob>>,
 }
 
@@ -948,19 +990,6 @@ impl WorktreeHandle for ModelHandle<Worktree> {
     }
 }
 
-trait UnwrapIgnoreTuple {
-    fn unwrap(self) -> Ignore;
-}
-
-impl UnwrapIgnoreTuple for (Ignore, Option<ignore::Error>) {
-    fn unwrap(self) -> Ignore {
-        if let Some(error) = self.1 {
-            log::error!("error loading gitignore data: {}", error);
-        }
-        self.0
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -1114,31 +1143,17 @@ mod tests {
             app.read(|ctx| tree.read(ctx).scan_complete()).await;
             app.read(|ctx| {
                 let tree = tree.read(ctx);
-                assert!(!tree
-                    .entry_for_path("tracked-dir/tracked-file1")
-                    .unwrap()
-                    .is_ignored());
-                assert!(tree
-                    .entry_for_path("ignored-dir/ignored-file1")
-                    .unwrap()
-                    .is_ignored());
+                assert!(!tree.is_path_ignored("tracked-dir/tracked-file1").unwrap());
+                assert!(tree.is_path_ignored("ignored-dir/ignored-file1").unwrap());
             });
 
             fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
             fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
-            // tree.condition(&app, move |_, ctx| file2.path(ctx) == Path::new("d/file2"))
-            //     .await;
-            app.read(|ctx| tree.read(ctx).scan_complete()).await;
+            app.read(|ctx| tree.read(ctx).next_scan_complete()).await;
             app.read(|ctx| {
                 let tree = tree.read(ctx);
-                assert!(!tree
-                    .entry_for_path("tracked-dir/tracked-file2")
-                    .unwrap()
-                    .is_ignored());
-                assert!(tree
-                    .entry_for_path("ignored-dir/ignored-file2")
-                    .unwrap()
-                    .is_ignored());
+                assert!(!tree.is_path_ignored("tracked-dir/tracked-file2").unwrap());
+                assert!(tree.is_path_ignored("ignored-dir/ignored-file2").unwrap());
             });
         });
     }
@@ -1177,6 +1192,7 @@ mod tests {
                     path: root_dir.path().into(),
                     root_inode: None,
                     entries: Default::default(),
+                    ignores: Default::default(),
                 })),
                 notify_tx,
                 0,
@@ -1206,6 +1222,7 @@ mod tests {
                     path: root_dir.path().into(),
                     root_inode: None,
                     entries: Default::default(),
+                    ignores: Default::default(),
                 })),
                 notify_tx,
                 1,
@@ -1330,14 +1347,6 @@ mod tests {
             .collect()
     }
 
-    impl Entry {
-        fn is_ignored(&self) -> bool {
-            match self {
-                Entry::Dir { is_ignored, .. } | Entry::File { is_ignored, .. } => *is_ignored,
-            }
-        }
-    }
-
     impl Snapshot {
         fn check_invariants(&self) {
             for entry in self.entries.items() {

zed/src/worktree/fuzzy.rs 🔗

@@ -21,11 +21,10 @@ pub struct PathEntry {
     pub path_chars: CharBag,
     pub path: Arc<[char]>,
     pub lowercase_path: Arc<[char]>,
-    pub is_ignored: bool,
 }
 
 impl PathEntry {
-    pub fn new(ino: u64, path: &Path, is_ignored: bool) -> Self {
+    pub fn new(ino: u64, path: &Path) -> Self {
         let path = path.to_string_lossy();
         let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>().into();
         let path: Arc<[char]> = path.chars().collect::<Vec<_>>().into();
@@ -36,7 +35,6 @@ impl PathEntry {
             path_chars,
             path,
             lowercase_path,
-            is_ignored,
         }
     }
 }
@@ -84,6 +82,7 @@ where
 {
     let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
     let query = query.chars().collect::<Vec<_>>();
+
     let lowercase_query = &lowercase_query;
     let query = &query;
     let query_chars = CharBag::from(&lowercase_query[..]);
@@ -139,7 +138,7 @@ where
                         };
 
                         match_single_tree_paths(
-                            snapshot.id,
+                            snapshot,
                             skipped_prefix_len,
                             path_entries,
                             query,
@@ -176,7 +175,7 @@ where
 }
 
 fn match_single_tree_paths<'a>(
-    tree_id: usize,
+    snapshot: &Snapshot,
     skipped_prefix_len: usize,
     path_entries: impl Iterator<Item = &'a PathEntry>,
     query: &[char],
@@ -193,11 +192,11 @@ fn match_single_tree_paths<'a>(
     best_position_matrix: &mut Vec<usize>,
 ) {
     for path_entry in path_entries {
-        if !include_ignored && path_entry.is_ignored {
+        if !path_entry.path_chars.is_superset(query_chars.clone()) {
             continue;
         }
 
-        if !path_entry.path_chars.is_superset(query_chars.clone()) {
+        if !include_ignored && snapshot.is_inode_ignored(path_entry.ino).unwrap_or(true) {
             continue;
         }
 
@@ -232,7 +231,7 @@ fn match_single_tree_paths<'a>(
 
         if score > 0.0 {
             results.push(Reverse(PathMatch {
-                tree_id,
+                tree_id: snapshot.id,
                 entry_id: path_entry.ino,
                 path: path_entry.path.iter().skip(skipped_prefix_len).collect(),
                 score,
@@ -438,6 +437,7 @@ fn recursive_score_match(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::path::PathBuf;
 
     #[test]
     fn test_match_path_entries() {
@@ -500,7 +500,6 @@ mod tests {
                 path_chars,
                 path,
                 lowercase_path,
-                is_ignored: false,
             });
         }
 
@@ -511,7 +510,13 @@ mod tests {
 
         let mut results = BinaryHeap::new();
         match_single_tree_paths(
-            0,
+            &Snapshot {
+                id: 0,
+                path: PathBuf::new().into(),
+                root_inode: None,
+                ignores: Default::default(),
+                entries: Default::default(),
+            },
             0,
             path_entries.iter(),
             &query[..],