use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use super::git;
use crate::core::config::{self, Settings};
use crate::core::paths;
use crate::core::plan;
use crate::core::reqfile::{self, Reqfile};
pub struct Workspace {
pub root: PathBuf,
pub settings: Settings,
pub targets: BTreeSet<String>,
pub reqfiles: Vec<Reqfile>,
}
impl Workspace {
pub fn load(cwd: &Path) -> Result<Self, Vec<String>> {
let root = git::root(cwd).map_err(|e| vec![e])?;
let files: Vec<String> = git::files(&root)
.map_err(|e| vec![e])?
.into_iter()
.filter(|p| fs::symlink_metadata(root.join(p)).is_ok_and(|m| m.is_file()))
.collect();
let mut errors = Vec::new();
let settings = load_settings(&root, &files, &mut errors);
let targets: BTreeSet<String> = files
.into_iter()
.filter(|p| !settings.is_excluded(p))
.collect();
let reqfiles = load_reqfiles(&root, &targets, &mut errors);
if errors.is_empty() {
Ok(Self {
root,
settings,
targets,
reqfiles,
})
} else {
Err(errors)
}
}
pub fn repository_path(&self, cwd: &Path, path: &Path) -> Result<String, String> {
let cwd = cwd
.canonicalize()
.map_err(|e| format!("cannot resolve the current folder: {e}"))?;
let absolute = cwd.join(path);
let relative = absolute
.strip_prefix(&self.root)
.map_err(|_| format!("{} is outside the repository", path.display()))?;
paths::normalize(&relative.to_string_lossy())
.ok_or_else(|| format!("{} is outside the repository", path.display()))
}
}
fn load_settings(root: &Path, files: &[String], errors: &mut Vec<String>) -> Settings {
let mut configs: Vec<(&str, &String)> = files
.iter()
.filter_map(|p| config::config_dir(p).map(|dir| (dir, p)))
.collect();
configs.sort_by_key(|(dir, _)| config::depth(dir));
let mut settings = Settings::default();
for (_, path) in configs {
if settings.excluded_by(path).is_some() {
continue;
}
match read(root, path)
.and_then(|text| config::parse(path, &text).map_err(|e| e.to_string()))
{
Ok(config) => settings.add(config),
Err(e) => errors.push(e),
}
}
for path in files
.iter()
.filter(|p| config::is_near_miss(p) && settings.excluded_by(p).is_none())
{
errors.push(format!(
"{path}:1: a config file must be named exactly .reqfile/config.yaml"
));
}
settings
}
fn load_reqfiles(
root: &Path,
targets: &BTreeSet<String>,
errors: &mut Vec<String>,
) -> Vec<Reqfile> {
for path in targets.iter().filter(|p| reqfile::is_near_miss(p)) {
match read(root, path) {
Ok(text) if !reqfile::looks_like_reqfile(&text) => {}
Ok(_) => errors.push(format!(
"{path}:1: a requirements file must be named exactly {}",
reqfile::FILE_NAME
)),
Err(e) => errors.push(e),
}
}
let mut reqfiles = Vec::new();
for path in targets
.iter()
.filter(|p| paths::file_name(p) == reqfile::FILE_NAME)
{
match read(root, path)
.and_then(|text| reqfile::parse(path, &text).map_err(|e| e.to_string()))
{
Ok(reqfile) => reqfiles.push(reqfile),
Err(e) => errors.push(e),
}
}
errors.extend(
plan::check_unique_ids(&reqfiles)
.iter()
.map(ToString::to_string),
);
reqfiles
}
fn read(root: &Path, path: &str) -> Result<String, String> {
fs::read_to_string(root.join(path)).map_err(|e| format!("{path}: cannot read: {e}"))
}