diff --git a/crates/ide-core/src/ignored.rs b/crates/ide-core/src/ignored.rs index fb6c20a3..efda11ac 100644 --- a/crates/ide-core/src/ignored.rs +++ b/crates/ide-core/src/ignored.rs @@ -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"))); + } } diff --git a/crates/ide-index/src/filter.rs b/crates/ide-index/src/filter.rs index b826df87..0a612b31 100644 --- a/crates/ide-index/src/filter.rs +++ b/crates/ide-index/src/filter.rs @@ -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 = + 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(); diff --git a/crates/ide-search/src/engine.rs b/crates/ide-search/src/engine.rs index 802595b0..ccb3e356 100644 --- a/crates/ide-search/src/engine.rs +++ b/crates/ide-search/src/engine.rs @@ -155,7 +155,7 @@ pub(crate) fn run(req: Request<'_>) -> IdeResult { .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"); diff --git a/crates/ide-search/src/files.rs b/crates/ide-search/src/files.rs index ba85acfc..bada8830 100644 --- a/crates/ide-search/src/files.rs +++ b/crates/ide-search/src/files.rs @@ -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"); diff --git a/crates/ide-search/tests/search.rs b/crates/ide-search/tests/search.rs index 412fe9b1..bd0f7d09 100644 --- a/crates/ide-search/tests/search.rs +++ b/crates/ide-search/tests/search.rs @@ -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"]); +} diff --git a/crates/ide-vfs/src/lib.rs b/crates/ide-vfs/src/lib.rs index 6ac24e21..c8059984 100644 --- a/crates/ide-vfs/src/lib.rs +++ b/crates/ide-vfs/src/lib.rs @@ -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;