use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
use ignore::{IncrementalIgnore, WalkBuilder};
use crate::git::{self, WorkTree};
use crate::walk::WalkError;
pub const DEFAULT_MIN_SIZE: u64 = 10 * 1024 * 1024;
#[derive(Debug, Clone, Default)]
pub struct FallbackReport {
pub enabled: bool,
pub min_size: u64,
pub work_trees: usize,
pub outside_work_tree: usize,
pub holding_a_checkout: usize,
pub hits: usize,
pub files_enabled: bool,
pub files: usize,
}
impl FallbackReport {
#[must_use]
pub fn is_inert(&self) -> bool {
self.enabled && self.work_trees == 0
}
}
#[derive(Debug)]
pub(crate) struct Fallback {
scan_root: PathBuf,
root_work_tree: Option<PathBuf>,
min_size: u64,
files: bool,
trees: Mutex<HashMap<PathBuf, Arc<Opened>>>,
outside_work_tree: AtomicUsize,
}
type Opened = OnceLock<Result<Arc<WorkTree>, Arc<str>>>;
impl Fallback {
pub(crate) fn new(scan_root: &Path, min_size: u64, files: bool) -> Self {
Self {
scan_root: scan_root.to_path_buf(),
root_work_tree: git::discover(scan_root),
min_size,
files,
trees: Mutex::new(HashMap::new()),
outside_work_tree: AtomicUsize::new(0),
}
}
pub(crate) fn claims_files(&self) -> bool {
self.files
}
pub(crate) fn thread(&self) -> Thread<'_> {
Thread {
shared: self,
recent: None,
matchers: HashMap::new(),
trees: HashMap::new(),
}
}
pub(crate) fn finish(
&self,
hits: usize,
files: usize,
holding_a_checkout: usize,
) -> (FallbackReport, Vec<WalkError>) {
let trees = lock(&self.trees);
let mut work_trees = 0;
let mut errors = Vec::new();
for (root, opened) in trees.iter() {
match opened.get() {
Some(Ok(_)) => work_trees += 1,
Some(Err(message)) => errors.push(WalkError {
path: Some(root.clone()),
message: message.to_string(),
forbidden: false,
}),
None => {}
}
}
let report = FallbackReport {
enabled: true,
min_size: self.min_size,
work_trees,
outside_work_tree: self.outside_work_tree.load(Ordering::Relaxed),
holding_a_checkout,
hits,
files_enabled: self.files,
files,
};
(report, errors)
}
}
#[derive(Debug)]
pub(crate) struct Thread<'a> {
shared: &'a Fallback,
recent: Option<(PathBuf, Option<PathBuf>)>,
matchers: HashMap<PathBuf, IncrementalIgnore>,
trees: HashMap<PathBuf, Option<Arc<WorkTree>>>,
}
impl Thread<'_> {
pub(crate) fn claims_files(&self) -> bool {
self.shared.claims_files()
}
pub(crate) fn judge(&mut self, path: &Path, is_dir: bool) -> Option<PathBuf> {
let Some(work_tree) = self.work_tree_of(path) else {
self.shared
.outside_work_tree
.fetch_add(1, Ordering::Relaxed);
return None;
};
let tracked = self.tree(&work_tree)?;
let relative = path.strip_prefix(&work_tree).ok()?.to_path_buf();
let matcher = self.matcher(&work_tree);
if !matcher.matched(&relative, is_dir).is_ignore() {
return None;
}
if tracked.holds_tracked_path(path) {
return None;
}
Some(work_tree)
}
fn work_tree_of(&mut self, dir: &Path) -> Option<PathBuf> {
if git::is_work_tree_root(dir) {
return Some(dir.to_path_buf());
}
let parent = dir.parent()?;
if let Some((cached, answer)) = &self.recent
&& cached == parent
{
return answer.clone();
}
let answer = self.search_up(parent);
self.recent = Some((parent.to_path_buf(), answer.clone()));
answer
}
fn search_up(&self, dir: &Path) -> Option<PathBuf> {
let mut cursor = Some(dir);
while let Some(candidate) = cursor {
if candidate == self.shared.scan_root || !candidate.starts_with(&self.shared.scan_root)
{
return self.shared.root_work_tree.clone();
}
if git::is_work_tree_root(candidate) {
return Some(candidate.to_path_buf());
}
cursor = candidate.parent();
}
self.shared.root_work_tree.clone()
}
fn matcher(&mut self, work_tree: &Path) -> &mut IncrementalIgnore {
self.matchers
.entry(work_tree.to_path_buf())
.or_insert_with(|| build_matcher(work_tree))
}
fn tree(&mut self, work_tree: &Path) -> Option<Arc<WorkTree>> {
if let Some(cached) = self.trees.get(work_tree) {
return cached.clone();
}
let opened = {
let mut trees = lock(&self.shared.trees);
Arc::clone(trees.entry(work_tree.to_path_buf()).or_default())
};
let tree = opened
.get_or_init(|| {
WorkTree::open(work_tree)
.map(Arc::new)
.map_err(|err| Arc::from(err.to_string().as_str()))
})
.as_ref()
.ok()
.map(Arc::clone);
self.trees.insert(work_tree.to_path_buf(), tree.clone());
tree
}
}
fn build_matcher(work_tree: &Path) -> IncrementalIgnore {
let mut builder = WalkBuilder::new(work_tree);
builder
.hidden(false)
.parents(false)
.ignore(false)
.git_ignore(true)
.git_exclude(true)
.git_global(true);
let mut matchers = builder.build_matchers();
debug_assert_eq!(matchers.len(), 1, "one root in, one matcher out");
matchers.remove(0)
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
#[cfg(test)]
mod tests {
use super::FallbackReport;
#[test]
fn a_tier_that_was_never_asked_is_not_inert() {
let off = FallbackReport::default();
assert!(!off.enabled);
assert!(!off.is_inert());
}
#[test]
fn a_tier_that_ran_and_found_no_work_tree_is_inert() {
let report = FallbackReport {
enabled: true,
work_trees: 0,
..FallbackReport::default()
};
assert!(report.is_inert());
}
#[test]
fn a_tier_that_found_a_work_tree_and_nothing_in_it_is_not_inert() {
let report = FallbackReport {
enabled: true,
work_trees: 3,
hits: 0,
..FallbackReport::default()
};
assert!(!report.is_inert());
}
}