feat(pi-walker): ignored parent rules that cover explicitly rooted walks
- Updated gitignore loading to filter out ancestor ignore rules that match the current explicit walk root. - Retained unrelated parent ignore rules and processed local ignore files within the root normally. - Added a regression test to ensure ignored roots still respect nested ignore rules.
This commit is contained in:
+101
-11
@@ -20,7 +20,7 @@ use std::{
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
io,
|
||||
io::{self, BufRead},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
@@ -3033,13 +3033,65 @@ fn has_repo_marker(dir: &Path) -> bool {
|
||||
dir.join(".git").exists() || dir.join(".jj").exists()
|
||||
}
|
||||
|
||||
fn load_gitignore(root: &Path, file: &Path) -> Option<ignore::gitignore::Gitignore> {
|
||||
fn ignore_line_covers_root(
|
||||
matcher_root: &Path,
|
||||
source: &Path,
|
||||
line: &str,
|
||||
explicit_root: &Path,
|
||||
) -> bool {
|
||||
let mut builder = ignore::gitignore::GitignoreBuilder::new(matcher_root);
|
||||
builder.add_line(Some(source.to_path_buf()), line).is_ok()
|
||||
&& builder.build().is_ok_and(|matcher| {
|
||||
matcher
|
||||
.matched_path_or_any_parents(explicit_root, true)
|
||||
.is_ignore()
|
||||
})
|
||||
}
|
||||
|
||||
/// Load an ignore source, removing ancestor rules that cover an explicit walk
|
||||
/// root.
|
||||
///
|
||||
/// Unrelated parent rules remain active, while ignore files discovered at or
|
||||
/// below the root are loaded without filtering.
|
||||
fn load_gitignore(
|
||||
matcher_root: &Path,
|
||||
file: &Path,
|
||||
explicit_root: Option<&Path>,
|
||||
) -> Option<ignore::gitignore::Gitignore> {
|
||||
if !file.is_file() {
|
||||
return None;
|
||||
}
|
||||
let mut builder = ignore::gitignore::GitignoreBuilder::new(root);
|
||||
let mut builder = ignore::gitignore::GitignoreBuilder::new(matcher_root);
|
||||
let _ = builder.add(file);
|
||||
builder.build().ok().filter(|matcher| !matcher.is_empty())
|
||||
let matcher = builder.build().ok().filter(|matcher| !matcher.is_empty())?;
|
||||
let Some(explicit_root) = explicit_root else {
|
||||
return Some(matcher);
|
||||
};
|
||||
if !matcher
|
||||
.matched_path_or_any_parents(explicit_root, true)
|
||||
.is_ignore()
|
||||
{
|
||||
return Some(matcher);
|
||||
}
|
||||
|
||||
let handle = std::fs::File::open(file).ok()?;
|
||||
let mut filtered = ignore::gitignore::GitignoreBuilder::new(matcher_root);
|
||||
let source = Some(file.to_path_buf());
|
||||
for (index, line) in io::BufReader::new(handle).lines().enumerate() {
|
||||
let Ok(line) = line else {
|
||||
break;
|
||||
};
|
||||
let line = if index == 0 {
|
||||
line.trim_start_matches('\u{feff}')
|
||||
} else {
|
||||
line.as_str()
|
||||
};
|
||||
if ignore_line_covers_root(matcher_root, file, line, explicit_root) {
|
||||
continue;
|
||||
}
|
||||
let _ = filtered.add_line(source.clone(), line);
|
||||
}
|
||||
filtered.build().ok().filter(|matcher| !matcher.is_empty())
|
||||
}
|
||||
|
||||
impl IgnoreState {
|
||||
@@ -3048,10 +3100,26 @@ impl IgnoreState {
|
||||
let git_exclude = dir.join(".git/info/exclude");
|
||||
Self::new(
|
||||
parent,
|
||||
load_gitignore(dir, &dir.join(".ignore")),
|
||||
load_gitignore(dir, &dir.join(".gitignore")),
|
||||
load_gitignore(dir, &dir.join(".ignore"), None),
|
||||
load_gitignore(dir, &dir.join(".gitignore"), None),
|
||||
if has_git {
|
||||
load_gitignore(dir, &git_exclude)
|
||||
load_gitignore(dir, &git_exclude, None)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
has_git,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_parent(dir: &Path, parent: Option<Arc<Self>>, explicit_root: &Path) -> Arc<Self> {
|
||||
let has_git = has_repo_marker(dir);
|
||||
let git_exclude = dir.join(".git/info/exclude");
|
||||
Self::new(
|
||||
parent,
|
||||
load_gitignore(dir, &dir.join(".ignore"), Some(explicit_root)),
|
||||
load_gitignore(dir, &dir.join(".gitignore"), Some(explicit_root)),
|
||||
if has_git {
|
||||
load_gitignore(dir, &git_exclude, Some(explicit_root))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -3067,17 +3135,17 @@ impl IgnoreState {
|
||||
Self::new(
|
||||
Some(Arc::clone(parent)),
|
||||
if names.ignore_file {
|
||||
load_gitignore(dir, &dir.join(".ignore"))
|
||||
load_gitignore(dir, &dir.join(".ignore"), None)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if names.gitignore_file {
|
||||
load_gitignore(dir, &dir.join(".gitignore"))
|
||||
load_gitignore(dir, &dir.join(".gitignore"), None)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if names.git_dir {
|
||||
load_gitignore(dir, &git_exclude)
|
||||
load_gitignore(dir, &git_exclude, None)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -3127,7 +3195,7 @@ impl IgnoreState {
|
||||
let repo_start = repo_start?;
|
||||
let mut parent = None;
|
||||
for ancestor in ancestors[..=repo_start].iter().rev() {
|
||||
parent = Some(Self::build(ancestor, parent));
|
||||
parent = Some(Self::build_parent(ancestor, parent, root));
|
||||
}
|
||||
parent
|
||||
}
|
||||
@@ -4295,6 +4363,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_ignored_root_keeps_unrelated_parent_and_nested_ignore_rules() {
|
||||
let tree = temp_tree("explicit-ignored-root");
|
||||
fs::create_dir_all(tree.path().join(".git")).expect("repo marker should be created");
|
||||
fs::write(tree.path().join(".gitignore"), "*.log\nignored/**\n")
|
||||
.expect("repo gitignore should be written");
|
||||
let project = tree.path().join("ignored").join("package");
|
||||
let nested = project.join("nested");
|
||||
fs::create_dir_all(&nested).expect("ignored project tree should be created");
|
||||
fs::write(project.join("keep.ts"), "keep").expect("kept file should be written");
|
||||
fs::write(project.join("trace.log"), "trace").expect("parent-ignored file should be written");
|
||||
fs::write(nested.join(".gitignore"), "generated.ts\n")
|
||||
.expect("nested gitignore should be written");
|
||||
fs::write(nested.join("generated.ts"), "generated")
|
||||
.expect("nested ignored file should be written");
|
||||
fs::write(nested.join("keep.ts"), "nested keep").expect("nested kept file should be written");
|
||||
|
||||
let paths = collect_file_paths(&project, true);
|
||||
|
||||
assert_eq!(paths, vec!["keep.ts", "nested/.gitignore", "nested/keep.ts"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_request_files_only_returns_relative_files_and_excludes_directories() {
|
||||
let tree = temp_tree("request-files-only");
|
||||
|
||||
Reference in New Issue
Block a user