2026-09-22 17:25:52 +02:00
|
|
|
//! Loading a content repo from a local checkout - the offline
|
|
|
|
|
//! counterparts to the Gitea loaders in `content`, for tools that lint
|
|
|
|
|
//! or replay a branch that has not been pushed (question_lint,
|
|
|
|
|
//! needs_replay). `dir` is always the pages directory (`questions/`);
|
|
|
|
|
//! `site.yaml`, `aggregates.yaml` and `needs.yaml` sit one level up.
|
|
|
|
|
|
|
|
|
|
use portal::{aggregates, content};
|
|
|
|
|
|
|
|
|
|
/// The offline counterpart to `content::load_site_from_gitea` -
|
|
|
|
|
/// `site.yaml` lives at the repo root like `aggregates.yaml`, and is
|
|
|
|
|
/// just as optional locally: missing means default branding.
|
|
|
|
|
pub fn load_site_from_dir(dir: &str) -> anyhow::Result<content::SiteConfig> {
|
|
|
|
|
let site_path = std::path::Path::new(dir)
|
|
|
|
|
.parent()
|
|
|
|
|
.unwrap_or_else(|| std::path::Path::new("."))
|
|
|
|
|
.join("site.yaml");
|
|
|
|
|
if !site_path.exists() {
|
|
|
|
|
return Ok(content::SiteConfig::default());
|
|
|
|
|
}
|
|
|
|
|
let raw = std::fs::read_to_string(&site_path)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("reading {}: {e}", site_path.display()))?;
|
|
|
|
|
serde_yaml::from_str(&raw).map_err(|e| anyhow::anyhow!("parsing {}: {e}", site_path.display()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The offline counterpart to `content::load_aggregates_from_gitea` -
|
|
|
|
|
/// `aggregates.yaml` lives at the repo root, one level up from the
|
|
|
|
|
/// pages directory `--path` names, so `dir`'s parent is where it's
|
|
|
|
|
/// looked for. Missing entirely is not an error here (unlike
|
|
|
|
|
/// `--repo` mode, where `aggregates.yaml` is required) - a local
|
|
|
|
|
/// checkout being linted may not have one, and every declared
|
|
|
|
|
/// transition still gets checked against whatever *is* found; an
|
|
|
|
|
/// empty map just means nothing is checked.
|
|
|
|
|
pub fn load_aggregates_from_dir(
|
|
|
|
|
dir: &str,
|
|
|
|
|
) -> anyhow::Result<std::collections::HashMap<String, aggregates::AggregateSchema>> {
|
|
|
|
|
let aggregates_path = std::path::Path::new(dir)
|
|
|
|
|
.parent()
|
|
|
|
|
.unwrap_or_else(|| std::path::Path::new("."))
|
|
|
|
|
.join("aggregates.yaml");
|
|
|
|
|
if !aggregates_path.exists() {
|
|
|
|
|
return Ok(std::collections::HashMap::new());
|
|
|
|
|
}
|
|
|
|
|
let raw = std::fs::read_to_string(&aggregates_path)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("reading {}: {e}", aggregates_path.display()))?;
|
|
|
|
|
aggregates::parse_aggregates_yaml(&raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The offline counterpart to `content::load_questions_from_gitea` -
|
|
|
|
|
/// the same recursive tree walk and `content::build_questions`
|
|
|
|
|
/// pipeline (derived ids, relative refs, sections, followup
|
|
|
|
|
/// inference), just reading a local checkout instead of Gitea's API,
|
|
|
|
|
/// for linting a branch that hasn't been pushed yet.
|
|
|
|
|
pub fn load_from_dir(
|
|
|
|
|
dir: &str,
|
|
|
|
|
) -> anyhow::Result<std::collections::HashMap<String, content::Question>> {
|
|
|
|
|
let base = std::path::Path::new(dir);
|
|
|
|
|
let mut files = Vec::new();
|
|
|
|
|
collect_yaml(base, base, &mut files)?;
|
|
|
|
|
content::build_questions(&files)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_yaml(
|
|
|
|
|
base: &std::path::Path,
|
|
|
|
|
dir: &std::path::Path,
|
|
|
|
|
out: &mut Vec<(String, String)>,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
for entry in std::fs::read_dir(dir)? {
|
|
|
|
|
let path = entry?.path();
|
|
|
|
|
if path.is_dir() {
|
|
|
|
|
collect_yaml(base, &path, out)?;
|
|
|
|
|
} else if path.extension().and_then(|e| e.to_str()) == Some("yaml") {
|
|
|
|
|
let rel = path
|
|
|
|
|
.strip_prefix(base)
|
|
|
|
|
.expect("walked paths sit under their base")
|
|
|
|
|
.to_string_lossy()
|
|
|
|
|
.replace('\\', "/");
|
|
|
|
|
let raw = std::fs::read_to_string(&path)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("reading {}: {e}", path.display()))?;
|
|
|
|
|
out.push((rel, raw));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 17:28:38 +02:00
|
|
|
/// The offline counterpart to `check`'s needs.yaml fetch -
|
2026-09-22 17:25:52 +02:00
|
|
|
/// `needs.yaml` sits at the repo root beside `aggregates.yaml`.
|
|
|
|
|
pub fn load_needs_from_dir(dir: &str) -> anyhow::Result<Option<String>> {
|
|
|
|
|
let needs_path = std::path::Path::new(dir)
|
|
|
|
|
.parent()
|
|
|
|
|
.unwrap_or_else(|| std::path::Path::new("."))
|
|
|
|
|
.join("needs.yaml");
|
|
|
|
|
if !needs_path.exists() {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
std::fs::read_to_string(&needs_path)
|
|
|
|
|
.map(Some)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("reading {}: {e}", needs_path.display()))
|
|
|
|
|
}
|
|
|
|
|
|