Merge pull request #1309 from zed-industries/gitignore-traverse-ancestors

Antonio Scandurra created

Honor `.gitignore` files above worktree's root

Change summary

crates/file_finder/src/file_finder.rs | 112 +++++++++++++
crates/project/src/ignore.rs          |  16 +-
crates/project/src/project.rs         |  62 +------
crates/project/src/project_tests.rs   |  44 -----
crates/project/src/worktree.rs        | 231 +++++++++++++++++++---------
5 files changed, 280 insertions(+), 185 deletions(-)

Detailed changes

crates/file_finder/src/file_finder.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use picker::{Picker, PickerDelegate};
-use project::{Project, ProjectPath, WorktreeId};
+use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
 use settings::Settings;
 use std::{
     path::Path,
@@ -134,17 +134,40 @@ impl FileFinder {
     }
 
     fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        let worktrees = self
+            .project
+            .read(cx)
+            .visible_worktrees(cx)
+            .collect::<Vec<_>>();
+        let include_root_name = worktrees.len() > 1;
+        let candidate_sets = worktrees
+            .into_iter()
+            .map(|worktree| {
+                let worktree = worktree.read(cx);
+                PathMatchCandidateSet {
+                    snapshot: worktree.snapshot(),
+                    include_ignored: worktree
+                        .root_entry()
+                        .map_or(false, |entry| entry.is_ignored),
+                    include_root_name,
+                }
+            })
+            .collect::<Vec<_>>();
+
         let search_id = util::post_inc(&mut self.search_count);
         self.cancel_flag.store(true, atomic::Ordering::Relaxed);
         self.cancel_flag = Arc::new(AtomicBool::new(false));
         let cancel_flag = self.cancel_flag.clone();
-        let project = self.project.clone();
         cx.spawn(|this, mut cx| async move {
-            let matches = project
-                .read_with(&cx, |project, cx| {
-                    project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
-                })
-                .await;
+            let matches = fuzzy::match_paths(
+                candidate_sets.as_slice(),
+                &query,
+                false,
+                100,
+                &cancel_flag,
+                cx.background(),
+            )
+            .await;
             let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
             this.update(&mut cx, |this, cx| {
                 this.set_matches(search_id, did_cancel, query, matches, cx)
@@ -389,6 +412,51 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_ignored_files(cx: &mut gpui::TestAppContext) {
+        let app_state = cx.update(AppState::test);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/ancestor",
+                json!({
+                    ".gitignore": "ignored-root",
+                    "ignored-root": {
+                        "happiness": "",
+                        "height": "",
+                        "hi": "",
+                        "hiccup": "",
+                    },
+                    "tracked-root": {
+                        ".gitignore": "height",
+                        "happiness": "",
+                        "height": "",
+                        "hi": "",
+                        "hiccup": "",
+                    },
+                }),
+            )
+            .await;
+
+        let project = Project::test(
+            app_state.fs.clone(),
+            [
+                "/ancestor/tracked-root".as_ref(),
+                "/ancestor/ignored-root".as_ref(),
+            ],
+            cx,
+        )
+        .await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, finder) =
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+        finder
+            .update(cx, |f, cx| f.spawn_search("hi".into(), cx))
+            .await;
+        finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 7));
+    }
+
     #[gpui::test]
     async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
         let app_state = cx.update(AppState::test);
@@ -475,4 +543,34 @@ mod tests {
             assert_eq!(f.selected_index(), 0);
         });
     }
+
+    #[gpui::test]
+    async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
+        let app_state = cx.update(AppState::test);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "dir1": {},
+                    "dir2": {
+                        "dir3": {}
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, finder) =
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+        finder
+            .update(cx, |f, cx| f.spawn_search("dir".into(), cx))
+            .await;
+        cx.read(|cx| {
+            let finder = finder.read(cx);
+            assert_eq!(finder.matches.len(), 0);
+        });
+    }
 }

