1use 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::ownership::check_physical_ownership;
17use crate::version::{aggregate_bumps, resolve_versions, VersionRepository};
18use crate::workspace_files::read_contained;
19use semver::Version;
20use std::path::Path;
21
22pub const MISSING_BASELINE_CODE: &str = "missing-baseline";
24
25pub const MISSING_BASELINE_NEXT_ACTION: &str = "intentional tag --baseline";
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ReleaseUnitStatus {
31 pub id: String,
33 pub current: Version,
35 pub next: Version,
37 pub bump: Bump,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct WorkspaceStatus {
44 pub intents: Vec<String>,
46 pub release_units: Vec<ReleaseUnitStatus>,
48 pub drift: Vec<Drift>,
50 pub missing_baselines: Vec<String>,
52 pub tag_record_issues: Vec<String>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct Drift {
59 pub release_unit: String,
61 pub file: std::path::PathBuf,
63 pub expected: String,
65 pub actual: String,
67}
68
69impl WorkspaceStatus {
70 pub fn load(root: &Path) -> Result<Self> {
72 let config = Config::load(root)?;
73 let intents = Intent::load_all(root, &config)?;
74 Self::compute(root, &config, &intents)
75 }
76
77 pub fn compute(root: &Path, config: &Config, intents: &[Intent]) -> Result<Self> {
79 check_physical_ownership(root, config)?;
80 let repository = VersionRepository::discover(root)?;
81 let declared = aggregate_bumps(intents.iter().map(|intent| &intent.release_units));
82 let mut current_versions = std::collections::BTreeMap::new();
83 let mut missing_baselines = Vec::new();
84 for id in config.release_units.keys() {
85 let (_, primary) = config.primary_tag(id)?;
86 let current = repository.current_version(id, &primary.template)?;
87 if !repository.has_matching_tag(id, &primary.template)? {
88 missing_baselines.push(id.clone());
89 }
90 current_versions.insert(id.clone(), current);
91 }
92 let resolved = resolve_versions(config, &declared, ¤t_versions)?;
93 let mut release_units = Vec::with_capacity(config.release_units.len());
94 let mut drift = Vec::new();
95 for (id, release_unit) in &config.release_units {
96 let versions = &resolved[id];
97 let expected = versions.current.to_string();
98 release_units.push(ReleaseUnitStatus {
99 id: id.clone(),
100 current: versions.current.clone(),
101 next: versions.next.clone(),
102 bump: versions.bump,
103 });
104 for projection in &release_unit.projections {
105 if projection.mode == ProjectionMode::None || projection.adapter == Adapter::Go {
106 continue;
107 }
108 let relative = projection.file.workspace_path(&release_unit.path);
109 let text = read_contained(root, &relative)?;
110 let actual = read_projection_version(root, &relative, projection, &text)?;
111 if actual != expected {
112 drift.push(Drift {
113 release_unit: id.clone(),
114 file: relative,
115 expected: expected.clone(),
116 actual,
117 });
118 }
119 }
120 }
121 Ok(Self {
122 intents: intents.iter().map(|intent| intent.id.clone()).collect(),
123 release_units,
124 drift,
125 missing_baselines,
126 tag_record_issues: crate::tag::tag_record_issues(root, config)?,
127 })
128 }
129}
130
131pub(crate) fn read_projection_version(
132 root: &Path,
133 relative: &Path,
134 projection: &crate::Projection,
135 text: &str,
136) -> Result<String> {
137 match projection.adapter {
138 Adapter::Npm => NpmAdapter.version(text),
139 Adapter::Cargo => match CargoAdapter.version(text)? {
140 Some(version) => Ok(version),
141 None => {
142 let workspace_path = crate::apply::workspace_manifest(root, relative)?;
143 let workspace = read_contained(root, &workspace_path)?;
144 TomlFormat.read_text(&workspace, "/workspace/package/version")
145 }
146 },
147 Adapter::Pub => PubAdapter.version(text),
148 Adapter::Python => PythonAdapter.version(text),
149 Adapter::Msbuild => MsbuildAdapter.version(text),
150 Adapter::Json => JsonFormat.read_text(
151 text,
152 projection.pointer.as_deref().expect("validated pointer"),
153 ),
154 Adapter::Toml => TomlFormat.read_text(
155 text,
156 projection.pointer.as_deref().expect("validated pointer"),
157 ),
158 Adapter::Yaml => YamlFormat.read_text(
159 text,
160 projection.pointer.as_deref().expect("validated pointer"),
161 ),
162 Adapter::Go => unreachable!("Go has no manifest version"),
163 }
164}