embeddenator_workspace/
version.rs1use anyhow::{Context, Result};
4use semver::Version;
5use std::collections::HashMap;
6use std::path::Path;
7
8use crate::cargo::CargoManifest;
9use crate::workspace::WorkspaceScanner;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum BumpType {
14 Major,
15 Minor,
16 Patch,
17 Prerelease,
18}
19
20pub struct VersionManager {
22 scanner: WorkspaceScanner,
23}
24
25impl VersionManager {
26 pub fn new(workspace_root: impl AsRef<Path>) -> Self {
28 Self {
29 scanner: WorkspaceScanner::new(workspace_root),
30 }
31 }
32
33 pub fn bump_versions(&self, bump_type: BumpType, dry_run: bool) -> Result<Vec<VersionChange>> {
35 let mut manifests = self
36 .scanner
37 .find_embeddenator_packages()
38 .context("Failed to find packages")?;
39
40 if manifests.is_empty() {
41 anyhow::bail!("No embeddenator packages found in workspace");
42 }
43
44 let mut changes = Vec::new();
45
46 for manifest in &mut manifests {
48 let old_version = manifest.version.clone();
49 let new_version = self.calculate_new_version(&old_version, bump_type)?;
50
51 changes.push(VersionChange {
52 package: manifest.package_name.clone(),
53 path: manifest.path.clone(),
54 old_version: old_version.clone(),
55 new_version: new_version.clone(),
56 });
57
58 if !dry_run {
59 manifest.set_version(&new_version)?;
60 }
61 }
62
63 if !dry_run {
65 self.update_dependencies(&mut manifests, &changes)?;
66
67 for manifest in manifests {
69 manifest.save()?;
70 }
71 }
72
73 Ok(changes)
74 }
75
76 fn calculate_new_version(&self, current: &Version, bump_type: BumpType) -> Result<Version> {
77 let mut new_version = current.clone();
78
79 match bump_type {
80 BumpType::Major => {
81 new_version.major += 1;
82 new_version.minor = 0;
83 new_version.patch = 0;
84 new_version.pre = semver::Prerelease::EMPTY;
85 }
86 BumpType::Minor => {
87 new_version.minor += 1;
88 new_version.patch = 0;
89 new_version.pre = semver::Prerelease::EMPTY;
90 }
91 BumpType::Patch => {
92 new_version.patch += 1;
93 new_version.pre = semver::Prerelease::EMPTY;
94 }
95 BumpType::Prerelease => {
96 if new_version.pre.is_empty() {
97 new_version.pre = "alpha.1".parse()?;
99 } else {
100 let pre_str = new_version.pre.as_str();
102
103 if let Some((prefix, num_str)) = pre_str.rsplit_once('.') {
105 if let Ok(num) = num_str.parse::<u64>() {
106 new_version.pre = format!("{}.{}", prefix, num + 1).parse()?;
107 } else {
108 new_version.pre = format!("{}.1", pre_str).parse()?;
110 }
111 } else {
112 new_version.pre = format!("{}.1", pre_str).parse()?;
114 }
115 }
116 }
117 }
118
119 Ok(new_version)
120 }
121
122 fn update_dependencies(
123 &self,
124 manifests: &mut [CargoManifest],
125 changes: &[VersionChange],
126 ) -> Result<()> {
127 let version_map: HashMap<String, Version> = changes
128 .iter()
129 .map(|c| (c.package.clone(), c.new_version.clone()))
130 .collect();
131
132 for manifest in manifests {
133 let deps_to_update: Vec<(String, Version)> = manifest
135 .embeddenator_dependencies()
136 .iter()
137 .filter_map(|dep| {
138 version_map
139 .get(&dep.name)
140 .map(|new_version| (dep.name.clone(), new_version.clone()))
141 })
142 .collect();
143
144 for (dep_name, new_version) in deps_to_update {
146 manifest.update_dependency(&dep_name, &new_version)?;
147 }
148 }
149
150 Ok(())
151 }
152
153 pub fn check_consistency(&self) -> Result<VersionReport> {
155 let manifests = self
156 .scanner
157 .find_embeddenator_packages()
158 .context("Failed to find packages")?;
159
160 let mut report = VersionReport::default();
161
162 let package_versions: HashMap<String, Version> = manifests
164 .iter()
165 .map(|m| (m.package_name.clone(), m.version.clone()))
166 .collect();
167
168 let mut versions_by_major: HashMap<u64, Vec<&str>> = HashMap::new();
170 for (name, version) in &package_versions {
171 versions_by_major
172 .entry(version.major)
173 .or_default()
174 .push(name.as_str());
175 }
176
177 if versions_by_major.len() > 1 {
178 report.drift_detected = true;
179 for (major, packages) in versions_by_major {
180 report.issues.push(format!(
181 "Version drift: {} package(s) on major version {}: {}",
182 packages.len(),
183 major,
184 packages.join(", ")
185 ));
186 }
187 }
188
189 for manifest in &manifests {
191 for dep in manifest.embeddenator_dependencies() {
192 if let Some(dep_version) = &dep.version {
193 if let Some(actual_version) = package_versions.get(&dep.name) {
194 if dep_version != actual_version {
195 report.inconsistencies.push(VersionInconsistency {
196 package: manifest.package_name.clone(),
197 dependency: dep.name.clone(),
198 expected: actual_version.clone(),
199 found: dep_version.clone(),
200 });
201 }
202 }
203 }
204 }
205 }
206
207 report.total_packages = manifests.len();
208 Ok(report)
209 }
210}
211
212#[derive(Debug, Clone)]
214pub struct VersionChange {
215 pub package: String,
216 pub path: std::path::PathBuf,
217 pub old_version: Version,
218 pub new_version: Version,
219}
220
221#[derive(Debug, Default)]
223pub struct VersionReport {
224 pub total_packages: usize,
225 pub drift_detected: bool,
226 pub issues: Vec<String>,
227 pub inconsistencies: Vec<VersionInconsistency>,
228}
229
230#[derive(Debug, Clone)]
231pub struct VersionInconsistency {
232 pub package: String,
233 pub dependency: String,
234 pub expected: Version,
235 pub found: Version,
236}
237
238impl VersionReport {
239 pub fn has_issues(&self) -> bool {
240 self.drift_detected || !self.inconsistencies.is_empty()
241 }
242}
243
244#[cfg(test)]
245#[path = "version_tests.rs"]
246mod tests;