use std::collections::HashSet;
use anyhow::Result;
use ctl_core::input::Input;
use crate::cli::CheckArgs;
use crate::ledger::{Ledger, graph_errors, load_value, resolve_path, schema_value};
use crate::report::Report;
use crate::trailers;
pub fn run(args: &CheckArgs) -> Result<Report> {
let path = resolve_path(&args.ledger);
let schema = schema_value()?;
let input = Input::read(&path)?;
let instance = load_value(&input)?;
if instance
.get("schema_version")
.and_then(serde_json::Value::as_u64)
== Some(3)
{
return Ok(Report::Check {
path: path.display().to_string(),
problems: vec![format!(
"{}: schema_version 3; run qctl fmt to rewrite notes into a list and set schema_version 4",
path.display()
)],
});
}
let validator = jsonschema::options()
.should_validate_formats(true)
.build(&schema)?;
let placed = yamled::Document::parse(input.text()).ok();
let mut errors: Vec<String> = validator
.iter_errors(&instance)
.map(|error| {
let pointer = error.instance_path().to_string();
let at = placed.as_ref().and_then(|document| {
document.locate_nearest(&yamled::Path::from_pointer(&pointer).ok()?)
});
match at {
Some(at) => format!(
"{}:{}:{}: {pointer}: {error}",
input.name(),
at.line,
at.column
),
None => format!("{}: {pointer}: {error}", input.name()),
}
})
.collect();
match input.parse::<Ledger>() {
Ok(ledger) => {
errors.extend(graph_errors(&ledger, &path));
if !args.no_git {
errors.extend(trailer_errors(&ledger, &path));
}
}
Err(error) => errors.push(format!("{error:#}")),
}
if errors.is_empty() {
return Ok(Report::Check {
path: path.display().to_string(),
problems: Vec::new(),
});
}
Ok(Report::Check {
path: path.display().to_string(),
problems: errors,
})
}
fn trailer_errors(ledger: &crate::ledger::Ledger, path: &std::path::Path) -> Vec<String> {
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(error) => return vec![format!("git trailer scan failed: {error}")],
};
let ledger_dir = match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent,
_ => cwd.as_path(),
};
let skip = "git trailer scan skipped: ledger is not in the current repository; pass --no-git";
let ledger_root = match trailers::git_root_status(ledger_dir) {
trailers::GitRoot::Failed(error) => {
return vec![format!("git trailer scan failed: {error:#}")];
}
trailers::GitRoot::Absent => return vec![skip.into()],
trailers::GitRoot::Root(root) => root,
};
let cwd_root = match trailers::git_root_status(&cwd) {
trailers::GitRoot::Failed(error) => {
return vec![format!("git trailer scan failed: {error:#}")];
}
trailers::GitRoot::Absent => return vec![skip.into()],
trailers::GitRoot::Root(root) => root,
};
let ledger_root = ledger_root.canonicalize().unwrap_or(ledger_root);
let cwd_root = cwd_root.canonicalize().unwrap_or(cwd_root);
if ledger_root != cwd_root {
return vec![skip.into()];
}
let closed = match trailers::closed_ids(&ledger_root) {
Ok(closed) => closed,
Err(error) => return vec![format!("git trailer scan failed: {error:#}")],
};
let queued: HashSet<_> = ledger.queue.iter().map(|task| task.id.as_str()).collect();
let mut errors = Vec::new();
let mut seen = HashSet::new();
for (id, sha) in closed {
if !queued.contains(id.as_str()) || !seen.insert(id.clone()) {
continue;
}
errors.push(format!(
"{id} is closed by {sha} but still queued; archive it (Closes/Completes trailer)"
));
}
errors
}