use crate::commands::bump::cargo_edit::{MemberInfo, WorkspaceInfo, load_workspace};
use crate::commands::bump::plan::resolve_member_version;
use crate::commands::version_files_resolve::resolve_version_files;
use crate::pipeline;
use anodizer_core::config::{Config, CrateConfig};
use anodizer_core::log::{StageLogger, Verbosity};
use anodizer_core::version_files::check_version_present;
use anodizer_stage_build::version_sync::read_cargo_version;
use anyhow::{Context, Result, bail};
use std::collections::BTreeSet;
use std::path::Path;
pub fn run(config_override: Option<&Path>, verbose: bool, debug: bool, quiet: bool) -> Result<()> {
let log = StageLogger::new("check", Verbosity::from_flags(quiet, verbose, debug));
let repo_root = match crate::commands::helpers::discover_workspace_root(config_override) {
Ok(root) => root,
Err(_) => std::env::current_dir().context("resolving repo root")?,
};
let config = match config_override {
Some(p) => {
log.verbose(&format!("loading config from {}", p.display()));
pipeline::load_config(p)?
}
None => {
log.verbose(&format!("loading config from {}", repo_root.display()));
pipeline::load_repo_config(&repo_root)?
}
};
run_guard(&config, &repo_root, &log)
}
fn run_guard(config: &Config, repo_root: &Path, log: &StageLogger) -> Result<()> {
let mut findings: Vec<String> = vec![];
let mut checked = 0usize;
let units = enrolled_units(config);
let ws = load_workspace(repo_root).ok();
let mut seen: BTreeSet<(String, String)> = BTreeSet::new();
for unit in units {
let version = match unit_reference_version(repo_root, &unit, ws.as_ref()) {
Ok(v) => v,
Err(e) => {
findings.push(format!(
"{}: cannot read current version: {e:#}",
unit.label
));
continue;
}
};
let file_list = unit.files;
for file in &file_list {
if !seen.insert((file.clone(), version.clone())) {
continue;
}
let abs = repo_root.join(file).to_string_lossy().into_owned();
match check_version_present(std::slice::from_ref(&abs), &version) {
Ok(results) => {
checked += 1;
let present = results.first().map(|(_, p)| *p).unwrap_or(false);
if present {
log.verbose(&format!("OK: {file} contains {version}"));
} else {
findings.push(format!("STALE: {file} (expected {version}, not found)"));
}
}
Err(e) => {
findings.push(format!("STALE: {file} ({e:#})"));
}
}
}
}
if checked == 0 && findings.is_empty() {
log.status("no version_files configured");
return Ok(());
}
if findings.is_empty() {
log.status(&format!("all {checked} version_files are in sync"));
Ok(())
} else {
for f in &findings {
log.error(f);
}
bail!(
"version_files check failed with {} finding(s)",
findings.len()
);
}
}
struct EnrolledUnit {
label: String,
path: String,
files: Vec<String>,
is_lockstep_root: bool,
}
fn enrolled_units(config: &Config) -> Vec<EnrolledUnit> {
let top_level = config.version_files.as_deref().unwrap_or_default();
let all_crates: Vec<&CrateConfig> = config
.crates
.iter()
.chain(
config
.workspaces
.iter()
.flatten()
.flat_map(|ws| ws.crates.iter()),
)
.collect();
let mut units: Vec<EnrolledUnit> = all_crates
.iter()
.filter_map(|c| {
let files = resolve_version_files(Some(c), Some(config));
(!files.is_empty()).then(|| EnrolledUnit {
label: format!("crate '{}'", c.name),
path: c.path.clone(),
files,
is_lockstep_root: false,
})
})
.collect();
if all_crates.is_empty() && !top_level.is_empty() {
units.push(EnrolledUnit {
label: "workspace".to_string(),
path: ".".to_string(),
files: top_level.to_vec(),
is_lockstep_root: true,
});
}
units
}
fn unit_reference_version(
repo_root: &Path,
unit: &EnrolledUnit,
ws: Option<&WorkspaceInfo>,
) -> Result<String> {
let unit_dir = repo_root.join(&unit.path);
if let Some(ws) = ws
&& let Some(member) = ws.members.iter().find(|m| member_matches(m, &unit_dir))
{
return resolve_member_version(member, ws);
}
if unit.is_lockstep_root {
return ws
.and_then(|ws| ws.workspace_package_version.clone())
.with_context(|| {
format!(
"lockstep workspace at {} has no [workspace.package].version",
unit_dir.display()
)
});
}
let own = read_cargo_version(&unit_dir.to_string_lossy())?;
if own == "0.0.0"
&& let Some(ws_version) = ws.and_then(|w| w.workspace_package_version.clone())
{
return Ok(ws_version);
}
Ok(own)
}
fn member_matches(member: &MemberInfo, unit_dir: &Path) -> bool {
match (member.crate_dir.canonicalize(), unit_dir.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => member.crate_dir == unit_dir,
}
}