crates/project/src/ignore.rs 🔗

@@ -4,7 +4,7 @@ use std::{ffi::OsStr, path::Path, sync::Arc};
 pub enum IgnoreStack {
     None,
     Some {
-        base: Arc<Path>,
+        abs_base_path: Arc<Path>,
         ignore: Arc<Gitignore>,
         parent: Arc<IgnoreStack>,
     },
@@ -24,19 +24,19 @@ impl IgnoreStack {
         matches!(self, IgnoreStack::All)
     }
 
-    pub fn append(self: Arc<Self>, base: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
+    pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
         match self.as_ref() {
             IgnoreStack::All => self,
             _ => Arc::new(Self::Some {
-                base,
+                abs_base_path,
                 ignore,
                 parent: self,
             }),
         }
     }
 
-    pub fn is_path_ignored(&self, path: &Path, is_dir: bool) -> bool {
-        if is_dir && path.file_name() == Some(OsStr::new(".git")) {
+    pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool {
+        if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) {
             return true;
         }
 
@@ -44,11 +44,11 @@ impl IgnoreStack {
             Self::None => false,
             Self::All => true,
             Self::Some {
-                base,
+                abs_base_path,
                 ignore,
                 parent: prev,
-            } => match ignore.matched(path.strip_prefix(base).unwrap(), is_dir) {
-                ignore::Match::None => prev.is_path_ignored(path, is_dir),
+            } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
+                ignore::Match::None => prev.is_abs_path_ignored(abs_path, is_dir),
                 ignore::Match::Ignore(_) => true,
                 ignore::Match::Whitelist(_) => false,
             },

crates/project/src/project.rs 🔗

@@ -13,7 +13,6 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
-use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
@@ -58,7 +57,7 @@ use std::{
     rc::Rc,
     str,
     sync::{
-        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
     },
     time::Instant,
@@ -5678,43 +5677,6 @@ impl Project {
         })
     }
 
-    pub fn match_paths<'a>(
-        &self,
-        query: &'a str,
-        include_ignored: bool,
-        smart_case: bool,
-        max_results: usize,
-        cancel_flag: &'a AtomicBool,
-        cx: &AppContext,
-    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
-        let worktrees = self
-            .worktrees(cx)
-            .filter(|worktree| worktree.read(cx).is_visible())
-            .collect::<Vec<_>>();
-        let include_root_name = worktrees.len() > 1;
-        let candidate_sets = worktrees
-            .into_iter()
-            .map(|worktree| CandidateSet {
-                snapshot: worktree.read(cx).snapshot(),
-                include_ignored,
-                include_root_name,
-            })
-            .collect::<Vec<_>>();
-
-        let background = cx.background().clone();
-        async move {
-            fuzzy::match_paths(
-                candidate_sets.as_slice(),
-                query,
-                smart_case,
-                max_results,
-                cancel_flag,
-                background,
-            )
-            .await
-        }
-    }
-
     fn edits_from_lsp(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -5942,14 +5904,14 @@ impl OpenBuffer {
     }
 }
 
-struct CandidateSet {
-    snapshot: Snapshot,
-    include_ignored: bool,
-    include_root_name: bool,
+pub struct PathMatchCandidateSet {
+    pub snapshot: Snapshot,
+    pub include_ignored: bool,
+    pub include_root_name: bool,
 }
 
-impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
-    type Candidates = CandidateSetIter<'a>;
+impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
+    type Candidates = PathMatchCandidateSetIter<'a>;
 
     fn id(&self) -> usize {
         self.snapshot.id().to_usize()
@@ -5974,23 +5936,23 @@ impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
     }
 
     fn candidates(&'a self, start: usize) -> Self::Candidates {
-        CandidateSetIter {
+        PathMatchCandidateSetIter {
             traversal: self.snapshot.files(self.include_ignored, start),
         }
     }
 }
 
-struct CandidateSetIter<'a> {
+pub struct PathMatchCandidateSetIter<'a> {
     traversal: Traversal<'a>,
 }
 
-impl<'a> Iterator for CandidateSetIter<'a> {
-    type Item = PathMatchCandidate<'a>;
+impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
+    type Item = fuzzy::PathMatchCandidate<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
         self.traversal.next().map(|entry| {
             if let EntryKind::File(char_bag) = entry.kind {
-                PathMatchCandidate {
+                fuzzy::PathMatchCandidate {
                     path: &entry.path,
                     char_bag,
                 }

crates/project/src/project_tests.rs 🔗

@@ -8,12 +8,12 @@ use language::{
 };
 use lsp::Url;
 use serde_json::json;
-use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll};
+use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
 use unindent::Unindent as _;
 use util::{assert_set_eq, test::temp_tree};
 
 #[gpui::test]
-async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
+async fn test_symlinks(cx: &mut gpui::TestAppContext) {
     let dir = temp_tree(json!({
         "root": {
             "apple": "",
@@ -38,7 +38,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
     .unwrap();
 
     let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
-
     project.read_with(cx, |project, cx| {
         let tree = project.worktrees(cx).next().unwrap().read(cx);
         assert_eq!(tree.file_count(), 5);
@@ -47,23 +46,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
             tree.inode_for_path("finnochio/grape")
         );
     });
-
-    let cancel_flag = Default::default();
-    let results = project
-        .read_with(cx, |project, cx| {
-            project.match_paths("bna", false, false, 10, &cancel_flag, cx)
-        })
-        .await;
-    assert_eq!(
-        results
-            .into_iter()
-            .map(|result| result.path)
-            .collect::<Vec<Arc<Path>>>(),
-        vec![
-            PathBuf::from("banana/carrot/date").into(),
-            PathBuf::from("banana/carrot/endive").into(),
-        ]
-    );
 }
 
 #[gpui::test]
@@ -1645,28 +1627,6 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
     chunks
 }
 
-#[gpui::test]
-async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
-    let dir = temp_tree(json!({
-        "root": {
-            "dir1": {},
-            "dir2": {
-                "dir3": {}
-            }
-        }
-    }));
-
-    let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
-    let cancel_flag = Default::default();
-    let results = project
-        .read_with(cx, |project, cx| {
-            project.match_paths("dir", false, false, 10, &cancel_flag, cx)
-        })
-        .await;
-
-    assert!(results.is_empty());
-}
-
 #[gpui::test(iterations = 10)]
 async fn test_definition(cx: &mut gpui::TestAppContext) {
     let mut language = Language::new(

crates/project/src/worktree.rs 🔗

@@ -102,7 +102,7 @@ pub struct Snapshot {
 #[derive(Clone)]
 pub struct LocalSnapshot {
     abs_path: Arc<Path>,
-    ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
+    ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
     removed_entry_ids: HashMap<u64, ProjectEntryId>,
     next_entry_id: Arc<AtomicUsize>,
     snapshot: Snapshot,
@@ -370,7 +370,7 @@ impl LocalWorktree {
         let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
             let mut snapshot = LocalSnapshot {
                 abs_path,
-                ignores: Default::default(),
+                ignores_by_parent_abs_path: Default::default(),
                 removed_entry_ids: Default::default(),
                 next_entry_id,
                 snapshot: Snapshot {
@@ -819,8 +819,8 @@ impl LocalWorktree {
                 {
                     let mut snapshot = this.background_snapshot.lock();
                     entry.is_ignored = snapshot
-                        .ignore_stack_for_path(&path, entry.is_dir())
-                        .is_path_ignored(&path, entry.is_dir());
+                        .ignore_stack_for_abs_path(&abs_path, entry.is_dir())
+                        .is_abs_path_ignored(&abs_path, entry.is_dir());
                     if let Some(old_path) = old_path {
                         snapshot.remove_path(&old_path);
                     }
@@ -1331,11 +1331,12 @@ impl LocalSnapshot {
     fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
         if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
             let abs_path = self.abs_path.join(&entry.path);
-            match build_gitignore(&abs_path, fs) {
+            match smol::block_on(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));
+                    self.ignores_by_parent_abs_path.insert(
+                        abs_path.parent().unwrap().into(),
+                        (Arc::new(ignore), self.scan_id),
+                    );
                 }
                 Err(error) => {
                     log::error!(
@@ -1387,7 +1388,10 @@ impl LocalSnapshot {
         };
 
         if let Some(ignore) = ignore {
-            self.ignores.insert(parent_path, (ignore, self.scan_id));
+            self.ignores_by_parent_abs_path.insert(
+                self.abs_path.join(&parent_path).into(),
+                (ignore, self.scan_id),
+            );
         }
         if matches!(parent_entry.kind, EntryKind::PendingDir) {
             parent_entry.kind = EntryKind::Dir;
@@ -1472,16 +1476,20 @@ impl LocalSnapshot {
         self.entries_by_id.edit(entries_by_id_edits, &());
 
         if path.file_name() == Some(&GITIGNORE) {
-            if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) {
+            let abs_parent_path = self.abs_path.join(path.parent().unwrap());
+            if let Some((_, scan_id)) = self
+                .ignores_by_parent_abs_path
+                .get_mut(abs_parent_path.as_path())
+            {
                 *scan_id = self.snapshot.scan_id;
             }
         }
     }
 
-    fn ignore_stack_for_path(&self, path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
+    fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
         let mut new_ignores = Vec::new();
-        for ancestor in path.ancestors().skip(1) {
-            if let Some((ignore, _)) = self.ignores.get(ancestor) {
+        for ancestor in abs_path.ancestors().skip(1) {
+            if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
                 new_ignores.push((ancestor, Some(ignore.clone())));
             } else {
                 new_ignores.push((ancestor, None));
@@ -1489,16 +1497,16 @@ impl LocalSnapshot {
         }
 
         let mut ignore_stack = IgnoreStack::none();
-        for (parent_path, ignore) in new_ignores.into_iter().rev() {
-            if ignore_stack.is_path_ignored(&parent_path, true) {
+        for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
+            if ignore_stack.is_abs_path_ignored(&parent_abs_path, true) {
                 ignore_stack = IgnoreStack::all();
                 break;
             } else if let Some(ignore) = ignore {
-                ignore_stack = ignore_stack.append(Arc::from(parent_path), ignore);
+                ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore);
             }
         }
 
-        if ignore_stack.is_path_ignored(path, is_dir) {
+        if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
             ignore_stack = IgnoreStack::all();
         }
 
@@ -1506,8 +1514,8 @@ impl LocalSnapshot {
     }
 }
 
-fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
-    let contents = smol::block_on(fs.load(&abs_path))?;
+async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
+    let contents = fs.load(&abs_path).await?;
     let parent = abs_path.parent().unwrap_or(Path::new("/"));
     let mut builder = GitignoreBuilder::new(parent);
     for line in contents.lines() {
@@ -2040,24 +2048,48 @@ 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_parent_abs_path
+                    .insert(ancestor.into(), (ignore.into(), 0));
+            }
+        }
+
+        let ignore_stack = {
+            let mut snapshot = self.snapshot.lock();
+            let ignore_stack = snapshot.ignore_stack_for_abs_path(&root_abs_path, true);
+            if ignore_stack.is_all() {
+                if let Some(mut root_entry) = snapshot.root_entry().cloned() {
+                    root_entry.is_ignored = true;
+                    snapshot.insert_entry(root_entry, self.fs.as_ref());
+                }
+            }
+            ignore_stack
+        };
+
         if is_dir {
             let path: Arc<Path> = Arc::from(Path::new(""));
-            let abs_path = self.abs_path();
             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();
@@ -2117,10 +2149,11 @@ 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 {
-                match build_gitignore(&child_abs_path, self.fs.as_ref()) {
+                match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
                     Ok(ignore) => {
                         let ignore = Arc::new(ignore);
-                        ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
+                        ignore_stack =
+                            ignore_stack.append(job.abs_path.as_path().into(), ignore.clone());
                         new_ignore = Some(ignore);
                     }
                     Err(error) => {
@@ -2138,7 +2171,9 @@ impl BackgroundScanner {
                 // new jobs as well.
                 let mut new_jobs = new_jobs.iter_mut();
                 for entry in &mut new_entries {
-                    entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir());
+                    let entry_abs_path = self.abs_path().join(&entry.path);
+                    entry.is_ignored =
+                        ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
                     if entry.is_dir() {
                         new_jobs.next().unwrap().ignore_stack = if entry.is_ignored {
                             IgnoreStack::all()
@@ -2157,7 +2192,7 @@ impl BackgroundScanner {
             );
 
             if child_metadata.is_dir {
-                let is_ignored = ignore_stack.is_path_ignored(&child_path, true);
+                let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
                 child_entry.is_ignored = is_ignored;
                 new_entries.push(child_entry);
                 new_jobs.push(ScanJob {
@@ -2171,7 +2206,7 @@ impl BackgroundScanner {
                     scan_queue: job.scan_queue.clone(),
                 });
             } else {
-                child_entry.is_ignored = ignore_stack.is_path_ignored(&child_path, false);
+                child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
                 new_entries.push(child_entry);
             };
         }
@@ -2200,8 +2235,8 @@ impl BackgroundScanner {
             next_entry_id = snapshot.next_entry_id.clone();
         }
 
-        let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await {
-            abs_path
+        let root_canonical_path = if let Ok(path) = self.fs.canonicalize(&root_abs_path).await {
+            path
         } else {
             return false;
         };
@@ -2221,27 +2256,29 @@ impl BackgroundScanner {
             let mut snapshot = self.snapshot.lock();
             snapshot.scan_id += 1;
             for event in &events {
-                if let Ok(path) = event.path.strip_prefix(&root_abs_path) {
+                if let Ok(path) = event.path.strip_prefix(&root_canonical_path) {
                     snapshot.remove_path(&path);
                 }
             }
 
             for (event, metadata) in events.into_iter().zip(metadata.into_iter()) {
-                let path: Arc<Path> = match event.path.strip_prefix(&root_abs_path) {
+                let path: Arc<Path> = match event.path.strip_prefix(&root_canonical_path) {
                     Ok(path) => Arc::from(path.to_path_buf()),
                     Err(_) => {
                         log::error!(
                             "unexpected event {:?} for root path {:?}",
                             event.path,
-                            root_abs_path
+                            root_canonical_path
                         );
                         continue;
                     }
                 };
+                let abs_path = root_abs_path.join(&path);
 
                 match metadata {
                     Ok(Some(metadata)) => {
-                        let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir);
+                        let ignore_stack =
+                            snapshot.ignore_stack_for_abs_path(&abs_path, metadata.is_dir);
                         let mut fs_entry = Entry::new(
                             path.clone(),
                             &metadata,
@@ -2253,7 +2290,7 @@ impl BackgroundScanner {
                         if metadata.is_dir {
                             self.executor
                                 .block(scan_queue_tx.send(ScanJob {
-                                    abs_path: event.path,
+                                    abs_path,
                                     path,
                                     ignore_stack,
                                     scan_queue: scan_queue_tx.clone(),
@@ -2301,37 +2338,42 @@ impl BackgroundScanner {
 
         let mut ignores_to_update = Vec::new();
         let mut ignores_to_delete = Vec::new();
-        for (parent_path, (_, scan_id)) in &snapshot.ignores {
-            if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() {
-                ignores_to_update.push(parent_path.clone());
-            }
+        for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_parent_abs_path {
+            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_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());
+                }
             }
         }
 
-        for parent_path in ignores_to_delete {
-            snapshot.ignores.remove(&parent_path);
-            self.snapshot.lock().ignores.remove(&parent_path);
+        for parent_abs_path in ignores_to_delete {
+            snapshot.ignores_by_parent_abs_path.remove(&parent_abs_path);
+            self.snapshot
+                .lock()
+                .ignores_by_parent_abs_path
+                .remove(&parent_abs_path);
         }
 
         let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
         ignores_to_update.sort_unstable();
         let mut ignores_to_update = ignores_to_update.into_iter().peekable();
-        while let Some(parent_path) = ignores_to_update.next() {
+        while let Some(parent_abs_path) = ignores_to_update.next() {
             while ignores_to_update
                 .peek()
-                .map_or(false, |p| p.starts_with(&parent_path))
+                .map_or(false, |p| p.starts_with(&parent_abs_path))
             {
                 ignores_to_update.next().unwrap();
             }
 
-            let ignore_stack = snapshot.ignore_stack_for_path(&parent_path, true);
+            let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true);
             ignore_queue_tx
                 .send(UpdateIgnoreStatusJob {
-                    path: parent_path,
+                    abs_path: parent_abs_path,
                     ignore_stack,
                     ignore_queue: ignore_queue_tx.clone(),
                 })
@@ -2355,15 +2397,17 @@ impl BackgroundScanner {
 
     async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
         let mut ignore_stack = job.ignore_stack;
-        if let Some((ignore, _)) = snapshot.ignores.get(&job.path) {
-            ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
+        if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
+            ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
         }
 
         let mut entries_by_id_edits = Vec::new();
         let mut entries_by_path_edits = Vec::new();
-        for mut entry in snapshot.child_entries(&job.path).cloned() {
+        let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap();
+        for mut entry in snapshot.child_entries(path).cloned() {
             let was_ignored = entry.is_ignored;
-            entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir());
+            let abs_path = self.abs_path().join(&entry.path);
+            entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
             if entry.is_dir() {
                 let child_ignore_stack = if entry.is_ignored {
                     IgnoreStack::all()
@@ -2372,7 +2416,7 @@ impl BackgroundScanner {
                 };
                 job.ignore_queue
                     .send(UpdateIgnoreStatusJob {
-                        path: entry.path.clone(),
+                        abs_path: abs_path.into(),
                         ignore_stack: child_ignore_stack,
                         ignore_queue: job.ignore_queue.clone(),
                     })
@@ -2413,7 +2457,7 @@ struct ScanJob {
 }
 
 struct UpdateIgnoreStatusJob {
-    path: Arc<Path>,
+    abs_path: Arc<Path>,
     ignore_stack: Arc<IgnoreStack>,
     ignore_queue: Sender<UpdateIgnoreStatusJob>,
 }
@@ -2766,23 +2810,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(),
@@ -2795,23 +2844,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);
         });
     }
 
@@ -2891,7 +2964,7 @@ mod tests {
         let mut initial_snapshot = LocalSnapshot {
             abs_path: root_dir.path().into(),
             removed_entry_ids: Default::default(),
-            ignores: Default::default(),
+            ignores_by_parent_abs_path: Default::default(),
             next_entry_id: next_entry_id.clone(),
             snapshot: Snapshot {
                 id: WorktreeId::from_usize(0),
@@ -3176,8 +3249,10 @@ mod tests {
                 .collect::<Vec<_>>();
             assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
 
-            for (ignore_parent_path, _) in &self.ignores {
-                assert!(self.entry_for_path(ignore_parent_path).is_some());
+            for (ignore_parent_abs_path, _) in &self.ignores_by_parent_abs_path {
+                let ignore_parent_path =
+                    ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
+                assert!(self.entry_for_path(&ignore_parent_path).is_some());
                 assert!(self
                     .entry_for_path(ignore_parent_path.join(&*GITIGNORE))
                     .is_some());