From 1d990806df6201a9f6bac288ab137df03662f3eb Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 18 Jul 2025 11:59:35 -0400 Subject: [PATCH] match at repo root and add test --- crates/worktree/src/ignore.rs | 28 ++++-- crates/worktree/src/worktree.rs | 122 +++++++++++++++----------- crates/worktree/src/worktree_tests.rs | 48 +++++++++- 3 files changed, 141 insertions(+), 57 deletions(-) diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs index 89c33fa280..a239520ff9 100644 --- a/crates/worktree/src/ignore.rs +++ b/crates/worktree/src/ignore.rs @@ -5,6 +5,7 @@ use std::{ffi::OsStr, path::Path, sync::Arc}; pub enum IgnoreStack { None, Global { + repo_root: Option>, ignore: Arc, }, Some { @@ -24,8 +25,8 @@ impl IgnoreStack { Arc::new(Self::All) } - pub fn global(ignore: Arc) -> Arc { - Arc::new(Self::Global { ignore }) + pub fn global(repo_root: Option>, ignore: Arc) -> Arc { + Arc::new(Self::Global { repo_root, ignore }) } pub fn append(self: Arc, abs_base_path: Arc, ignore: Arc) -> Arc { @@ -47,11 +48,24 @@ impl IgnoreStack { match self { Self::None => false, Self::All => true, - Self::Global { ignore } => match ignore.matched(abs_path, is_dir) { - ignore::Match::None => false, - ignore::Match::Ignore(_) => true, - ignore::Match::Whitelist(_) => false, - }, + Self::Global { repo_root, ignore } => { + let combined_path; + let abs_path = if let Some(repo_root) = repo_root { + combined_path = ignore.path().join( + abs_path + .strip_prefix(repo_root) + .expect("repo root should be a parent of matched path"), + ); + &combined_path + } else { + abs_path + }; + match ignore.matched(abs_path, is_dir) { + ignore::Match::None => false, + ignore::Match::Ignore(_) => true, + ignore::Match::Whitelist(_) => false, + } + } Self::Some { abs_base_path, ignore, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a0262bf604..b4a9729972 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3,7 +3,7 @@ mod worktree_settings; #[cfg(test)] mod worktree_tests; -use ::ignore::gitignore::{Gitignore, GitignoreBuilder, gitconfig_excludes_path}; +use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{Context as _, Result, anyhow}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; @@ -65,7 +65,7 @@ use std::{ use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit}; use text::{LineEnding, Rope}; use util::{ - ResultExt, path, + ResultExt, paths::{PathMatcher, SanitizedPath, home_dir}, }; pub use worktree_settings::WorktreeSettings; @@ -336,23 +336,6 @@ impl Default for WorkDirectory { } } -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub struct WorkDirectoryEntry(ProjectEntryId); - -impl Deref for WorkDirectoryEntry { - type Target = ProjectEntryId; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for WorkDirectoryEntry { - fn from(value: ProjectEntryId) -> Self { - WorkDirectoryEntry(value) - } -} - #[derive(Debug, Clone)] pub struct LocalSnapshot { snapshot: Snapshot, @@ -2794,8 +2777,14 @@ impl LocalSnapshot { inodes } - fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { + fn ignore_stack_for_abs_path( + &self, + abs_path: &Path, + is_dir: bool, + fs: &dyn Fs, + ) -> Arc { let mut new_ignores = Vec::new(); + let mut repo_root = None; for (index, ancestor) in abs_path.ancestors().enumerate() { if index > 0 { if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { @@ -2804,18 +2793,17 @@ impl LocalSnapshot { new_ignores.push((ancestor, None)); } } - if ancestor.join(*DOT_GIT).exists() { - // FIXME HERE + let metadata = smol::block_on(fs.metadata(&ancestor.join(*DOT_GIT))) + .ok() + .flatten(); + if metadata.is_some() { + repo_root = Some(Arc::from(ancestor)); break; } } - // FIXME the plan for global - // - the abs_base_path for the ignore is "" - // - match relative to dot git parent using existing stripping logic - // - global variant?? let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() { - IgnoreStack::global(global_gitignore) + IgnoreStack::global(repo_root, global_gitignore) } else { IgnoreStack::none() }; @@ -2945,9 +2933,15 @@ impl BackgroundScannerState { .any(|p| entry.path.starts_with(p)) } - fn enqueue_scan_dir(&self, abs_path: Arc, entry: &Entry, scan_job_tx: &Sender) { + fn enqueue_scan_dir( + &self, + abs_path: Arc, + entry: &Entry, + scan_job_tx: &Sender, + fs: &dyn Fs, + ) { let path = entry.path.clone(); - let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); + let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true, fs); let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); if !ancestor_inodes.contains(&entry.inode) { @@ -3888,14 +3882,21 @@ impl BackgroundScanner { let mut state = self.state.lock(); state.snapshot.scan_id += 1; if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(root_abs_path.as_path(), true); + let ignore_stack = state.snapshot.ignore_stack_for_abs_path( + root_abs_path.as_path(), + true, + self.fs.as_ref(), + ); if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) { 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.into(), &root_entry, &scan_job_tx); + state.enqueue_scan_dir( + root_abs_path.into(), + &root_entry, + &scan_job_tx, + self.fs.as_ref(), + ); } }; @@ -4183,7 +4184,10 @@ impl BackgroundScanner { state.snapshot.global_gitignore = ignore; // FIXME is_dir (do we care?) let abs_path = state.snapshot.abs_path().clone(); - let ignore_stack = state.snapshot.ignore_stack_for_abs_path(&abs_path, true); + let ignore_stack = + state + .snapshot + .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref()); (state.snapshot.clone(), ignore_stack, abs_path) }; let (scan_job_tx, scan_job_rx) = channel::unbounded(); @@ -4207,7 +4211,12 @@ impl BackgroundScanner { if let Some(entry) = state.snapshot.entry_for_path(ancestor) { if entry.kind == EntryKind::UnloadedDir { let abs_path = root_path.as_path().join(ancestor); - state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); + state.enqueue_scan_dir( + abs_path.into(), + entry, + &scan_job_tx, + self.fs.as_ref(), + ); state.paths_to_scan.insert(path.clone()); break; } @@ -4585,9 +4594,11 @@ impl BackgroundScanner { let abs_path: Arc = root_abs_path.as_path().join(path).into(); match metadata { Ok(Some((metadata, canonical_path))) => { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(&abs_path, metadata.is_dir); + let ignore_stack = state.snapshot.ignore_stack_for_abs_path( + &abs_path, + metadata.is_dir, + self.fs.as_ref(), + ); let is_external = !canonical_path.starts_with(&root_canonical_path); let mut fs_entry = Entry::new( path.clone(), @@ -4612,7 +4623,12 @@ impl BackgroundScanner { || (fs_entry.path.as_os_str().is_empty() && abs_path.file_name() == Some(*DOT_GIT)) { - state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx); + state.enqueue_scan_dir( + abs_path, + &fs_entry, + scan_queue_tx, + self.fs.as_ref(), + ); } else { fs_entry.kind = EntryKind::UnloadedDir; } @@ -4756,7 +4772,11 @@ impl BackgroundScanner { { ignores_to_update.next().unwrap(); } - let ignore_stack = prev_snapshot.ignore_stack_for_abs_path(&parent_abs_path, true); + let ignore_stack = prev_snapshot.ignore_stack_for_abs_path( + &parent_abs_path, + true, + self.fs.as_ref(), + ); Some((parent_abs_path, ignore_stack)) } }); @@ -4796,7 +4816,12 @@ impl BackgroundScanner { if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { let state = self.state.lock(); if state.should_scan_directory(&entry) { - state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue); + state.enqueue_scan_dir( + abs_path.clone(), + &entry, + &job.scan_queue, + self.fs.as_ref(), + ); } } @@ -4829,9 +4854,6 @@ impl BackgroundScanner { } } - if !entries_by_path_edits.is_empty() { - dbg!(&entries_by_path_edits); - } state .snapshot .entries_by_path @@ -5665,10 +5687,12 @@ fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, (repository_dir_abs_path, common_dir_abs_path) } +#[cfg(test)] fn global_gitignore_path() -> Option> { - if cfg!(test) { - Some(Path::new(path!("/home/zed/.config/git/ignore")).into()) - } else { - gitconfig_excludes_path().map(Into::into) - } + Some(Path::new(util::path!("/home/zed/.config/git/ignore")).into()) +} + +#[cfg(not(test))] +fn global_gitignore_path() -> Option> { + ::ignore::gitignore::gitconfig_excludes_path().map(Into::into) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 74d5aeaf40..48eb6cc08d 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2077,7 +2077,6 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA }); pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); - eprintln!(">>>>>>>>>> touch"); fs.touch_path(path!("/root/subproject")).await; worktree .update(cx, |worktree, _| { @@ -2098,6 +2097,53 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); } +#[gpui::test] +async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor); + fs.insert_tree( + path!("/home/zed"), + json!({ + ".config": { + "git": { + "ignore": "foo\n/bar\nbaz\n" + } + }, + "project": { + ".git": {}, + ".gitignore": "!baz", + "foo": "", + "bar": "", + "sub": { + "bar": "" + }, + "baz": "" + } + }), + ) + .await; + let worktree = Worktree::local( + path!("/home/zed/project").as_ref(), + true, + fs.clone(), + Arc::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + worktree + .update(cx, |worktree, _| { + worktree.as_local().unwrap().scan_complete() + }) + .await; + cx.run_until_parked(); + + worktree.update(cx, |worktree, _cx| { + check_worktree_entries(worktree, &[], &["foo", "bar"], &["sub/bar", "baz"], &[]); + }) +} + #[track_caller] fn check_worktree_entries( tree: &Worktree,