intentional_core/
status.rs1use crate::adapters::{
9 CargoAdapter, FormatAdapter, JsonFormat, MsbuildAdapter, NpmAdapter, PubAdapter, PythonAdapter,
10 TomlFormat, YamlFormat,
11};
12use crate::config::Config;
13use crate::error::Result;
14use crate::intent::Intent;
15use crate::model::{Adapter, Bump, ProjectionMode};
16use crate::version::{aggregate_bumps, effective_bumps, PackageVersion, VersionRepository};
17use semver::Version;
18use std::path::Path;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct PackageStatus {
23 pub id: String,
25 pub current: Version,
27 pub next: Version,
29 pub bump: Bump,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct WorkspaceStatus {
36 pub intents: Vec<String>,
38 pub packages: Vec<PackageStatus>,
40 pub drift: Vec<Drift>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Drift {
47 pub package: String,
49 pub file: std::path::PathBuf,
51 pub expected: String,
53 pub actual: String,
55}
56
57impl WorkspaceStatus {
58 pub fn load(root: &Path) -> Result<Self> {
60 let config = Config::load(root)?;
61 let intents = Intent::load_all(root, &config)?;
62 Self::compute(root, &config, &intents)
63 }
64
65 pub fn compute(root: &Path, config: &Config, intents: &[Intent]) -> Result<Self> {
67 let repository = VersionRepository::discover(root)?;
68 let declared = aggregate_bumps(intents.iter().map(|intent| &intent.packages));
69 let bumps = effective_bumps(config, &declared);
70 let mut packages = Vec::with_capacity(config.packages.len());
71 let mut drift = Vec::new();
72 for (id, package) in &config.packages {
73 let current = repository.current_version(id, &package.tag)?;
74 let versions = PackageVersion::new(current, bumps[id]);
75 let expected = versions.current.to_string();
76 packages.push(PackageStatus {
77 id: id.clone(),
78 current: versions.current,
79 next: versions.next,
80 bump: versions.bump,
81 });
82 for projection in &package.projections {
83 if projection.mode == ProjectionMode::None || projection.adapter == Adapter::Go {
84 continue;
85 }
86 let relative = package.path.join(&projection.file);
87 let text = std::fs::read_to_string(root.join(&relative))
88 .map_err(|error| crate::Error::io(root.join(&relative), error))?;
89 let actual = read_projection_version(root, &relative, projection, &text)?;
90 if actual != expected {
91 drift.push(Drift {
92 package: id.clone(),
93 file: relative,
94 expected: expected.clone(),
95 actual,
96 });
97 }
98 }
99 }
100 Ok(Self {
101 intents: intents.iter().map(|intent| intent.id.clone()).collect(),
102 packages,
103 drift,
104 })
105 }
106}
107
108fn read_projection_version(
109 root: &Path,
110 relative: &Path,
111 projection: &crate::Projection,
112 text: &str,
113) -> Result<String> {
114 match projection.adapter {
115 Adapter::Npm => NpmAdapter.version(text),
116 Adapter::Cargo => match CargoAdapter.version(text)? {
117 Some(version) => Ok(version),
118 None => {
119 let workspace_path = crate::apply::workspace_manifest(root, relative)?;
120 let workspace = std::fs::read_to_string(root.join(&workspace_path))
121 .map_err(|error| crate::Error::io(root.join(&workspace_path), error))?;
122 TomlFormat.read_text(&workspace, "/workspace/package/version")
123 }
124 },
125 Adapter::Pub => PubAdapter.version(text),
126 Adapter::Python => PythonAdapter.version(text),
127 Adapter::Msbuild => MsbuildAdapter.version(text),
128 Adapter::Json => JsonFormat.read_text(
129 text,
130 projection.pointer.as_deref().expect("validated pointer"),
131 ),
132 Adapter::Toml => TomlFormat.read_text(
133 text,
134 projection.pointer.as_deref().expect("validated pointer"),
135 ),
136 Adapter::Yaml => YamlFormat.read_text(
137 text,
138 projection.pointer.as_deref().expect("validated pointer"),
139 ),
140 Adapter::Go => unreachable!("Go has no manifest version"),
141 }
142}