diff --git a/crates/ide-lsp/src/external_project.rs b/crates/ide-lsp/src/external_project.rs index cb9393c9e..b00900a4e 100644 --- a/crates/ide-lsp/src/external_project.rs +++ b/crates/ide-lsp/src/external_project.rs @@ -59,7 +59,7 @@ fn source_files(root: &Path, exclude_root: &Path, excludes: Vec, limit: } let Ok(ignores) = ignores.build() else { return Vec::new() }; let mut walk = ignore::WalkBuilder::new(root); - walk.require_git(false).follow_links(false).filter_entry(move |entry| { + walk.require_git(ide_core::ignored::inside_git_repo(root)).follow_links(false).filter_entry(move |entry| { if entry.depth() == 0 { return true; } diff --git a/crates/ide-sdk/src/lombok.rs b/crates/ide-sdk/src/lombok.rs index 8b292d482..3a4b41927 100644 --- a/crates/ide-sdk/src/lombok.rs +++ b/crates/ide-sdk/src/lombok.rs @@ -246,7 +246,7 @@ fn sources_import_lombok(root: &Path) -> bool { // Imports precede the first type, after at most a license header. const HEAD: u64 = 8 * 1024; let mut walker = ignore::WalkBuilder::new(root); - walker.require_git(false).follow_links(false).filter_entry(|e| { + walker.require_git(ide_core::ignored::inside_git_repo(root)).follow_links(false).filter_entry(|e| { e.depth() == 0 || !e.file_type().is_some_and(|t| t.is_dir()) || !OUTPUT_DIRS.contains(&e.file_name().to_string_lossy().as_ref()) diff --git a/crates/ide-search/tests/search.rs b/crates/ide-search/tests/search.rs index bd0f7d097..b63c97220 100644 --- a/crates/ide-search/tests/search.rs +++ b/crates/ide-search/tests/search.rs @@ -207,6 +207,34 @@ fn honours_ignore_files_hidden_and_git() { assert_eq!(paths(&views), ["src/a.rs", "sub/kept.txt"]); } +#[test] +fn nested_repository_ignores_outer_repository_rules() { + // #1254: the opened project is a Git repository inside a folder its parent repository ignores. + let p = project(&[ + (".git/HEAD", b"ref: refs/heads/main\n"), + ("projects/.gitignore", b"*\n!.gitignore\n"), + ("projects/nested/.git/HEAD", b"ref: refs/heads/main\n"), + ("projects/nested/.gitignore", b"*.log\n"), + ("projects/nested/search-test.txt", b"somesss\n"), + ("projects/nested/debug.log", b"somesss\n"), + ]); + let nested = p.path().join("projects/nested"); + let (views, _) = run(&nested, &query("somesss")); + assert_eq!(paths(&views), ["search-test.txt"]); + + let files = ide_search::scope_files( + &nested, + &SearchScope::Project, + &ide_search::FileMask::new(&[], &[]).unwrap(), + None, + &CancellationToken::new(), + ) + .unwrap(); + let mut rels: Vec<_> = files.iter().map(|f| f.rel_path.as_str()).collect(); + rels.sort(); + assert_eq!(rels, ["search-test.txt"]); +} + #[test] fn skips_binary_files() { let mut late_nul = b"needle\n".to_vec(); diff --git a/crates/ide-vfs/tests/vfs.rs b/crates/ide-vfs/tests/vfs.rs index 76d2cdc71..0a08f7326 100644 --- a/crates/ide-vfs/tests/vfs.rs +++ b/crates/ide-vfs/tests/vfs.rs @@ -259,6 +259,47 @@ fn walk_honours_ignores() { assert_eq!(found, expected); } +#[test] +fn walk_scopes_gitignore_to_each_repository() { + let (_t, r) = root(); + // An outer repository ignores everything under `projects/`, nested repository included (#1254). + write(&r.join("outer/.git/HEAD"), "ref: refs/heads/main\n"); + write(&r.join("outer/top.txt"), ""); + write(&r.join("outer/projects/.gitignore"), "*\n!.gitignore\n"); + write(&r.join("outer/projects/nested/.git/HEAD"), "ref: refs/heads/main\n"); + write(&r.join("outer/projects/nested/.git/info/exclude"), "local.txt\n"); + write(&r.join("outer/projects/nested/.gitignore"), "*.log\n"); + write(&r.join("outer/projects/nested/search-test.txt"), ""); + write(&r.join("outer/projects/nested/src/a.rs"), ""); + write(&r.join("outer/projects/nested/debug.log"), ""); + write(&r.join("outer/projects/nested/local.txt"), ""); + write(&r.join("outer/projects/plain/file.txt"), ""); + write(&r.join("loose/.gitignore"), "*.log\n"); + write(&r.join("loose/a.txt"), ""); + write(&r.join("loose/b.log"), ""); + + let walk = |dir: &str| { + let base = r.join(dir); + let found = Mutex::new(BTreeSet::new()); + walk_files(&base, &[], |p| { + found.lock().insert(p.strip_prefix(&base).unwrap().to_string_lossy().replace('\\', "/")); + }); + found.into_inner().into_iter().collect::>() + }; + let cases: [(&str, &[&str]); 4] = [ + // The nested repository's own rules apply; the outer repository's do not. + ("outer/projects/nested", &[".gitignore", "search-test.txt", "src/a.rs"]), + // A plain folder inside the outer repository is still governed by it, as in Git. + ("outer/projects/plain", &[]), + ("outer", &["projects/.gitignore", "top.txt"]), + // Outside any repository `.gitignore` still applies. + ("loose", &[".gitignore", "a.txt"]), + ]; + for (dir, expected) in cases { + assert_eq!(walk(dir), expected, "{dir}"); + } +} + fn wait_for(events: &Mutex>, pred: impl Fn(&[FsBatch]) -> bool) -> bool { let deadline = Instant::now() + Duration::from_secs(10); while Instant::now() < deadline {