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 =
130 get_next_prerelease(version.prerelease.as_ref(), PRERELEASE_ALPHA);
131 Version {
132 major: version.major,
133 minor: version.minor,
134 patch: version.patch,
135 prerelease: Some(prerelease),
136 }
137 }
138 BumpVersionType::Beta => {
139 let prerelease: String =
140 get_next_prerelease(version.prerelease.as_ref(), PRERELEASE_BETA);
141 Version {
142 major: version.major,
143 minor: version.minor,
144 patch: version.patch,
145 prerelease: Some(prerelease),
146 }
147 }
148 BumpVersionType::Rc => {
149 let prerelease: String = get_next_prerelease(version.prerelease.as_ref(), "rc");
150 Version {
151 major: version.major,
152 minor: version.minor,
153 patch: version.patch,
154 prerelease: Some(prerelease),
155 }
156 }
157 }
158}
159
160fn bump_version_str(version_str: &str, bump_type: &BumpVersionType) -> Option<String> {
171 let version: Version = parse_version(version_str)?;
172 Some(version_to_string(&bump_version(&version, bump_type)))
173}
174
175fn expand_member_entry(root_dir: &Path, entry: &str) -> Vec<PathBuf> {
190 match entry.strip_suffix("/*") {
191 Some(prefix) => {
192 let mut dirs: Vec<PathBuf> = Vec::new();
193 if let Ok(entries) = std::fs::read_dir(root_dir.join(prefix)) {
194 for entry in entries.flatten() {
195 let path: PathBuf = entry.path();
196 if path.is_dir() && path.join(CARGO_TOML).exists() {
197 dirs.push(path);
198 }
199 }
200 }
201 dirs.sort();
202 dirs
203 }
204 None => vec![root_dir.join(entry)],
205 }
206}
207
208fn realign_dep_versions(
225 deps: &mut dyn TableLike,
226 base_dir: &Path,
227 bumped: &[(PathBuf, String)],
228) -> bool {
229 let mut changed: bool = false;
230 for (_alias, entry) in deps.iter_mut() {
231 let Some(dep_path) = entry
232 .get(TOML_PATH)
233 .and_then(|path_item: &Item| path_item.as_str())
234 else {
235 continue;
236 };
237 let Ok(canonical) = base_dir.join(dep_path).canonicalize() else {
238 continue;
239 };
240 let Some((_, new_version)) = bumped
241 .iter()
242 .find(|(dir, _bumped_version): &&(PathBuf, String)| *dir == canonical)
243 else {
244 continue;
245 };
246 if entry.get(TOML_VERSION).is_none() {
247 continue;
248 }
249 let Some(version_slot) = entry.get_mut(TOML_VERSION) else {
250 continue;
251 };
252 if version_slot.as_str() == Some(new_version.as_str()) {
253 continue;
254 }
255 set_item_string_preserving_decor(version_slot, new_version);
256 changed = true;
257 }
258 changed
259}
260
261async fn realign_member_manifest(
275 member_dir: &Path,
276 member_manifest_path: &Path,
277 bumped: &[(PathBuf, String)],
278) -> Result<(), Box<dyn std::error::Error>> {
279 let member_content: String = read_to_string(member_manifest_path).await?;
280 let mut member_doc: DocumentMut = member_content.parse().map_err(|e: TomlError| {
281 format!("failed to parse {}: {}", member_manifest_path.display(), e)
282 })?;
283 let mut changed: bool = false;
284 for section in [
285 TOML_DEPENDENCIES,
286 TOML_DEV_DEPENDENCIES,
287 TOML_BUILD_DEPENDENCIES,
288 ] {
289 if let Some(deps) = member_doc
290 .get_mut(section)
291 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
292 {
293 changed |= realign_dep_versions(deps, member_dir, bumped);
294 }
295 }
296 if let Some(targets) = member_doc
297 .get_mut(TOML_TARGET)
298 .and_then(|target_item: &mut Item| target_item.as_table_like_mut())
299 {
300 let target_keys: Vec<String> = targets
301 .iter()
302 .map(|(key, _target_value): (&str, &Item)| key.to_string())
303 .collect();
304 for target_key in target_keys {
305 let Some(target_table) = targets
306 .get_mut(&target_key)
307 .and_then(|target_item: &mut Item| target_item.as_table_like_mut())
308 else {
309 continue;
310 };
311 for section in [
312 TOML_DEPENDENCIES,
313 TOML_DEV_DEPENDENCIES,
314 TOML_BUILD_DEPENDENCIES,
315 ] {
316 if let Some(deps) = target_table
317 .get_mut(section)
318 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
319 {
320 changed |= realign_dep_versions(deps, member_dir, bumped);
321 }
322 }
323 }
324 }
325 if changed {
326 write(member_manifest_path, member_doc.to_string()).await?;
327 }
328 Ok(())
329}
330
331async fn bump_workspace_members(
349 root_path: &Path,
350 doc: &mut DocumentMut,
351 bump_type: &BumpVersionType,
352) -> Result<String, Box<dyn std::error::Error>> {
353 let root_dir: &Path = root_path.parent().unwrap_or_else(|| Path::new("."));
354 let member_entries: Vec<String> = doc
355 .get(TOML_WORKSPACE)
356 .and_then(|workspace: &Item| workspace.get(TOML_MEMBERS))
357 .and_then(|members_item: &Item| members_item.as_array())
358 .map(|members: &toml_edit::Array| {
359 members
360 .iter()
361 .filter_map(|member: &toml_edit::Value| {
362 member.as_str().map(|s: &str| s.to_string())
363 })
364 .collect()
365 })
366 .unwrap_or_default();
367 let mut member_dirs: Vec<PathBuf> = Vec::new();
368 for entry in &member_entries {
369 member_dirs.extend(expand_member_entry(root_dir, entry));
370 }
371 member_dirs.sort();
372 member_dirs.dedup();
373 let mut bumped: Vec<(PathBuf, String)> = Vec::new();
374 for dir in &member_dirs {
375 let member_manifest_path: PathBuf = dir.join(CARGO_TOML);
376 if !member_manifest_path.exists() {
377 return Err(format!(
378 "member manifest not found: {}",
379 member_manifest_path.display()
380 )
381 .into());
382 }
383 let member_content: String = read_to_string(&member_manifest_path).await?;
384 let mut member_doc: DocumentMut = member_content.parse().map_err(|e: TomlError| {
385 format!("failed to parse {}: {}", member_manifest_path.display(), e)
386 })?;
387 let version_slot: &mut Item = member_doc
388 .get_mut(TOML_PACKAGE)
389 .and_then(|package: &mut Item| package.get_mut(TOML_VERSION))
390 .ok_or_else(|| -> Box<dyn std::error::Error> {
391 format!(
392 "package.version not found in {}",
393 member_manifest_path.display()
394 )
395 .into()
396 })?;
397 let Some(old_version) = version_slot.as_str().map(|s: &str| s.to_string()) else {
398 continue;
399 };
400 let new_version: String = bump_version_str(&old_version, bump_type).ok_or_else(
401 || -> Box<dyn std::error::Error> {
402 format!("failed to parse version: {}", old_version).into()
403 },
404 )?;
405 set_item_string_preserving_decor(version_slot, &new_version);
406 write(&member_manifest_path, member_doc.to_string()).await?;
407 log::info!(
408 "bump: {} -> {}",
409 member_manifest_path.display(),
410 new_version
411 );
412 bumped.push((dir.canonicalize()?, new_version));
413 }
414 if bumped.is_empty() {
415 return Ok(REPORT_ZERO_WORKSPACE_MEMBERS.to_string());
416 }
417 let root_changed: bool = doc
418 .get_mut(TOML_WORKSPACE)
419 .and_then(|workspace: &mut Item| workspace.get_mut(TOML_DEPENDENCIES))
420 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
421 .is_some_and(|deps: &mut dyn TableLike| realign_dep_versions(deps, root_dir, &bumped));
422 if root_changed {
423 write(root_path, doc.to_string()).await?;
424 }
425 for dir in &member_dirs {
426 realign_member_manifest(dir, &dir.join(CARGO_TOML), &bumped).await?;
427 }
428 Ok(format!("{} workspace members", bumped.len()))
429}
430
431pub async fn execute_bump(
457 manifest_path: &str,
458 bump_type: &BumpVersionType,
459) -> Result<String, Box<dyn std::error::Error>> {
460 let path: &Path = Path::new(manifest_path);
461 let content: String = read_to_string(path).await?;
462 let mut doc: DocumentMut = content
463 .parse()
464 .map_err(|e: TomlError| format!("failed to parse {}: {}", manifest_path, e))?;
465 let has_workspace_version: bool = doc
466 .get(TOML_WORKSPACE)
467 .and_then(|workspace: &Item| workspace.get(TOML_PACKAGE))
468 .and_then(|package: &Item| package.get(TOML_VERSION))
469 .is_some();
470 let has_root_package: bool = doc.get(TOML_PACKAGE).is_some();
471 let has_members: bool = doc
472 .get(TOML_WORKSPACE)
473 .and_then(|workspace: &Item| workspace.get(TOML_MEMBERS))
474 .and_then(|members_item: &Item| members_item.as_array())
475 .is_some_and(|members: &toml_edit::Array| !members.is_empty());
476 if !has_workspace_version && !has_root_package && has_members {
477 return bump_workspace_members(path, &mut doc, bump_type).await;
478 }
479 let version_slot: &mut Item = if has_workspace_version {
480 doc.get_mut(TOML_WORKSPACE)
481 .and_then(|workspace: &mut Item| workspace.get_mut(TOML_PACKAGE))
482 .and_then(|package: &mut Item| package.get_mut(TOML_VERSION))
483 .ok_or_else(|| -> Box<dyn std::error::Error> {
484 ERROR_WORKSPACE_PACKAGE_VERSION_MISSING.into()
485 })?
486 } else if has_root_package {
487 doc.get_mut(TOML_PACKAGE)
488 .and_then(|package: &mut Item| package.get_mut(TOML_VERSION))
489 .ok_or_else(|| -> Box<dyn std::error::Error> { ERROR_PACKAGE_VERSION_MISSING.into() })?
490 } else {
491 return Err(ERROR_NO_VERSION_SLOT.into());
492 };
493 let version_str: String = version_slot
494 .as_str()
495 .ok_or_else(|| -> Box<dyn std::error::Error> { ERROR_VERSION_NOT_STRING.into() })?
496 .to_string();
497 let version: Version =
498 parse_version(&version_str).ok_or_else(|| -> Box<dyn std::error::Error> {
499 format!("failed to parse version: {}", version_str).into()
500 })?;
501 let bumped: Version = bump_version(&version, bump_type);
502 let version_string: String = version_to_string(&bumped);
503 set_item_string_preserving_decor(version_slot, &version_string);
504 write(path, doc.to_string()).await?;
505 Ok(version_string)
506}