fix(search): stop an outer repository's .gitignore at a nested repository (#1309)

The walkers used require_git(false), and with it the ignore crate never treats a .git directory as a boundary, so an outer repository's .gitignore hid a project that is its own repository inside an ignored folder (Find in Files found nothing). Walkers now require git when the walked folder is inside a repository, so rules stop at a nested repository's root as in git; folders outside any repository still honour their .gitignore. Covers the Find in Files engine, the file scope, the index walk and ide-vfs walk_files.
This commit is contained in:
l0ng-ai
2026-11-04 23:16:47 +08:00
committed by GitHub
parent 092a700ddc
commit a08707dabd
6 changed files with 71 additions and 5 deletions
+23
View File
@@ -2,6 +2,18 @@
//! treats as nonexistent — hidden from the project view and navigation, never
//! indexed or searched, never offered as unversioned changes.
use std::path::Path;
/// Whether `dir` lies in a Git (or jj) working tree: it or an ancestor holds `.git` / `.jj`.
///
/// Walkers pass it to `ignore::WalkBuilder::require_git`: inside a repository, `.gitignore`
/// rules then stop at a nested repository's root as in git, so an outer repository's
/// `.gitignore` does not hide a project that is its own repository. A folder outside any
/// repository still honours its `.gitignore`.
pub fn inside_git_repo(dir: &Path) -> bool {
dir.ancestors().any(|d| d.join(".git").exists() || d.join(".jj").exists())
}
/// IDEA's default list, plus `.jj` and the Windows shell files.
pub const DEFAULT_IGNORED_NAMES: &[&str] = &[
"*.pyc",
@@ -103,4 +115,15 @@ mod tests {
assert!(m.is_ignored_path("pkg/__pycache__/mod.cpython-312.pyc"));
assert!(!m.is_ignored_path("src/main.rs"));
}
#[test]
fn finds_the_repository_at_or_above_a_directory() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("a/b");
std::fs::create_dir_all(&sub).unwrap();
assert!(!inside_git_repo(&sub));
std::fs::create_dir(dir.path().join("a/.git")).unwrap();
assert!(inside_git_repo(&sub));
assert!(inside_git_repo(&dir.path().join("a")));
}
}
+20 -1
View File
@@ -215,7 +215,7 @@ impl PathFilter {
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.require_git(false)
.require_git(ide_core::ignored::inside_git_repo(dir))
.follow_links(false)
.filter_entry(move |e| {
let name = e.file_name().to_string_lossy();
@@ -311,6 +311,25 @@ mod tests {
assert!(!filter.is_excluded(&root.join("src/new.rs"), false));
}
#[test]
fn a_nested_repository_is_not_hidden_by_the_outer_repositorys_gitignore() {
let dir = tempfile::tempdir().unwrap();
let base = dunce::canonicalize(dir.path()).unwrap();
for f in ["projects/nested/src/a.rs", "projects/nested/out/x.rs"] {
touch(&base, f);
}
std::fs::create_dir(base.join(".git")).unwrap();
std::fs::create_dir(base.join("projects/nested/.git")).unwrap();
std::fs::write(base.join("projects/.gitignore"), "*\n!.gitignore\n").unwrap();
std::fs::write(base.join("projects/nested/.gitignore"), "out/\n").unwrap();
let root = base.join("projects/nested");
let filter = PathFilter::new(&root, &vec![].into(), &[]);
let mut found: Vec<String> =
filter.walk(&root, &AtomicBool::new(false)).iter().filter_map(|f| filter.rel(&f.path)).collect();
found.sort();
assert_eq!(found, [".gitignore", "src/a.rs"]);
}
#[test]
fn extra_content_roots_are_walked_and_prefixed() {
let dir = tempfile::tempdir().unwrap();
+1 -1
View File
@@ -155,7 +155,7 @@ pub(crate) fn run(req: Request<'_>) -> IdeResult<SearchSummary> {
.git_global(true)
.git_exclude(true)
.parents(true)
.require_git(false)
.require_git(ide_core::ignored::inside_git_repo(first))
.follow_links(false)
.threads(std::thread::available_parallelism().map_or(4, |n| n.get()).min(12))
.filter_entry(|e| e.file_name() != ".git");
+1 -1
View File
@@ -54,7 +54,7 @@ pub fn scope_files(
.git_global(true)
.git_exclude(true)
.parents(true)
.require_git(false)
.require_git(ide_core::ignored::inside_git_repo(first))
.follow_links(false)
.threads(std::thread::available_parallelism().map_or(4, |n| n.get()).min(12))
.filter_entry(|e| e.file_name() != ".git");
+23
View File
@@ -643,3 +643,26 @@ fn archives_are_skipped() {
let (views, _) = run(p.path(), &query("needle"));
assert_eq!(paths(&views), ["src/a.txt"]);
}
#[test]
fn a_nested_repository_is_not_hidden_by_the_outer_repositorys_gitignore() {
// parent/.git, parent/projects/.gitignore ignores everything, parent/projects/nested is its own repository.
let p = project(&[
("projects/.gitignore", b"*\n!.gitignore\n"),
("projects/nested/search-test.txt", b"somesss\n"),
("projects/nested/.gitignore", b"*.log\n"),
("projects/nested/debug.log", b"somesss\n"),
]);
std::fs::create_dir(p.path().join(".git")).unwrap();
std::fs::create_dir(p.path().join("projects/nested/.git")).unwrap();
let (views, _) = run(&p.path().join("projects/nested"), &query("somesss"));
assert_eq!(paths(&views), ["search-test.txt"], "the nested repository's own .gitignore still applies");
}
#[test]
fn a_project_inside_a_repository_keeps_the_repositorys_gitignore() {
let p = project(&[(".gitignore", b"build/\n"), ("app/build/out.txt", b"hit\n"), ("app/src/a.txt", b"hit\n")]);
std::fs::create_dir(p.path().join(".git")).unwrap();
let (views, _) = run(&p.path().join("app"), &query("hit"));
assert_eq!(paths(&views), ["src/a.txt"]);
}
+3 -2
View File
@@ -274,13 +274,14 @@ pub fn copy(from: &Path, to: &Path) -> IdeResult<()> {
/// honouring .gitignore, .ignore and `excludes`. Shared by index and search.
///
/// Hidden files are included; `.git` is always skipped. `.gitignore` applies
/// even outside a git repository. Symlinks to files are visited, symlinked
/// even outside a git repository; inside one, a nested repository's root is a
/// boundary for the outer repository's rules. Symlinks to files are visited, symlinked
/// directories are not followed.
pub fn walk_files(root: &Path, excludes: &[String], visit: impl Fn(&Path) + Send + Sync) {
let excludes = Arc::new(ExcludeMatcher::new(excludes));
let base = root.to_path_buf();
let mut builder = ignore::WalkBuilder::new(root);
builder.hidden(false).require_git(false).follow_links(false);
builder.hidden(false).require_git(ide_core::ignored::inside_git_repo(root)).follow_links(false);
builder.filter_entry(move |entry| {
if entry.depth() > 0 && entry.file_name() == ".git" {
return false;