mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
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:
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user