callisto-graph 0.3.1

Dependency graph, cascade, aggregation, and config resolution for callisto.
Documentation
use callisto_model::{CommandRunner, ValidateReport, SCHEMA_VERSION};

use crate::commands::escalate;
use crate::error::GraphError;
use crate::resolver::DependencyResolver;
use crate::Workspace;

#[derive(Clone, Debug, Default)]
pub struct ValidateOptions {
    pub staged: bool,
    pub since: Option<String>,
    pub strict: bool,
    pub strict_graph: bool,
}

pub fn validate<R: CommandRunner, D: DependencyResolver>(
    ws: &Workspace<'_, R, D>,
    opts: &ValidateOptions,
) -> Result<ValidateReport, GraphError> {
    let mut diagnostics = Vec::new();
    let loaded = crate::load_changesets(&ws.root, &ws.config)?;

    let target_changesets: Vec<_> = if opts.staged {
        let out = ws
            .runner
            .run("git", &["diff", "--cached", "--name-only"], &ws.root)?;
        if !out.success() {
            return Err(GraphError::Command(callisto_model::CommandError::Io {
                program: "git".to_string(),
                message: out.stderr,
            }));
        }
        let files: Vec<String> = out
            .stdout_trimmed()
            .lines()
            .map(|l| l.trim_matches('"').to_string())
            .collect();
        loaded
            .into_iter()
            .filter(|cs| files.iter().any(|f| cs.path.ends_with(f)))
            .collect()
    } else if let Some(ref since) = opts.since {
        if since.starts_with('-') {
            return Err(GraphError::Command(callisto_model::CommandError::Io {
                program: "git".to_string(),
                message: "invalid since ref".to_string(),
            }));
        }
        let range = format!("{since}..HEAD");
        let out = ws
            .runner
            .run("git", &["diff", "--name-only", &range, "--"], &ws.root)?;
        if !out.success() {
            return Err(GraphError::Command(callisto_model::CommandError::Io {
                program: "git".to_string(),
                message: out.stderr,
            }));
        }
        let files: Vec<String> = out
            .stdout_trimmed()
            .lines()
            .map(|l| l.trim_matches('"').to_string())
            .collect();
        loaded
            .into_iter()
            .filter(|cs| files.iter().any(|f| cs.path.ends_with(f)))
            .collect()
    } else {
        loaded
    };

    for cs in &target_changesets {
        if cs.changeset.entries.is_empty() {
            diagnostics.push(callisto_model::Diagnostic {
                code: callisto_model::DiagnosticCode::EmptyChangeset,
                severity: callisto_model::DiagnosticSeverity::Error,
                message: format!("Changeset `{}` is empty", cs.path.display()),
                package: None,
                path: Some(cs.path.clone()),
                governed_by: None,
                escalated_by: None,
            });
        }

        for entry in &cs.changeset.entries {
            if let Ok(id) = callisto_model::PackageId::parse(&entry.name) {
                if !ws.graph.packages().any(|p| p.id.matches(&id)) {
                    diagnostics.push(callisto_model::Diagnostic {
                        code: callisto_model::DiagnosticCode::UnknownPackage,
                        severity: callisto_model::DiagnosticSeverity::Error,
                        message: format!(
                            "Changeset `{}` references unknown package `{}`",
                            cs.path.display(),
                            entry.name
                        ),
                        package: Some(id),
                        path: Some(cs.path.clone()),
                        governed_by: None,
                        escalated_by: None,
                    });
                }
            }
        }
    }

    escalate(&mut diagnostics, opts.strict, opts.strict_graph);

    let is_valid = diagnostics
        .iter()
        .all(|d| d.severity != callisto_model::DiagnosticSeverity::Error);
    Ok(ValidateReport {
        schema_version: SCHEMA_VERSION,
        valid: is_valid,
        diagnostics,
    })
}