use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::detect::read_listing;
use crate::error::Result;
use crate::indicator::Match;
use crate::registry::Registry;
#[derive(Debug, Clone, Default)]
pub struct FindOptions {
pub types: Vec<String>,
pub excludes: Vec<String>,
pub nested: bool,
pub respect_gitignore: bool,
pub timeout: Option<Duration>,
pub cancel: Option<Arc<AtomicBool>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FindResult {
pub projects: Vec<FoundProject>,
pub count: usize,
#[serde(default, skip_serializing_if = "is_false")]
pub cancelled: bool,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub cancellation_reason: String,
#[serde(default, skip_serializing_if = "is_zero")]
pub elapsed_seconds: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FoundProject {
pub path: PathBuf,
pub types: Vec<Match>,
}
fn is_false(b: &bool) -> bool {
!*b
}
fn is_zero(f: &f64) -> bool {
*f == 0.0
}
impl Registry {
pub fn find(&self, root: impl AsRef<Path>, opts: &FindOptions) -> Result<FindResult> {
let root = root.as_ref();
let start = Instant::now();
let want: Option<HashSet<String>> = if opts.types.is_empty() {
None
} else {
Some(opts.types.iter().cloned().collect())
};
let gitignore = if opts.respect_gitignore {
build_root_gitignore(root)
} else {
None
};
let mut walk = Walk {
reg: self,
opts,
excluder: Excluder::new(&opts.excludes),
gitignore,
want,
deadline: opts.timeout.map(|d| start + d),
out: FindResult::default(),
};
walk.walk(root, true);
let mut out = walk.out;
out.count = out.projects.len();
out.elapsed_seconds = start.elapsed().as_secs_f64();
Ok(out)
}
}
struct Walk<'a> {
reg: &'a Registry,
opts: &'a FindOptions,
excluder: Excluder,
gitignore: Option<ignore::gitignore::Gitignore>,
want: Option<HashSet<String>>,
deadline: Option<Instant>,
out: FindResult,
}
impl Walk<'_> {
fn walk(&mut self, dir: &Path, is_root: bool) -> bool {
if let Some(reason) = self.check_cancel() {
self.out.cancelled = true;
self.out.cancellation_reason = reason.to_string();
return false;
}
if !is_root {
let base = dir
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
if self.excluder.skip(&base) {
return true;
}
if let Some(gi) = &self.gitignore {
if gi.matched(dir, true).is_ignore() {
return true;
}
}
}
let (files, subdirs) = match read_listing(dir) {
Ok(v) => v,
Err(_) => return true,
};
let mut matches = self.reg.match_dir(&files, &subdirs);
if !matches.is_empty() {
if let Some(want) = &self.want {
matches.retain(|m| want.contains(&m.r#type));
}
if !matches.is_empty() {
self.out.projects.push(FoundProject {
path: dir.to_path_buf(),
types: matches,
});
if !self.opts.nested {
return true;
}
}
}
let mut subs: Vec<&String> = subdirs.iter().collect();
subs.sort();
for sd in subs {
if !self.walk(&dir.join(sd), false) {
return false;
}
}
true
}
fn check_cancel(&self) -> Option<&'static str> {
if let Some(flag) = &self.opts.cancel {
if flag.load(Ordering::Relaxed) {
return Some("client_cancel");
}
}
if let Some(dl) = self.deadline {
if Instant::now() >= dl {
return Some("timeout");
}
}
None
}
}
fn build_root_gitignore(root: &Path) -> Option<ignore::gitignore::Gitignore> {
let gi_path = root.join(".gitignore");
if !gi_path.is_file() {
return None;
}
let mut builder = ignore::gitignore::GitignoreBuilder::new(root);
builder.add(&gi_path);
builder.build().ok()
}
struct Excluder {
patterns: Vec<(String, Option<glob::Pattern>)>,
}
impl Excluder {
fn new(patterns: &[String]) -> Self {
Excluder {
patterns: patterns
.iter()
.map(|p| (p.clone(), glob::Pattern::new(p).ok()))
.collect(),
}
}
fn skip(&self, name: &str) -> bool {
self.patterns.iter().any(|(lit, pat)| match pat {
Some(p) => p.matches(name),
None => lit == name,
})
}
}