1use super::*;
2
3fn parse_version(version_str: &str) -> Option<Version> {
13 let parts: Vec<&str> = version_str.split('-').collect();
14 let version_part: &str = parts.first()?;
15 let prerelease: Option<String> = parts.get(1).map(|s: &&str| s.to_string());
16 let nums: Vec<&str> = version_part.split('.').collect();
17 if nums.len() != 3 {
18 return None;
19 }
20 let major: u64 = nums.first()?.parse().ok()?;
21 let minor: u64 = nums.get(1)?.parse().ok()?;
22 let patch: u64 = nums.get(2)?.parse().ok()?;
23 Some(Version {
24 major,
25 minor,
26 patch,
27 prerelease,
28 })
29}
30
31fn parse_prerelease(prerelease: &str) -> Option<(&str, u64)> {
41 let parts: Vec<&str> = prerelease.split('.').collect();
42 let pre_type: &str = parts.first()?;
43 let number: u64 = parts
44 .get(1)
45 .and_then(|s: &&str| s.parse().ok())
46 .unwrap_or(0);
47 Some((pre_type, number))
48}
49
50fn get_next_prerelease(current: Option<&String>, target_type: &str) -> String {
61 match current {
62 Some(pre) => {
63 if let Some((pre_type, number)) = parse_prerelease(pre)
64 && pre_type == target_type
65 && number > 0
66 {
67 return format!("{}.{}", target_type, number + 1);
68 }
69 format!("{target_type}.1")
70 }
71 None => target_type.to_string(),
72 }
73}
74
75fn version_to_string(version: &Version) -> String {
85 let base: String = format!("{}.{}.{}", version.major, version.minor, version.patch);
86 match &version.prerelease {
87 Some(pre) => format!("{base}-{pre}"),
88 None => base,
89 }
90}
91
92fn bump_version(version: &Version, bump_type: &BumpVersionType) -> Version {
103 match bump_type {
104 BumpVersionType::Patch => Version {
105 major: version.major,
106 minor: version.minor,
107 patch: version.patch + 1,
108 prerelease: None,
109 },
110 BumpVersionType::Minor => Version {
111 major: version.major,
112 minor: version.minor + 1,
113 patch: 0,
114 prerelease: None,
115 },
116 BumpVersionType::Major => Version {
117 major: version.major + 1,
118 minor: 0,
119 patch: 0,
120 prerelease: None,
121 },
122 BumpVersionType::Release => Version {
123 major: version.major,
124 minor: version.minor,
125 patch: version.patch,
126 prerelease: None,
127 },
128 BumpVersionType::Alpha => {
129 let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "alpha");
130 Version {
131 major: version.major,
132 minor: version.minor,
133 patch: version.patch,
134 prerelease: Some(prerelease),
135 }
136 }
137 BumpVersionType::Beta => {
138 let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "beta");
139 Version {
140 major: version.major,
141 minor: version.minor,
142 patch: version.patch,
143 prerelease: Some(prerelease),
144 }
145 }
146 BumpVersionType::Rc => {
147 let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "rc");
148 Version {
149 major: version.major,
150 minor: version.minor,
151 patch: version.patch,
152 prerelease: Some(prerelease),
153 }
154 }
155 }
156}
157
158fn bump_version_str(version_str: &str, bump_type: &BumpVersionType) -> Option<String> {
169 let version: Version = parse_version(version_str)?;
170 Some(version_to_string(&bump_version(&version, bump_type)))
171}
172
173fn expand_member_entry(root_dir: &Path, entry: &str) -> Vec<PathBuf> {
188 match entry.strip_suffix("/*") {
189 Some(prefix) => {
190 let mut dirs: Vec<PathBuf> = Vec::new();
191 if let Ok(entries) = std::fs::read_dir(root_dir.join(prefix)) {
192 for entry in entries.flatten() {
193 let path: PathBuf = entry.path();
194 if path.is_dir() && path.join("Cargo.toml").exists() {
195 dirs.push(path);
196 }
197 }
198 }
199 dirs.sort();
200 dirs
201 }
202 None => vec![root_dir.join(entry)],
203 }
204}
205
206fn realign_dep_versions(
223 deps: &mut dyn TableLike,
224 base_dir: &Path,
225 bumped: &[(PathBuf, String)],
226) -> bool {
227 let mut changed: bool = false;
228 for (_alias, entry) in deps.iter_mut() {
229 let Some(dep_path) = entry
230 .get("path")
231 .and_then(|path_item: &Item| path_item.as_str())
232 else {
233 continue;
234 };
235 let Ok(canonical) = base_dir.join(dep_path).canonicalize() else {
236 continue;
237 };
238 let Some((_, new_version)) = bumped.iter().find(|(dir, _)| *dir == canonical) else {
239 continue;
240 };
241 if entry.get("version").is_none() {
242 continue;
243 }
244 let Some(version_slot) = entry.get_mut("version") else {
245 continue;
246 };
247 if version_slot.as_str() == Some(new_version.as_str()) {
248 continue;
249 }
250 set_item_string_preserving_decor(version_slot, new_version);
251 changed = true;
252 }
253 changed
254}
255
256async fn realign_member_manifest(
266 member_dir: &Path,
267 member_manifest_path: &Path,
268 bumped: &[(PathBuf, String)],
269) -> Result<(), Box<dyn std::error::Error>> {
270 let member_content: String = read_to_string(member_manifest_path).await?;
271 let mut member_doc: DocumentMut = member_content.parse().map_err(|e: TomlError| {
272 format!("failed to parse {}: {}", member_manifest_path.display(), e)
273 })?;
274 let mut changed: bool = false;
275 for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
276 if let Some(deps) = member_doc
277 .get_mut(section)
278 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
279 {
280 changed |= realign_dep_versions(deps, member_dir, bumped);
281 }
282 }
283 if let Some(targets) = member_doc
284 .get_mut("target")
285 .and_then(|target_item: &mut Item| target_item.as_table_like_mut())
286 {
287 let target_keys: Vec<String> = targets.iter().map(|(key, _)| key.to_string()).collect();
288 for target_key in target_keys {
289 let Some(target_table) = targets
290 .get_mut(&target_key)
291 .and_then(|target_item: &mut Item| target_item.as_table_like_mut())
292 else {
293 continue;
294 };
295 for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
296 if let Some(deps) = target_table
297 .get_mut(section)
298 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
299 {
300 changed |= realign_dep_versions(deps, member_dir, bumped);
301 }
302 }
303 }
304 }
305 if changed {
306 write(member_manifest_path, member_doc.to_string()).await?;
307 }
308 Ok(())
309}
310
311async fn bump_workspace_members(
329 root_path: &Path,
330 doc: &mut DocumentMut,
331 bump_type: &BumpVersionType,
332) -> Result<String, Box<dyn std::error::Error>> {
333 let root_dir: &Path = root_path.parent().unwrap_or_else(|| Path::new("."));
334 let member_entries: Vec<String> = doc
335 .get("workspace")
336 .and_then(|workspace: &Item| workspace.get("members"))
337 .and_then(|members_item: &Item| members_item.as_array())
338 .map(|members: &toml_edit::Array| {
339 members
340 .iter()
341 .filter_map(|member: &TomlEditValue| member.as_str().map(|s: &str| s.to_string()))
342 .collect()
343 })
344 .unwrap_or_default();
345 let mut member_dirs: Vec<PathBuf> = Vec::new();
346 for entry in &member_entries {
347 member_dirs.extend(expand_member_entry(root_dir, entry));
348 }
349 member_dirs.sort();
350 member_dirs.dedup();
351 let mut bumped: Vec<(PathBuf, String)> = Vec::new();
352 for dir in &member_dirs {
353 let member_manifest_path: PathBuf = dir.join("Cargo.toml");
354 if !member_manifest_path.exists() {
355 return Err(format!(
356 "member manifest not found: {}",
357 member_manifest_path.display()
358 )
359 .into());
360 }
361 let member_content: String = read_to_string(&member_manifest_path).await?;
362 let mut member_doc: DocumentMut = member_content.parse().map_err(|e: TomlError| {
363 format!("failed to parse {}: {}", member_manifest_path.display(), e)
364 })?;
365 let version_slot: &mut Item = member_doc
366 .get_mut("package")
367 .and_then(|package: &mut Item| package.get_mut("version"))
368 .ok_or_else(|| -> Box<dyn std::error::Error> {
369 format!(
370 "package.version not found in {}",
371 member_manifest_path.display()
372 )
373 .into()
374 })?;
375 let Some(old_version) = version_slot.as_str().map(|s: &str| s.to_string()) else {
376 continue;
377 };
378 let new_version: String = bump_version_str(&old_version, bump_type).ok_or_else(
379 || -> Box<dyn std::error::Error> {
380 format!("failed to parse version: {}", old_version).into()
381 },
382 )?;
383 set_item_string_preserving_decor(version_slot, &new_version);
384 write(&member_manifest_path, member_doc.to_string()).await?;
385 log::info!(
386 "bump: {} -> {}",
387 member_manifest_path.display(),
388 new_version
389 );
390 bumped.push((dir.canonicalize()?, new_version));
391 }
392 if bumped.is_empty() {
393 return Ok("0 workspace members".to_string());
394 }
395 let root_changed: bool = doc
396 .get_mut("workspace")
397 .and_then(|workspace: &mut Item| workspace.get_mut("dependencies"))
398 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
399 .is_some_and(|deps: &mut dyn TableLike| realign_dep_versions(deps, root_dir, &bumped));
400 if root_changed {
401 write(root_path, doc.to_string()).await?;
402 }
403 for dir in &member_dirs {
404 realign_member_manifest(dir, &dir.join("Cargo.toml"), &bumped).await?;
405 }
406 Ok(format!("{} workspace members", bumped.len()))
407}
408
409pub async fn execute_bump(
435 manifest_path: &str,
436 bump_type: &BumpVersionType,
437) -> Result<String, Box<dyn std::error::Error>> {
438 let path: &Path = Path::new(manifest_path);
439 let content: String = read_to_string(path).await?;
440 let mut doc: DocumentMut = content
441 .parse()
442 .map_err(|e: TomlError| format!("failed to parse {}: {}", manifest_path, e))?;
443 let has_workspace_version: bool = doc
444 .get("workspace")
445 .and_then(|workspace: &Item| workspace.get("package"))
446 .and_then(|package: &Item| package.get("version"))
447 .is_some();
448 let has_root_package: bool = doc.get("package").is_some();
449 let has_members: bool = doc
450 .get("workspace")
451 .and_then(|workspace: &Item| workspace.get("members"))
452 .and_then(|members_item: &Item| members_item.as_array())
453 .is_some_and(|members: &toml_edit::Array| !members.is_empty());
454 if !has_workspace_version && !has_root_package && has_members {
455 return bump_workspace_members(path, &mut doc, bump_type).await;
456 }
457 let version_slot: &mut Item = if has_workspace_version {
458 doc.get_mut("workspace")
459 .and_then(|workspace: &mut Item| workspace.get_mut("package"))
460 .and_then(|package: &mut Item| package.get_mut("version"))
461 .ok_or_else(|| -> Box<dyn std::error::Error> {
462 "workspace.package.version not found".into()
463 })?
464 } else if has_root_package {
465 doc.get_mut("package")
466 .and_then(|package: &mut Item| package.get_mut("version"))
467 .ok_or_else(|| -> Box<dyn std::error::Error> { "package.version not found".into() })?
468 } else {
469 return Err("neither [package] nor [workspace.package] found in Cargo.toml".into());
470 };
471 let version_str: String = version_slot
472 .as_str()
473 .ok_or_else(|| -> Box<dyn std::error::Error> { "version field is not a string".into() })?
474 .to_string();
475 let version: Version =
476 parse_version(&version_str).ok_or_else(|| -> Box<dyn std::error::Error> {
477 format!("failed to parse version: {}", version_str).into()
478 })?;
479 let bumped: Version = bump_version(&version, bump_type);
480 let version_string: String = version_to_string(&bumped);
481 set_item_string_preserving_decor(version_slot, &version_string);
482 write(path, doc.to_string()).await?;
483 Ok(version_string)
484}