use std::{path::Path, sync::Arc};
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use super::{BackendEntry, DirectoryBackend, Walker};
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 root<B: DirectoryBackend + ?Sized>(walker: &Walker, backend: &B) -> Self {
if !walker.respect_git_ignore {
return Self::default();
}
Self::default().link(read_rules(
backend,
&walker.root,
&[REPOSITORY_EXCLUDE_FILE],
))
}
pub(crate) fn enter<B: DirectoryBackend + ?Sized>(
self,
walker: &Walker,
backend: &B,
directory: &Path,
entries: &[BackendEntry],
) -> Self {
if !walker.respect_git_ignore {
return self;
}
let present = IGNORE_FILES
.into_iter()
.filter(|file| {
entries
.iter()
.any(|entry| entry.path.file_name().is_some_and(|name| name == *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 {
self.rules
.as_ref()
.and_then(|node| node.verdict(path, is_dir))
.unwrap_or(false)
}
fn link(self, rules: Gitignore) -> Self {
if rules.is_empty() {
return self;
}
Self {
rules: Some(Arc::new(IgnoreNode {
rules,
parent: self.rules,
})),
}
}
}
#[derive(Debug)]
struct IgnoreNode {
rules: Gitignore,
parent: Option<Arc<IgnoreNode>>,
}
impl IgnoreNode {
fn verdict(&self, path: &Path, is_dir: bool) -> Option<bool> {
let matched = self.rules.matched(path, is_dir);
if !matched.is_none() {
return Some(matched.is_ignore());
}
self.parent
.as_ref()
.and_then(|parent| parent.verdict(path, is_dir))
}
}
fn read_rules<B: DirectoryBackend + ?Sized>(
backend: &B,
directory: &Path,
files: &[&str],
) -> Gitignore {
let mut builder = GitignoreBuilder::new(directory);
for file in files {
let path = directory.join(file);
if let Ok(contents) = backend.read_ignore_file(&path) {
add_rules(&mut builder, &path, &contents);
}
}
builder.build().unwrap_or_else(|_| Gitignore::empty())
}
fn add_rules(builder: &mut GitignoreBuilder, path: &Path, 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
};
let _ = builder.add_line(Some(path.to_path_buf()), line);
}
}