use std::collections::{BTreeMap, BTreeSet, HashMap};
use super::config;
use super::decision::DecisionSpec;
use super::error::ConfigError;
use super::paths;
use super::reqfile::{CommandCheck, Reqfile, Requirement};
pub fn check_unique_ids(reqfiles: &[Reqfile]) -> Vec<ConfigError> {
let mut seen: HashMap<&str, &Reqfile> = HashMap::new();
let mut errors = Vec::new();
for reqfile in reqfiles {
for requirement in &reqfile.requirements {
match seen.get(requirement.id.as_str()) {
Some(first) => errors.push(ConfigError::at(
&reqfile.path,
requirement.line,
format!(
"duplicate requirement id {}, also defined in {}",
requirement.id, first.path
),
)),
None => {
seen.insert(&requirement.id, reqfile);
}
}
}
}
errors
}
pub fn applicable<'r>(reqfiles: &'r [Reqfile], dir: &str) -> Vec<(&'r Reqfile, &'r Requirement)> {
let mut found: Vec<&Reqfile> = reqfiles
.iter()
.filter(|r| paths::contains_dir(&r.dir, dir))
.collect();
found.sort_by_key(|r| r.dir.matches('/').count() + usize::from(!r.dir.is_empty()));
found
.into_iter()
.flat_map(|r| r.requirements.iter().map(move |req| (r, req)))
.collect()
}
pub struct Selection {
pub targets: BTreeSet<String>,
pub changed: Option<BTreeMap<String, Changed>>,
}
#[derive(Clone)]
pub struct Changed {
pub targets: BTreeSet<String>,
pub all: BTreeSet<String>,
}
impl Selection {
fn candidates<'s>(
&'s self,
dir: &'s str,
full: bool,
) -> impl Iterator<Item = (&'s str, &'s str)> + 's {
let changed = self.changed.as_ref().map(|by_dir| {
&by_dir
.get(dir)
.expect("changed paths are listed for every Reqfile folder")
.targets
});
let (first, second) = match changed {
Some(changed) if !full => (changed, None),
Some(changed) => (&self.targets, Some(changed)),
None => (&self.targets, None),
};
first
.iter()
.chain(
second
.into_iter()
.flatten()
.filter(|p| !self.targets.contains(*p)),
)
.filter_map(move |path| paths::relative_to(dir, path).map(|rel| (path.as_str(), rel)))
}
fn exists(&self, path: &str) -> bool {
self.targets.contains(path)
}
pub fn redefined(&self, reqfile: &Reqfile, requirement: &Requirement) -> bool {
let Some(changed) = self
.changed
.as_ref()
.and_then(|by_dir| by_dir.get(&reqfile.dir))
else {
return false;
};
let own = paths::join(&reqfile.dir, &format!(".reqfile/{}/", requirement.id));
changed.all.iter().any(|path| {
path == &reqfile.path
|| path.starts_with(&own)
|| config::config_dir(path).is_some_and(|dir| {
paths::contains_dir(dir, &reqfile.dir) || paths::contains_dir(&reqfile.dir, dir)
})
})
}
}
#[derive(Debug, PartialEq)]
pub enum CommandPlan {
Run(Vec<String>),
NoMatchingFiles,
}
pub fn plan_command(
dir: &str,
check: &CommandCheck,
selection: &Selection,
full: bool,
) -> CommandPlan {
let Some(glob) = &check.files else {
return CommandPlan::Run(Vec::new());
};
let matching: Vec<(&str, &str)> = selection
.candidates(dir, full)
.filter(|(_, rel)| glob.is_match(rel))
.collect();
if check.pass_files {
let args: Vec<String> = matching
.iter()
.filter(|(path, _)| selection.exists(path))
.map(|(_, rel)| rel.to_string())
.collect();
if args.is_empty() {
CommandPlan::NoMatchingFiles
} else {
CommandPlan::Run(args)
}
} else if matching.is_empty() {
CommandPlan::NoMatchingFiles
} else {
CommandPlan::Run(Vec::new())
}
}
pub fn decision_files<'s>(
dir: &'s str,
spec: &DecisionSpec,
selection: &'s Selection,
full: bool,
) -> Vec<(&'s str, &'s str)> {
selection
.candidates(dir, full)
.filter(|(path, rel)| selection.exists(path) && spec.applies_to(rel))
.collect()
}