use std::{path::Path, sync::Arc};
use super::{
DirectoryBackend, Listing, Walker, glob_path_bytes,
ignore_rules::{RuleSet, RuleSetBuilder},
};
const IGNORE_FILES: [&str; 2] = [".gitignore", ".ignore"];
const REPOSITORY_EXCLUDE_FILE: &str = ".git/info/exclude";
#[derive(Debug, Clone, Default)]
pub(crate) struct IgnoreScope {
rules: Option<Arc<IgnoreNode>>,
}
impl IgnoreScope {
pub(crate) fn for_root<B: DirectoryBackend + ?Sized>(
walker: &Walker,
backend: &B,
root: &Path,
) -> Self {
if !walker.respect_git_ignore {
return Self::default();
}
Self::default().link(read_rules(backend, root, &[REPOSITORY_EXCLUDE_FILE]))
}
pub(crate) fn enter<B: DirectoryBackend + ?Sized>(
self,
walker: &Walker,
backend: &B,
directory: &Path,
listing: &Listing,
) -> Self {
if !walker.respect_git_ignore {
return self;
}
let present = IGNORE_FILES
.into_iter()
.filter(|file| listing.contains(file))
.collect::<Vec<_>>();
if present.is_empty() {
return self;
}
let rules = read_rules(backend, directory, &present);
self.link(rules)
}
pub(crate) fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
let Some(node) = self.rules.as_ref() else {
return false;
};
let candidate = glob_path_bytes(path);
node.verdict(&candidate, is_dir).unwrap_or(false)
}
fn link(self, rules: RuleSet) -> Self {
if rules.is_empty() {
return self;
}
Self {
rules: Some(Arc::new(IgnoreNode {
rules,
parent: self.rules,
})),
}
}
}
#[derive(Debug)]
struct IgnoreNode {
rules: RuleSet,
parent: Option<Arc<IgnoreNode>>,
}
impl IgnoreNode {
fn verdict(&self, candidate: &[u8], is_dir: bool) -> Option<bool> {
self.rules.matched(candidate, is_dir).or_else(|| {
self.parent
.as_ref()
.and_then(|parent| parent.verdict(candidate, is_dir))
})
}
}
fn read_rules<B: DirectoryBackend + ?Sized>(
backend: &B,
directory: &Path,
files: &[&str],
) -> RuleSet {
let mut builder = RuleSetBuilder::new(directory);
for file in files {
let path = directory.join(file);
if let Ok(contents) = backend.read_ignore_file(&path) {
add_rules(&mut builder, &contents);
}
}
builder.build()
}
fn add_rules(builder: &mut RuleSetBuilder, contents: &[u8]) {
let text = match std::str::from_utf8(contents) {
Ok(text) => text,
Err(error) => {
let valid = &contents[..error.valid_up_to()];
let complete = valid
.iter()
.rposition(|&byte| byte == b'\n')
.map_or(0, |index| index + 1);
std::str::from_utf8(&valid[..complete]).unwrap_or_default()
}
};
for (index, line) in text.lines().enumerate() {
let line = if index == 0 {
line.trim_start_matches('\u{feff}')
} else {
line
};
builder.add_line(line);
}
}