1use std::collections::HashMap;
4use std::path::PathBuf;
5
6use rayon::prelude::*;
7use semver::Version;
8
9use crate::adapter::LanguageAdapter;
10use crate::error::{Error, Result};
11use crate::graph::DependencyGraph;
12use crate::release_reporter::ReleaseReporter;
13
14type AdapterGetter =
15 Box<dyn Fn(&crate::package::Language) -> Box<dyn LanguageAdapter> + Send + Sync>;
16
17pub struct ReleaseEngine {
19 packages_dir: PathBuf,
20 graph: DependencyGraph,
21 dry_run: bool,
22 adapter_getter: AdapterGetter,
23 reporter: Box<dyn ReleaseReporter>,
24}
25
26#[derive(Debug, Clone)]
28pub struct ReleasePlan {
29 pub packages: Vec<ReleasePackage>,
31}
32
33#[derive(Debug, Clone)]
35pub struct ReleasePackage {
36 pub name: String,
38 pub old_version: Option<String>,
40 pub new_version: String,
42 pub bump_type: BumpType,
44}
45
46#[derive(Debug, Clone, Copy)]
48pub enum BumpType {
49 Major,
51 Minor,
53 Patch,
55}
56
57impl ReleaseEngine {
58 pub fn new<F, R>(
68 packages_dir: impl Into<PathBuf>,
69 graph: DependencyGraph,
70 dry_run: bool,
71 adapter_getter: F,
72 reporter: R,
73 ) -> Self
74 where
75 F: Fn(&crate::package::Language) -> Box<dyn LanguageAdapter> + Send + Sync + 'static,
76 R: ReleaseReporter + 'static,
77 {
78 Self {
79 packages_dir: packages_dir.into(),
80 graph,
81 dry_run,
82 adapter_getter: Box::new(adapter_getter),
83 reporter: Box::new(reporter),
84 }
85 }
86
87 pub fn plan_release(&self, package_name: &str, bump_type: BumpType) -> Result<ReleasePlan> {
97 let order = self.graph.topological_order();
98 let mut plan = ReleasePlan {
99 packages: Vec::new(),
100 };
101
102 let available: Vec<String> = self
103 .graph
104 .all_packages()
105 .iter()
106 .map(|p| p.name.clone())
107 .collect();
108 let available_str = available.join(", ");
109 let target_idx = order
110 .iter()
111 .position(|n| n == package_name)
112 .ok_or_else(|| Error::PackageNotFound {
113 name: package_name.to_string(),
114 available: available_str.clone(),
115 })?;
116
117 let packages_with_paths: Vec<(String, PathBuf, crate::package::Language)> = order
119 .iter()
120 .filter_map(|name| {
121 self.graph.get_package(name).map(|pkg| {
122 (
123 name.clone(),
124 self.packages_dir.join(&pkg.path),
125 pkg.language,
126 )
127 })
128 })
129 .collect();
130
131 let metadata_results: Result<Vec<(String, Option<String>)>> = packages_with_paths
133 .into_par_iter()
134 .map(|(name, path, language)| {
135 let adapter = (self.adapter_getter)(&language);
136 let metadata = adapter.read_metadata(&path)?;
137 Ok((name, metadata.version))
138 })
139 .collect();
140
141 let metadata_map: HashMap<String, Option<String>> =
142 metadata_results?.into_iter().collect::<HashMap<_, _>>();
143
144 let mut versions = HashMap::new();
145
146 for (idx, package_name) in order.iter().enumerate() {
148 let old_version =
149 metadata_map
150 .get(package_name)
151 .cloned()
152 .ok_or_else(|| Error::PackageNotFound {
153 name: package_name.clone(),
154 available: available_str.clone(),
155 })?;
156
157 let new_version = if idx == target_idx {
158 self.bump_version(&old_version, bump_type)?
159 } else if idx < target_idx {
160 let deps = self.graph.dependencies(package_name)?;
161 let current_version = old_version.as_deref().unwrap_or("0.1.0");
162 let needs_bump = deps.iter().any(|dep| {
163 versions
164 .get(dep)
165 .map(|v: &String| v != current_version)
166 .unwrap_or(false)
167 });
168
169 if needs_bump {
170 self.bump_version(&old_version, BumpType::Patch)?
171 } else {
172 old_version.clone().unwrap_or_else(|| "0.1.0".to_string())
173 }
174 } else {
175 old_version.clone().unwrap_or_else(|| "0.1.0".to_string())
176 };
177
178 versions.insert(package_name.clone(), new_version.clone());
179
180 if let Some(old) = &old_version {
181 if old != &new_version {
182 plan.packages.push(ReleasePackage {
183 name: package_name.clone(),
184 old_version: old_version.clone(),
185 new_version,
186 bump_type: if idx == target_idx {
187 bump_type
188 } else {
189 BumpType::Patch
190 },
191 });
192 }
193 } else if idx <= target_idx {
194 plan.packages.push(ReleasePackage {
195 name: package_name.clone(),
196 old_version: None,
197 new_version,
198 bump_type: if idx == target_idx {
199 bump_type
200 } else {
201 BumpType::Patch
202 },
203 });
204 }
205 }
206
207 Ok(plan)
208 }
209
210 pub fn execute_release(&self, plan: &ReleasePlan) -> Result<()> {
219 for release_pkg in &plan.packages {
220 let available: Vec<String> = self
221 .graph
222 .all_packages()
223 .iter()
224 .map(|p| p.name.clone())
225 .collect();
226 let available_str = available.join(", ");
227 let package = self.graph.get_package(&release_pkg.name).ok_or_else(|| {
228 Error::PackageNotFound {
229 name: release_pkg.name.clone(),
230 available: available_str,
231 }
232 })?;
233
234 let adapter = (self.adapter_getter)(&package.language);
235 let package_path = self.packages_dir.join(&package.path);
236
237 if !self.dry_run {
238 adapter.bump_version(&package_path, &release_pkg.new_version)?;
239 }
240
241 self.reporter.report_bump(
242 &release_pkg.name,
243 release_pkg.old_version.as_deref(),
244 &release_pkg.new_version,
245 self.dry_run,
246 );
247 }
248
249 Ok(())
250 }
251
252 fn bump_version(&self, current: &Option<String>, bump_type: BumpType) -> Result<String> {
253 let version = if let Some(v) = current {
254 Version::parse(v)
255 .map_err(|e| Error::Release(format!("Invalid version {}: {}", v, e)))?
256 } else {
257 Version::parse("0.1.0")
258 .map_err(|e| Error::Release(format!("Failed to parse default version: {}", e)))?
259 };
260
261 let new_version = match bump_type {
262 BumpType::Major => Version::new(version.major + 1, 0, 0),
263 BumpType::Minor => Version::new(version.major, version.minor + 1, 0),
264 BumpType::Patch => Version::new(version.major, version.minor, version.patch + 1),
265 };
266
267 Ok(new_version.to_string())
268 }
269}