mirror of
https://github.com/watchexec/watchexec.git
synced 2024-11-16 09:08:37 +01:00
3ad0e1aa57
Previously, when importing multiple nested ignore files, some info from the parent—notably the "root" path—would be inherited. This lead to some problems with matching of "pseudo-absolute" rules (those with a leading slash) in nested ignore files (see #745 for more details). To fix this, we now fully isolate each path of the tree during the import process. This leads to more accurate, though unfortunately slightly less performant, rule matching. The only time a builder is reused now is if two input files have the same `applies_in` value, in which case they are merged together. I have added tests to ensure correctness and prevent a regression. I also was careful to make sure no previous tests broke in any way (all changes to existing tests were made in isolation, and thus are not affected by the logic changes). As far as I can tell, the only behavior change is that now some previously-ignored rules will now be applied, which could, in very rare configurations, lead to files being unintentionally ignored. However, due to the aforementioned logic bug, those files were all ignored by git already, so I suspect the number of people actually caught off guard by this change to be extremely low, likely zero. Fixes #745.
159 lines
3.5 KiB
Rust
159 lines
3.5 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use ignore::{gitignore::Glob, Match};
|
|
use ignore_files::{IgnoreFile, IgnoreFilter};
|
|
|
|
pub mod ignore_tests {
|
|
pub use super::ig_file as file;
|
|
pub use super::ignore_filt as filt;
|
|
pub use super::Applies;
|
|
pub use super::PathHarness;
|
|
}
|
|
|
|
/// Get the drive letter of the current working directory.
|
|
#[cfg(windows)]
|
|
fn drive_root() -> String {
|
|
let path = std::fs::canonicalize(".").unwrap();
|
|
|
|
let Some(prefix) = path.components().next() else {
|
|
return r"C:\".into();
|
|
};
|
|
|
|
match prefix {
|
|
std::path::Component::Prefix(prefix_component) => prefix_component
|
|
.as_os_str()
|
|
.to_str()
|
|
.map(|p| p.to_owned() + r"\")
|
|
.unwrap_or(r"C:\".into()),
|
|
_ => r"C:\".into(),
|
|
}
|
|
}
|
|
|
|
fn normalize_path(path: &str) -> PathBuf {
|
|
#[cfg(windows)]
|
|
let path: &str = &String::from(path)
|
|
.strip_prefix("/")
|
|
.map_or(path.into(), |p| drive_root() + p);
|
|
|
|
let path: PathBuf = if Path::new(path).has_root() {
|
|
path.into()
|
|
} else {
|
|
std::fs::canonicalize(".").unwrap().join("tests").join(path)
|
|
};
|
|
|
|
dunce::simplified(&path).into()
|
|
}
|
|
|
|
pub trait PathHarness {
|
|
fn check_path(&self, path: &Path, is_dir: bool) -> Match<&Glob>;
|
|
|
|
fn path_pass(&self, path: &str, is_dir: bool, pass: bool) {
|
|
let full_path = &normalize_path(path);
|
|
|
|
tracing::info!(?path, ?is_dir, ?pass, "check");
|
|
|
|
let result = self.check_path(full_path, is_dir);
|
|
|
|
assert_eq!(
|
|
match result {
|
|
Match::None => true,
|
|
Match::Ignore(glob) => !glob.from().map_or(true, |f| full_path.starts_with(f)),
|
|
Match::Whitelist(_glob) => true,
|
|
},
|
|
pass,
|
|
"{} {:?} (expected {}) [result: {}]",
|
|
if is_dir { "dir" } else { "file" },
|
|
full_path,
|
|
if pass { "pass" } else { "fail" },
|
|
match result {
|
|
Match::None => String::from("None"),
|
|
Match::Ignore(glob) => format!(
|
|
"Ignore({})",
|
|
glob.from()
|
|
.map_or(String::from(""), |f| f.display().to_string())
|
|
),
|
|
Match::Whitelist(glob) => format!(
|
|
"Whitelist({})",
|
|
glob.from()
|
|
.map_or(String::from(""), |f| f.display().to_string())
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
fn file_does_pass(&self, path: &str) {
|
|
self.path_pass(path, false, true);
|
|
}
|
|
|
|
fn file_doesnt_pass(&self, path: &str) {
|
|
self.path_pass(path, false, false);
|
|
}
|
|
|
|
fn dir_does_pass(&self, path: &str) {
|
|
self.path_pass(path, true, true);
|
|
}
|
|
|
|
fn dir_doesnt_pass(&self, path: &str) {
|
|
self.path_pass(path, true, false);
|
|
}
|
|
|
|
fn agnostic_pass(&self, path: &str) {
|
|
self.file_does_pass(path);
|
|
self.dir_does_pass(path);
|
|
}
|
|
|
|
fn agnostic_fail(&self, path: &str) {
|
|
self.file_doesnt_pass(path);
|
|
self.dir_doesnt_pass(path);
|
|
}
|
|
}
|
|
|
|
impl PathHarness for IgnoreFilter {
|
|
fn check_path(&self, path: &Path, is_dir: bool) -> Match<&Glob> {
|
|
self.match_path(&path, is_dir)
|
|
}
|
|
}
|
|
|
|
fn tracing_init() {
|
|
use tracing_subscriber::{
|
|
fmt::{format::FmtSpan, Subscriber},
|
|
util::SubscriberInitExt,
|
|
EnvFilter,
|
|
};
|
|
Subscriber::builder()
|
|
.pretty()
|
|
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
|
.with_env_filter(EnvFilter::from_default_env())
|
|
.finish()
|
|
.try_init()
|
|
.ok();
|
|
}
|
|
|
|
pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> IgnoreFilter {
|
|
tracing_init();
|
|
let origin = normalize_path(origin);
|
|
IgnoreFilter::new(origin, ignore_files)
|
|
.await
|
|
.expect("making filterer")
|
|
}
|
|
|
|
pub fn ig_file(name: &str) -> IgnoreFile {
|
|
let path = normalize_path(name);
|
|
let parent: PathBuf = path.parent().unwrap_or(&path).into();
|
|
IgnoreFile {
|
|
path,
|
|
applies_in: Some(parent),
|
|
applies_to: None,
|
|
}
|
|
}
|
|
|
|
pub trait Applies {
|
|
fn applies_globally(self) -> Self;
|
|
}
|
|
|
|
impl Applies for IgnoreFile {
|
|
fn applies_globally(mut self) -> Self {
|
|
self.applies_in = None;
|
|
self
|
|
}
|
|
}
|