#![warn(missing_docs)]
#![forbid(unsafe_code)]
pub mod abspath;
pub mod error;
pub mod ignore_rules;
pub mod notify;
pub mod sync;
pub use abspath::AbsolutePath;
use crossbeam::queue::SegQueue;
pub use error::{Error, Result};
pub use ignore_rules::IgnoreRules;
pub use notify::make_watcher;
pub use std::hash::Hash;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;
pub use sync::{PathSync, PathSyncSingleton};
use xvc_logging::debug;
use xvc_logging::warn;
use xvc_logging::XvcOutputSender;
pub use notify::PathEvent;
pub use notify::RecommendedWatcher;
use xvc_logging::watch;
use crossbeam_channel::Sender;
pub use globset::{self, Glob, GlobSet, GlobSetBuilder};
use std::{
ffi::OsString,
fmt::Debug,
fs::{self, Metadata},
path::{Path, PathBuf},
};
use anyhow::{anyhow, Context};
static MAX_THREADS_PARALLEL_WALK: usize = 8;
#[derive(Debug, Clone)]
pub struct PathMetadata {
pub path: PathBuf,
pub metadata: Metadata,
}
#[derive(Debug, Clone)]
pub enum MatchResult {
NoMatch,
Ignore,
Whitelist,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum PatternRelativity {
Anywhere,
RelativeTo {
directory: String,
},
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum PathKind {
Any,
Directory,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum PatternEffect {
Ignore,
Whitelist,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Source {
File {
path: PathBuf,
line: usize,
},
Global,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Pattern<T>
where
T: PartialEq + Hash,
{
pub pattern: T,
original: String,
source: Source,
effect: PatternEffect,
relativity: PatternRelativity,
path_kind: PathKind,
}
impl<T: PartialEq + Hash> Pattern<T> {
pub fn map<U, F>(self, f: F) -> Pattern<U>
where
U: PartialEq + Hash,
F: FnOnce(T) -> U,
{
Pattern::<U> {
pattern: f(self.pattern),
original: self.original,
source: self.source,
effect: self.effect,
relativity: self.relativity,
path_kind: self.path_kind,
}
}
}
impl<T: PartialEq + Hash> Pattern<Result<T>> {
fn transpose(self) -> Result<Pattern<T>> {
match self.pattern {
Ok(p) => Ok(Pattern::<T> {
pattern: p,
original: self.original,
source: self.source,
effect: self.effect,
relativity: self.relativity,
path_kind: self.path_kind,
}),
Err(e) => Err(e),
}
}
}
type GlobPattern = Pattern<Glob>;
#[derive(Debug, Clone)]
pub struct WalkOptions {
pub ignore_filename: Option<String>,
pub include_dirs: bool,
}
impl WalkOptions {
pub fn gitignore() -> Self {
Self {
ignore_filename: Some(".gitignore".to_owned()),
include_dirs: true,
}
}
pub fn xvcignore() -> Self {
Self {
ignore_filename: Some(".xvcignore".to_owned()),
include_dirs: true,
}
}
pub fn without_dirs(self) -> Self {
Self {
ignore_filename: self.ignore_filename,
include_dirs: false,
}
}
pub fn with_dirs(self) -> Self {
Self {
ignore_filename: self.ignore_filename,
include_dirs: true,
}
}
}
fn walk_parallel_inner(
ignore_rules: Arc<RwLock<IgnoreRules>>,
dir: &Path,
walk_options: WalkOptions,
path_sender: Sender<Result<PathMetadata>>,
ignore_sender: Sender<Result<Arc<RwLock<IgnoreRules>>>>,
) -> Result<Vec<PathMetadata>> {
let child_paths: Vec<PathMetadata> = directory_list(dir)?
.into_iter()
.filter_map(|pm_res| match pm_res {
Ok(pm) => Some(pm),
Err(e) => {
path_sender
.send(Err(e))
.expect("Channel error in walk_parallel");
None
}
})
.collect();
let dir_with_ignores = if let Some(ignore_filename) = walk_options.ignore_filename.clone() {
let ignore_filename = OsString::from(ignore_filename);
if let Some(ignore_path_metadata) = child_paths
.iter()
.find(|pm| pm.path.file_name() == Some(&ignore_filename))
{
let ignore_path = dir.join(&ignore_path_metadata.path);
let new_patterns = clear_glob_errors(
&path_sender,
patterns_from_file(&ignore_rules.read()?.root, &ignore_path)?,
);
watch!(new_patterns);
ignore_rules.write()?.update(new_patterns)?;
watch!(ignore_rules);
ignore_sender.send(Ok(ignore_rules.clone()))?;
ignore_rules
} else {
ignore_rules
}
} else {
ignore_rules
};
let mut child_dirs = Vec::<PathMetadata>::new();
watch!(child_paths);
for child_path in child_paths {
match check_ignore(&(*dir_with_ignores.read()?), child_path.path.as_ref()) {
MatchResult::NoMatch | MatchResult::Whitelist => {
watch!(child_path.path);
if child_path.metadata.is_dir() {
if walk_options.include_dirs {
path_sender.send(Ok(child_path.clone()))?;
}
child_dirs.push(child_path);
} else {
path_sender.send(Ok(child_path.clone()))?;
}
}
MatchResult::Ignore => {
watch!(child_path.path);
}
}
}
Ok(child_dirs)
}
pub fn walk_parallel(
ignore_rules: IgnoreRules,
dir: &Path,
walk_options: WalkOptions,
path_sender: Sender<Result<PathMetadata>>,
ignore_sender: Sender<Result<Arc<RwLock<IgnoreRules>>>>,
) -> Result<()> {
let dir_queue = Arc::new(SegQueue::<PathMetadata>::new());
let ignore_rules = Arc::new(RwLock::new(ignore_rules.clone()));
let child_dirs = walk_parallel_inner(
ignore_rules.clone(),
dir,
walk_options.clone(),
path_sender.clone(),
ignore_sender.clone(),
)?;
child_dirs.into_iter().for_each(|pm| {
dir_queue.push(pm);
});
if dir_queue.is_empty() {
return Ok(());
}
crossbeam::scope(|s| {
for thread_i in 0..MAX_THREADS_PARALLEL_WALK {
let path_sender = path_sender.clone();
let ignore_sender = ignore_sender.clone();
let walk_options = walk_options.clone();
let ignore_rules = ignore_rules.clone();
let dir_queue = dir_queue.clone();
s.spawn(move |_| {
watch!(path_sender);
watch!(ignore_sender);
while let Some(pm) = dir_queue.pop() {
let child_dirs = walk_parallel_inner(
ignore_rules.clone(),
&pm.path,
walk_options.clone(),
path_sender.clone(),
ignore_sender.clone(),
)
.unwrap_or_else(|e| {
path_sender
.send(Err(e))
.expect("Channel error in walk_parallel");
Vec::<PathMetadata>::new()
});
for child_dir in child_dirs {
dir_queue.push(child_dir);
}
}
watch!("End of thread {}", thread_i);
});
}
})
.expect("Error in crossbeam scope in walk_parallel");
watch!("End of walk_parallel");
Ok(())
}
pub fn walk_serial(
output_snd: &XvcOutputSender,
ignore_rules: IgnoreRules,
dir: &Path,
walk_options: &WalkOptions,
) -> Result<(Vec<PathMetadata>, IgnoreRules)> {
let ignore_filename = walk_options.ignore_filename.clone().map(OsString::from);
let ignore_rules = Arc::new(Mutex::new(ignore_rules.clone()));
let dir_stack = crossbeam::queue::SegQueue::new();
let res_paths = Arc::new(Mutex::new(Vec::<PathMetadata>::new()));
dir_stack.push(dir.to_path_buf());
let get_child_paths = |dir: &Path| -> Result<Vec<PathMetadata>> {
Ok(directory_list(dir)?
.into_iter()
.filter_map(|pm_res| match pm_res {
Ok(pm) => Some(pm),
Err(e) => {
warn!(output_snd, "{}", e);
None
}
})
.collect())
};
let update_ignore_rules = |child_paths: &Vec<PathMetadata>| -> Result<()> {
if let Some(ref ignore_filename) = &ignore_filename {
watch!(ignore_filename);
if let Some(ignore_path_metadata) = child_paths
.iter()
.find(|pm| pm.path.file_name() == Some(ignore_filename))
{
let ignore_path = dir.join(&ignore_path_metadata.path);
let new_patterns: Vec<GlobPattern> =
patterns_from_file(&ignore_rules.lock()?.root, &ignore_path)?
.into_iter()
.filter_map(|res_p| match res_p.pattern {
Ok(_) => Some(res_p.map(|p| p.unwrap())),
Err(e) => {
warn!(output_snd, "{}", e);
None
}
})
.collect();
ignore_rules.lock()?.update(new_patterns)?;
}
}
Ok(())
};
let filter_child_paths = |child_paths: &Vec<PathMetadata>| -> Result<()> {
for child_path in child_paths {
watch!(child_path.path);
let ignore_res = check_ignore(&(*ignore_rules.lock()?), child_path.path.as_ref());
watch!(ignore_res);
match ignore_res {
MatchResult::NoMatch | MatchResult::Whitelist => {
watch!(child_path);
if child_path.metadata.is_dir() {
watch!("here");
if walk_options.include_dirs {
watch!("here2");
res_paths.lock()?.push(child_path.clone());
}
watch!("here3");
dir_stack.push(child_path.path.clone());
watch!("here4");
} else {
watch!("here5");
res_paths.lock()?.push(child_path.clone());
watch!("here6");
}
}
MatchResult::Ignore => {
debug!(output_snd, "Ignored: {:?}", child_path.path);
}
}
watch!(child_path);
}
Ok(())
};
while let Some(dir) = { dir_stack.pop().clone() } {
watch!(dir);
let dir = dir.clone();
watch!(dir);
let child_paths = get_child_paths(&dir)?;
watch!(child_paths);
update_ignore_rules(&child_paths)?;
filter_child_paths(&child_paths)?;
}
let res_paths: Vec<PathMetadata> = res_paths.lock()?.clone();
let ignore_rules = ignore_rules.lock()?.clone();
Ok((res_paths, ignore_rules))
}
pub fn build_ignore_rules(
given: IgnoreRules,
dir: &Path,
ignore_filename: &str,
) -> Result<IgnoreRules> {
let elements = dir
.read_dir()
.map_err(|e| anyhow!("Error reading directory: {:?}, {:?}", dir, e))?;
let mut child_dirs = Vec::<PathBuf>::new();
let ignore_fn = OsString::from(ignore_filename);
xvc_logging::watch!(ignore_fn);
let ignore_root = given.root.clone();
xvc_logging::watch!(ignore_root);
let mut ignore_rules = given;
let mut new_patterns: Option<Vec<GlobPattern>> = None;
for entry in elements {
match entry {
Ok(entry) => {
if entry.path().is_dir() {
xvc_logging::watch!(entry.path());
child_dirs.push(entry.path());
}
if entry.file_name() == ignore_fn && entry.path().exists() {
let ignore_path = entry.path();
watch!(ignore_path);
new_patterns = Some(
patterns_from_file(&ignore_root, &ignore_path)?
.into_iter()
.filter_map(|p| match p.transpose() {
Ok(p) => Some(p),
Err(e) => {
warn!("{:?}", e);
None
}
})
.collect(),
);
}
}
Err(e) => {
warn!("{}", e);
}
}
}
if let Some(new_patterns) = new_patterns {
ignore_rules.update(new_patterns)?;
}
for child_dir in child_dirs {
match check_ignore(&ignore_rules, &child_dir) {
MatchResult::NoMatch | MatchResult::Whitelist => {
ignore_rules = build_ignore_rules(ignore_rules, &child_dir, ignore_filename)?;
}
MatchResult::Ignore => {}
}
}
Ok(ignore_rules)
}
fn clear_glob_errors(
sender: &Sender<Result<PathMetadata>>,
new_patterns: Vec<Pattern<Result<Glob>>>,
) -> Vec<Pattern<Glob>> {
let new_glob_patterns: Vec<Pattern<Glob>> = new_patterns
.into_iter()
.filter_map(|p| match p.transpose() {
Ok(p) => Some(p),
Err(e) => {
sender
.send(Err(Error::from(anyhow!("Error in glob pattern: {:?}", e))))
.expect("Error in channel");
None
}
})
.collect();
new_glob_patterns
}
fn transform_pattern_for_glob(pattern: Pattern<String>) -> Pattern<String> {
let anything_anywhere = |p| format!("**/{p}");
let anything_relative = |p, directory| format!("{directory}/**/{p}");
let directory_anywhere = |p| format!("**{p}/**");
let directory_relative = |p, directory| format!("{directory}/**/{p}/**");
let transformed_pattern = match (&pattern.path_kind, &pattern.relativity) {
(PathKind::Any, PatternRelativity::Anywhere) => anything_anywhere(pattern.pattern),
(PathKind::Any, PatternRelativity::RelativeTo { directory }) => {
anything_relative(pattern.pattern, directory)
}
(PathKind::Directory, PatternRelativity::Anywhere) => directory_anywhere(pattern.pattern),
(PathKind::Directory, PatternRelativity::RelativeTo { directory }) => {
directory_relative(pattern.pattern, directory)
}
};
Pattern {
pattern: transformed_pattern,
..pattern
}
}
fn build_globset(patterns: Vec<Glob>) -> Result<GlobSet> {
let mut gs_builder = GlobSetBuilder::new();
for p in patterns {
gs_builder.add(p.clone());
}
gs_builder
.build()
.map_err(|e| anyhow!("Error building glob set: {:?}", e).into())
}
fn patterns_from_file(
ignore_root: &Path,
ignore_path: &Path,
) -> Result<Vec<Pattern<Result<Glob>>>> {
watch!(ignore_root);
watch!(ignore_path);
let content = fs::read_to_string(ignore_path).with_context(|| {
format!(
"Cannot read file: {:?}\n
If the file is present, it may be an encoding issue. Please check if it's UTF-8 encoded.",
ignore_path
)
})?;
watch!(&content);
Ok(content_to_patterns(
ignore_root,
Some(ignore_path),
&content,
))
}
pub fn content_to_patterns(
ignore_root: &Path,
source: Option<&Path>,
content: &str,
) -> Vec<Pattern<Result<Glob>>> {
let patterns: Vec<Pattern<Result<Glob>>> = content
.lines()
.enumerate()
.filter(|(_, line)| !(line.trim().is_empty() || line.starts_with('#')))
.map(|(i, line)| {
if !line.ends_with("\\ ") {
(i, line.trim_end())
} else {
(i, line)
}
})
.map(|(i, line)| {
(
line,
match source {
Some(p) => Source::File {
path: p
.strip_prefix(ignore_root)
.expect("path must be within ignore_root")
.to_path_buf(),
line: (i + 1),
},
None => Source::Global,
},
)
})
.map(|(line, source)| build_pattern(source, line))
.map(transform_pattern_for_glob)
.map(|pc| pc.map(|s| Glob::new(&s).map_err(Error::from)))
.collect();
patterns
}
fn build_pattern(source: Source, original: &str) -> Pattern<String> {
let current_dir = match &source {
Source::Global => "".to_string(),
Source::File { path, .. } => {
let path = path
.parent()
.expect("Pattern source file doesn't have parent")
.to_string_lossy()
.to_string();
if path.starts_with('/') {
path
} else {
format!("/{path}")
}
}
};
let begin_exclamation = original.starts_with('!');
let mut line = if begin_exclamation || original.starts_with(r"\!") {
original[1..].to_owned()
} else {
original.to_owned()
};
if !line.ends_with("\\ ") {
line = line.trim_end().to_string();
}
let end_slash = line.ends_with('/');
if end_slash {
line = line[..line.len() - 1].to_string()
}
let begin_slash = line.starts_with('/');
let non_final_slash = if !line.is_empty() {
line[..line.len() - 1].chars().any(|c| c == '/')
} else {
false
};
if begin_slash {
line = line[1..].to_string();
}
let current_dir = if current_dir.ends_with('/') {
¤t_dir[..current_dir.len() - 1]
} else {
¤t_dir
};
let effect = if begin_exclamation {
PatternEffect::Whitelist
} else {
PatternEffect::Ignore
};
let path_kind = if end_slash {
PathKind::Directory
} else {
PathKind::Any
};
let relativity = if non_final_slash {
PatternRelativity::RelativeTo {
directory: current_dir.to_owned(),
}
} else {
PatternRelativity::Anywhere
};
Pattern::<String> {
pattern: line,
original: original.to_owned(),
source,
effect,
relativity,
path_kind,
}
}
pub fn check_ignore(ignore_rules: &IgnoreRules, path: &Path) -> MatchResult {
let is_abs = path.is_absolute();
watch!(is_abs);
let path_str = path.to_string_lossy();
watch!(path_str);
let final_slash = path_str.ends_with('/');
watch!(final_slash);
let path = if is_abs {
if final_slash {
format!(
"/{}/",
path.strip_prefix(&ignore_rules.root)
.expect("path must be within root")
.to_string_lossy()
)
} else {
format!(
"/{}",
path.strip_prefix(&ignore_rules.root)
.expect("path must be within root")
.to_string_lossy()
)
}
} else {
path_str.to_string()
};
watch!(path);
if ignore_rules.whitelist_set.read().unwrap().is_match(&path) {
MatchResult::Whitelist
} else if ignore_rules.ignore_set.read().unwrap().is_match(&path) {
MatchResult::Ignore
} else {
MatchResult::NoMatch
}
}
pub fn directory_list(dir: &Path) -> Result<Vec<Result<PathMetadata>>> {
let elements = dir
.read_dir()
.map_err(|e| anyhow!("Error reading directory: {:?}, {:?}", dir, e))?;
let mut child_paths = Vec::<Result<PathMetadata>>::new();
for entry in elements {
match entry {
Err(err) => child_paths.push(Err(Error::from(anyhow!(
"Error reading entry in dir {:?} {:?}",
dir,
err
)))),
Ok(entry) => match entry.metadata() {
Err(err) => child_paths.push(Err(Error::from(anyhow!(
"Error getting metadata {:?} {:?}",
entry,
err
)))),
Ok(md) => {
child_paths.push(Ok(PathMetadata {
path: entry.path(),
metadata: md.clone(),
}));
}
},
}
}
Ok(child_paths)
}
#[cfg(test)]
mod tests {
use super::*;
use log::LevelFilter;
use test_case::test_case;
use crate::error::Result;
use crate::AbsolutePath;
use xvc_test_helper::*;
#[test_case("!mydir/*/file" => matches PatternEffect::Whitelist ; "t1159938339")]
#[test_case("!mydir/myfile" => matches PatternEffect::Whitelist ; "t1302522194")]
#[test_case("!myfile" => matches PatternEffect::Whitelist ; "t3599739725")]
#[test_case("!myfile/" => matches PatternEffect::Whitelist ; "t389990097")]
#[test_case("/my/file" => matches PatternEffect::Ignore ; "t3310011546")]
#[test_case("mydir/*" => matches PatternEffect::Ignore ; "t1461510927")]
#[test_case("mydir/file" => matches PatternEffect::Ignore; "t4096563949")]
#[test_case("myfile" => matches PatternEffect::Ignore; "t4042406621")]
#[test_case("myfile*" => matches PatternEffect::Ignore ; "t3367706249")]
#[test_case("myfile/" => matches PatternEffect::Ignore ; "t1204466627")]
fn test_pattern_effect(line: &str) -> PatternEffect {
let pat = build_pattern(Source::Global, line);
pat.effect
}
#[test_case("", "!mydir/*/file" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t500415168")]
#[test_case("", "!mydir/myfile" => matches PatternRelativity::RelativeTo {directory} if directory.is_empty() ; "t1158125354")]
#[test_case("dir/", "!mydir/*/file" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t3052699971")]
#[test_case("dir/", "!mydir/myfile" => matches PatternRelativity::RelativeTo {directory} if directory == "/dir" ; "t885029019")]
#[test_case("", "!myfile" => matches PatternRelativity::Anywhere; "t3101661374")]
#[test_case("", "!myfile/" => matches PatternRelativity::Anywhere ; "t3954695505")]
#[test_case("", "/my/file" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t1154256567")]
#[test_case("", "mydir/*" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t865348822")]
#[test_case("", "mydir/file" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t809589695")]
#[test_case("root/", "/my/file" => matches PatternRelativity::RelativeTo { directory } if directory == "/root" ; "t7154256567")]
#[test_case("root/", "mydir/*" => matches PatternRelativity::RelativeTo { directory } if directory == "/root" ; "t765348822")]
#[test_case("root/", "mydir/file" => matches PatternRelativity::RelativeTo { directory } if directory == "/root" ; "t709589695")]
#[test_case("", "myfile" => matches PatternRelativity::Anywhere; "t949952742")]
#[test_case("", "myfile*" => matches PatternRelativity::Anywhere ; "t2212007572")]
#[test_case("", "myfile/" => matches PatternRelativity::Anywhere; "t900104620")]
fn test_pattern_relativity(dir: &str, line: &str) -> PatternRelativity {
let source = Source::File {
path: PathBuf::from(dir).join(".gitignore"),
line: 1,
};
let pat = build_pattern(source, line);
pat.relativity
}
#[test_case("", "!mydir/*/file" => matches PathKind::Any ; "t4069397926")]
#[test_case("", "!mydir/myfile" => matches PathKind::Any ; "t206435934")]
#[test_case("", "!myfile" => matches PathKind::Any ; "t4262638148")]
#[test_case("", "!myfile/" => matches PathKind::Directory ; "t214237847")]
#[test_case("", "/my/file" => matches PathKind::Any ; "t187692643")]
#[test_case("", "mydir/*" => matches PathKind::Any ; "t1159784957")]
#[test_case("", "mydir/file" => matches PathKind::Any ; "t2011171465")]
#[test_case("", "myfile" => matches PathKind::Any ; "t167946945")]
#[test_case("", "myfile*" => matches PathKind::Any ; "t3091563211")]
#[test_case("", "myfile/" => matches PathKind::Directory ; "t1443554623")]
fn test_path_kind(dir: &str, line: &str) -> PathKind {
let source = Source::File {
path: PathBuf::from(dir).join(".gitignore"),
line: 1,
};
let pat = build_pattern(source, line);
pat.path_kind
}
#[test_case("" => 0)]
#[test_case("myfile" => 1)]
#[test_case("mydir/myfile" => 1)]
#[test_case("mydir/myfile\n!myfile" => 2)]
#[test_case("mydir/myfile\n/another" => 2)]
#[test_case("mydir/myfile\n\n\nanother" => 2)]
#[test_case("#comment\nmydir/myfile\n\n\nanother" => 2)]
#[test_case("#mydir/myfile" => 0)]
fn test_content_to_patterns_count(contents: &str) -> usize {
let patterns = content_to_patterns(Path::new(""), None, contents);
patterns.len()
}
fn create_patterns(root: &str, dir: Option<&str>, patterns: &str) -> Vec<Pattern<Glob>> {
content_to_patterns(Path::new(root), dir.map(Path::new), patterns)
.into_iter()
.map(|pat_res_g| pat_res_g.map(|res_g| res_g.unwrap()))
.collect()
}
fn new_dir_with_ignores(
root: &str,
dir: Option<&str>,
initial_patterns: &str,
) -> Result<IgnoreRules> {
let patterns = create_patterns(root, dir, initial_patterns);
let mut initialized = IgnoreRules::empty(&PathBuf::from(root));
initialized.update(patterns)?;
Ok(initialized)
}
#[test_case(".", "" ; "empty_dwi")]
#[test_case("dir", "myfile")]
#[test_case("dir", "mydir/myfile")]
#[test_case("dir", "mydir/myfile\n!myfile")]
#[test_case("dir", "mydir/myfile\n/another")]
#[test_case("dir", "mydir/myfile\n\n\nanother")]
#[test_case("dir", "#comment\nmydir/myfile\n\n\nanother")]
#[test_case("dir", "#mydir/myfile" ; "single ignored lined")]
fn test_dir_with_ignores(dir: &str, contents: &str) {
new_dir_with_ignores(dir, None, contents).unwrap();
}
#[test_case("/dir", "/mydir/myfile/" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t868594159")]
#[test_case("/dir", "mydir" => matches PatternRelativity::Anywhere ; "t4030766779")]
#[test_case("/dir/", "mydir/myfile" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t2043231107")]
#[test_case("dir", "myfile" => matches PatternRelativity::Anywhere; "t871610344" )]
#[test_case("dir/", "mydir/myfile" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t21398102")]
#[test_case("dir/", "myfile" => matches PatternRelativity::Anywhere ; "t1846637197")]
#[test_case("dir//", "/mydir/myfile" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t2556287848")]
fn test_path_relativity(dir: &str, pattern: &str) -> PatternRelativity {
let source = Source::File {
path: PathBuf::from(format!("{dir}/.gitignore")),
line: 1,
};
let pattern = build_pattern(source, pattern);
pattern.relativity
}
#[test_case("", "myfile" => "myfile" ; "t1142345310")]
#[test_case("", "/myfile" => "myfile" ; "t1427001291")]
#[test_case("", "myfile/" => "myfile" ; "t789151905")]
#[test_case("", "mydir/myfile" => "mydir/myfile" ; "t21199018162")]
#[test_case("", "myfile.*" => "myfile.*" ; "t31199018162")]
#[test_case("", "mydir/**.*" => "mydir/**.*" ; "t41199018162")]
#[test_case("dir", "myfile" => "myfile" ; "t1242345310")]
#[test_case("dir", "/myfile" => "myfile" ; "t3427001291")]
#[test_case("dir", "myfile/" => "myfile" ; "t759151905")]
#[test_case("dir", "mydir/myfile" => "mydir/myfile" ; "t21199018562")]
#[test_case("dir", "/my/file.*" => "my/file.*" ; "t61199018162")]
#[test_case("dir", "/mydir/**.*" => "mydir/**.*" ; "t47199018162")]
fn test_pattern_line(dir: &str, pattern: &str) -> String {
let source = Source::File {
path: PathBuf::from(format!("{dir}.gitignore")),
line: 1,
};
let pattern = build_pattern(source, pattern);
pattern.pattern
}
#[test_case("", "#mydir/myfile", "" => matches MatchResult::NoMatch ; "t01")]
#[test_case("", "", "" => matches MatchResult::NoMatch ; "t02" )]
#[test_case("", "\n\n \n", "" => matches MatchResult::NoMatch; "t03" )]
#[test_case("", "dir-0001", "" => matches MatchResult::NoMatch ; "t04" )]
#[test_case("", "dir-0001/file-0001.bin", "" => matches MatchResult::NoMatch ; "t05" )]
#[test_case("", "dir-0001/*", "" => matches MatchResult::NoMatch ; "t06" )]
#[test_case("", "dir-0001/**", "" => matches MatchResult::NoMatch ; "t07" )]
#[test_case("", "dir-0001/dir-0001**", "" => matches MatchResult::NoMatch ; "t08" )]
#[test_case("", "dir-0001/dir-00*", "" => matches MatchResult::NoMatch ; "t09" )]
#[test_case("", "dir-00**/", "" => matches MatchResult::NoMatch ; "t10" )]
#[test_case("", "dir-00**/*/file-0001.bin", "" => matches MatchResult::NoMatch ; "t11" )]
#[test_case("", "dir-00**/*/*.bin", "" => matches MatchResult::NoMatch ; "t12" )]
#[test_case("", "dir-00**/", "" => matches MatchResult::NoMatch ; "t13" )]
#[test_case("", "#mydir/myfile", "" => matches MatchResult::NoMatch ; "t148864489901")]
#[test_case("", "", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t172475356002" )]
#[test_case("", "\n\n \n", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch; "t8688937603" )]
#[test_case("", "dir-0001", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t132833780304" )]
#[test_case("", "dir-0001/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t173193800505" )]
#[test_case("", "dir-0001/dir-0001**", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t318664043308" )]
#[test_case("", "dir-0001/dir-00*", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t269908728009" )]
#[test_case("", "dir-00**/*/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t142240004811" )]
#[test_case("", "dir-00**/*/*.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t414921892712" )]
#[test_case("", "dir-00**/", "dir-0001/file-0002.bin" => matches MatchResult::Ignore; "t256322548613" )]
#[test_case("", "dir-0001/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3378553489" )]
#[test_case("", "dir-0001/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3449646229" )]
#[test_case("", "dir-0001/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1232001745" )]
#[test_case("", "dir-0001/*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t2291655464" )]
#[test_case("", "dir-0001/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t355659763" )]
#[test_case("", "dir-0001/**", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1888678340" )]
#[test_case("", "dir-000?/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1603222532" )]
#[test_case("", "dir-000?/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t2528090273" )]
#[test_case("", "dir-*/*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3141482339" )]
#[test_case("", "!dir-0001", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t2963495371" )]
#[test_case("", "!dir-0001/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t3935333051" )]
#[test_case("", "!dir-0001/dir-0001**", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t3536143628" )]
#[test_case("", "!dir-0001/dir-00*", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t4079058836" )]
#[test_case("", "!dir-00**/", "dir-0001/file-0002.bin" => matches MatchResult::Whitelist ; "t3713155445" )]
#[test_case("", "!dir-00**/*/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t1434153118" )]
#[test_case("", "!dir-00**/*/*.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t1650195998" )]
#[test_case("", "!dir-0001/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t1569068369" )]
#[test_case("", "!dir-0001/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t2919165396" )]
#[test_case("", "!dir-0001/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t2682012728" )]
#[test_case("", "!dir-0001/*", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t4009543743" )]
#[test_case("", "!dir-0001/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t3333689486" )]
#[test_case("", "!dir-0001/**", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t4259364613" )]
#[test_case("", "!dir-000?/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t3424909626" )]
#[test_case("", "!dir-000?/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t3741545053" )]
#[test_case("", "!dir-*/*", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t1793504005" )]
#[test_case("dir-0001", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1295565113" )]
#[test_case("dir-0001", "/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t4048655621" )]
#[test_case("dir-0001", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t2580936986" )]
#[test_case("dir-0001", "/*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t109602877" )]
#[test_case("dir-0001", "/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t112292599" )]
#[test_case("dir-0001", "/**", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1323958164" )]
#[test_case("dir-0001", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t4225367752" )]
#[test_case("dir-0001", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3478922394" )]
#[test_case("dir-0002", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t345532514" )]
#[test_case("dir-0002", "/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t1313276210" )]
#[test_case("dir-0002", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t657078396" )]
#[test_case("dir-0002", "/*", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t2456576806" )]
#[test_case("dir-0002", "/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t2629832143" )]
#[test_case("dir-0002", "/**", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t2090580478" )]
#[test_case("dir-0002", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t1588943529" )]
#[test_case("dir-0002", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t371313784" )]
fn test_match_result(dir: &str, contents: &str, path: &str) -> MatchResult {
test_logging(LevelFilter::Trace);
let root = create_directory_hierarchy(false).unwrap();
let source_file = format!("{root}/{dir}/.gitignore");
let path = root.as_ref().join(path).to_owned();
let dwi =
new_dir_with_ignores(root.to_str().unwrap(), Some(&source_file), contents).unwrap();
check_ignore(&dwi, &path)
}
#[test_case(true => matches Ok(_); "this is to refresh the dir for each test run")]
fn create_directory_hierarchy(force: bool) -> Result<AbsolutePath> {
let temp_dir: PathBuf = seeded_temp_dir("xvc-walker", Some(20220615));
if force && temp_dir.exists() {
fs::remove_dir_all(&temp_dir)?;
}
if !temp_dir.exists() {
fs::create_dir(&temp_dir)?;
create_directory_tree(&temp_dir, 10, 10, 1000, None)?;
let level_1 = &temp_dir.join("dir-0001");
create_directory_tree(level_1, 10, 10, 1000, None)?;
let level_2 = &level_1.join("dir-0001");
create_directory_tree(level_2, 10, 10, 1000, None)?;
}
Ok(AbsolutePath::from(temp_dir))
}
}