use std::{
collections::{HashMap, hash_map::Entry},
ffi::OsStr,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use crate::evaluator::{self, File, types::Result, utils};
#[derive(Debug, Default)]
pub struct Evaluator {
files: Mutex<HashMap<PathBuf, Arc<File>>>,
}
impl Evaluator {
#[must_use]
pub fn is_ignored(&self, path: impl AsRef<Path>) -> bool {
let parts = path.as_ref().iter().collect::<Vec<&OsStr>>();
let mut ignored = false;
for i in 1..=parts.len() {
let base_path: PathBuf = parts[0..i].iter().collect();
let potential_gitignore = base_path.join(".gitignore");
if !potential_gitignore.exists() {
continue;
}
let gitignore_file = match self
.get_or_parse_gitignore(base_path.as_path(), potential_gitignore.as_path())
{
Ok(Some(gitignore_file)) => gitignore_file,
Ok(None) => continue,
Err(e) => {
log::error!(
"Failed to read .gitignore file at {}: {:?}",
potential_gitignore.display(),
e
);
continue;
}
};
let parent_path = parts[0..=i].iter().collect::<PathBuf>().join("");
if gitignore_file
.is_ignored(parent_path.as_path())
.is_some_and(|ignored| ignored)
{
ignored = true;
log::debug!(
"{} is ignored so {} is ignored by association.",
parent_path.as_path().display(),
path.as_ref().display()
);
break;
}
if let Some(result) = gitignore_file.is_ignored(path.as_ref()) {
ignored = result;
}
}
log::debug!("{} is ignored: {ignored}", path.as_ref().display());
ignored
}
fn get_or_parse_gitignore(
&self,
base_path: impl AsRef<Path>,
potential_gitignore: impl AsRef<Path>,
) -> Result<Option<Arc<File>>> {
if !potential_gitignore.as_ref().exists() {
return Ok(None);
}
let mut guard = self.files.lock().map_err(|_| {
evaluator::Error::CachePoisoned(potential_gitignore.as_ref().to_path_buf())
})?;
let gitignore_file = match guard.entry(potential_gitignore.as_ref().to_path_buf()) {
Entry::Occupied(mut e) => {
{
let existing_file = e.get_mut();
let (target_checksum, _) = crate::utils::compute_checksum(
potential_gitignore.as_ref(),
)
.map_err(|e| evaluator::Error::FileError {
file: potential_gitignore.as_ref().to_path_buf(),
source: e,
})?;
if existing_file.checksum == target_checksum {
return Ok(Some(Arc::clone(existing_file)));
}
}
Arc::clone(&e.insert(Arc::new(utils::read_gitignore(
base_path,
potential_gitignore.as_ref(),
)?)))
}
Entry::Vacant(e) => {
let gitignore_file = Arc::new(utils::read_gitignore(
base_path,
potential_gitignore.as_ref(),
)?);
Arc::clone(e.insert(gitignore_file))
}
};
drop(guard);
Ok(Some(gitignore_file))
}
}