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) = 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 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 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 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 =
401 bump_version_str(&old_version, bump_type).ok_or_else(|| -> Box<dyn Error> {
402 format!("failed to parse version: {}", old_version).into()
403 })?;
404 set_item_string_preserving_decor(version_slot, &new_version);
405 write(&member_manifest_path, member_doc.to_string()).await?;
406 log::info!(
407 "bump: {} -> {}",
408 member_manifest_path.display(),
409 new_version
410 );
411 bumped.push((dir.canonicalize()?, new_version));
412 }
413 if bumped.is_empty() {
414 return Ok(REPORT_ZERO_WORKSPACE_MEMBERS.to_string());
415 }
416 let root_changed: bool = doc
417 .get_mut(TOML_WORKSPACE)
418 .and_then(|workspace: &mut Item| workspace.get_mut(TOML_DEPENDENCIES))
419 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
420 .is_some_and(|deps: &mut dyn TableLike| realign_dep_versions(deps, root_dir, &bumped));
421 if root_changed {
422 write(root_path, doc.to_string()).await?;
423 }
424 for dir in &member_dirs {
425 realign_member_manifest(dir, &dir.join(CARGO_TOML), &bumped).await?;
426 }
427 Ok(format!("{} workspace members", bumped.len()))
428}
429
430pub async fn execute_bump(
456 manifest_path: &str,
457 bump_type: &BumpVersionType,
458) -> Result<String, Box<dyn Error>> {
459 let path: &Path = Path::new(manifest_path);
460 let content: String = read_to_string(path).await?;
461 let mut doc: DocumentMut = content
462 .parse()
463 .map_err(|e: TomlError| format!("failed to parse {}: {}", manifest_path, e))?;
464 let has_workspace_version: bool = doc
465 .get(TOML_WORKSPACE)
466 .and_then(|workspace: &Item| workspace.get(TOML_PACKAGE))
467 .and_then(|package: &Item| package.get(TOML_VERSION))
468 .is_some();
469 let has_root_package: bool = doc.get(TOML_PACKAGE).is_some();
470 let has_members: bool = doc
471 .get(TOML_WORKSPACE)
472 .and_then(|workspace: &Item| workspace.get(TOML_MEMBERS))
473 .and_then(|members_item: &Item| members_item.as_array())
474 .is_some_and(|members: &toml_edit::Array| !members.is_empty());
475 if !has_workspace_version && !has_root_package && has_members {
476 return bump_workspace_members(path, &mut doc, bump_type).await;
477 }
478 let version_slot: &mut Item = if has_workspace_version {
479 doc.get_mut(TOML_WORKSPACE)
480 .and_then(|workspace: &mut Item| workspace.get_mut(TOML_PACKAGE))
481 .and_then(|package: &mut Item| package.get_mut(TOML_VERSION))
482 .ok_or_else(|| -> Box<dyn Error> { ERROR_WORKSPACE_PACKAGE_VERSION_MISSING.into() })?
483 } else if has_root_package {
484 doc.get_mut(TOML_PACKAGE)
485 .and_then(|package: &mut Item| package.get_mut(TOML_VERSION))
486 .ok_or_else(|| -> Box<dyn Error> { ERROR_PACKAGE_VERSION_MISSING.into() })?
487 } else {
488 return Err(ERROR_NO_VERSION_SLOT.into());
489 };
490 let version_str: String = version_slot
491 .as_str()
492 .ok_or_else(|| -> Box<dyn Error> { ERROR_VERSION_NOT_STRING.into() })?
493 .to_string();
494 let version: Version = parse_version(&version_str).ok_or_else(|| -> Box<dyn Error> {
495 format!("failed to parse version: {}", version_str).into()
496 })?;
497 let bumped: Version = bump_version(&version, bump_type);
498 let version_string: String = version_to_string(&bumped);
499 set_item_string_preserving_decor(version_slot, &version_string);
500 write(path, doc.to_string()).await?;
501 Ok(version_string)
502}