use {
anyhow::{anyhow, Context, Result},
clap::{Args, ValueEnum},
log::{debug, info},
semver::Version,
std::{fs, process::Command},
toml_edit::{value, DocumentMut},
};
#[derive(Args)]
pub struct CommandArgs {
#[arg(value_enum)]
pub level: BumpLevel,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum BumpLevel {
#[value(help = "Bump major: x.y.z -> x+1.0.0")]
Major,
#[value(help = "Bump minor: x.y.z -> x.y+1.0")]
Minor,
#[value(help = "Bump patch: x.y.z -> x.y.z+1")]
Patch,
#[value(
help = "Bump prerelease suffix: x.y.z-<tag>.n -> x.y.z-<tag>.n+1 (e.g. alpha/beta/rc)"
)]
PreRelease,
#[value(
help = "Promote prerelease stage: alpha.n -> beta.0, beta.n -> rc.0, rc.n -> '' (removed rc prerelease)"
)]
PromotePreRelease,
#[value(
help = "Bump prerelease if present; otherwise bump patch (x.y.z-<tag>.n -> x.y.z-<tag>.n+1, x.y.z -> x.y.z+1)"
)]
PatchOrPreRelease,
}
pub fn run(args: CommandArgs) -> Result<()> {
let current_version_str =
crate::utils::get_current_version().context("failed to get current version")?;
let current_version = Version::parse(¤t_version_str)?;
let new_version = bump_version(&args.level, ¤t_version)?;
let all_crates = crate::utils::get_all_crates().context("failed to get all crates")?;
let all_cargo_tomls =
crate::utils::find_all_cargo_tomls().context("failed to find all cargo.toml files")?;
info!("found {} cargo.toml files", all_cargo_tomls.len());
for cargo_toml in all_cargo_tomls {
info!("processing {}", cargo_toml.display());
let content = fs::read_to_string(&cargo_toml)
.context(format!("failed to read {}", cargo_toml.display()))?;
let mut doc = content
.parse::<DocumentMut>()
.context(format!("failed to parse {}", cargo_toml.display()))?;
if let Some(workspace_package_version_str) = doc
.get("workspace")
.and_then(|workspace| workspace.get("package"))
.and_then(|package| package.get("version"))
.and_then(|version| version.as_str())
{
if workspace_package_version_str == current_version.to_string() {
doc["workspace"]["package"]["version"] = value(new_version.to_string());
info!(" bumped workspace.package.version from {current_version} to {new_version}",);
}
}
if let Some(package_version_str) = doc
.get("package")
.and_then(|package| package.get("version"))
.and_then(|version| version.as_str())
{
if package_version_str == current_version.to_string() {
doc["package"]["version"] = value(new_version.to_string());
info!(" bumped package.version from {current_version} to {current_version}",);
}
}
if let Some(dependencies) = doc
.get("workspace")
.and_then(|ws| ws.get("dependencies"))
.and_then(|deps| deps.as_table())
{
let keys: Vec<String> = dependencies.iter().map(|(k, _)| k.to_string()).collect();
for name in keys {
if all_crates.contains(&name) {
if let Some(version) = doc["workspace"]["dependencies"]
.get(&name)
.and_then(|v| v.get("version"))
.and_then(|v| v.as_str())
{
if !version.contains(¤t_version.to_string()) {
continue;
}
let old_version = version.to_string();
let new_version = old_version
.replace(¤t_version.to_string(), &new_version.to_string());
doc["workspace"]["dependencies"][&name]["version"] = value(&new_version);
info!(
" bumped workspace.dependencies.{name}.version from {old_version} to \
{new_version}",
);
}
}
}
}
debug!("writing {}", cargo_toml.display());
fs::write(&cargo_toml, doc.to_string())
.context(format!("failed to write {}", cargo_toml.display()))?;
}
let all_cargo_locks =
crate::utils::find_all_cargo_locks().context("failed to find all Cargo.lock files")?;
info!("found {} Cargo.lock files", all_cargo_locks.len());
for cargo_lock in all_cargo_locks {
let dir = cargo_lock.parent().context(format!(
"failed to get {}'s parent directory",
cargo_lock.display()
))?;
info!("running `cargo tree` in {}", dir.display());
let output = Command::new("cargo")
.arg("tree")
.current_dir(dir)
.output()
.context(format!("failed to run `cargo tree` in {}", dir.display()))?;
if !output.status.success() {
return Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr)));
}
}
Ok(())
}
pub fn bump_version(level: &BumpLevel, current: &Version) -> Result<Version> {
let mut new_version = current.clone();
match level {
BumpLevel::Major => {
new_version.major = new_version.major.saturating_add(1);
new_version.minor = 0;
new_version.patch = 0;
}
BumpLevel::Minor => {
new_version.minor = new_version.minor.saturating_add(1);
new_version.patch = 0;
}
BumpLevel::Patch => {
new_version.patch = new_version.patch.saturating_add(1);
}
BumpLevel::PreRelease => {
if let Some((prefix, number_str)) = current.pre.as_str().split_once('.') {
if let Ok(number) = number_str.parse::<u64>() {
let next = number.saturating_add(1);
if let Ok(next_pre) = semver::Prerelease::new(&format!("{prefix}.{next}")) {
new_version.pre = next_pre;
}
} else {
return Err(anyhow!("unexpected prerelease format: {}", current.pre));
}
} else {
return Err(anyhow!("unexpected prerelease format: {}", current.pre));
}
}
BumpLevel::PromotePreRelease => {
if let Some((prefix, _)) = current.pre.as_str().split_once('.') {
match prefix {
"alpha" => {
new_version.pre = semver::Prerelease::new("beta.0").unwrap();
}
"beta" => {
new_version.pre = semver::Prerelease::new("rc.0").unwrap();
}
"rc" => {
new_version.pre = semver::Prerelease::new("").unwrap();
}
_ => {
return Err(anyhow!("unexpected prerelease format: {}, only alpha, beta, and rc are supported", current.pre));
}
}
} else {
return Err(anyhow!("unexpected prerelease format: {}", current.pre));
}
}
BumpLevel::PatchOrPreRelease => {
if current.pre.is_empty() {
new_version = bump_version(&BumpLevel::Patch, current)?;
} else {
new_version = bump_version(&BumpLevel::PreRelease, current)?;
}
}
}
Ok(new_version)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bump_version_major() {
assert_eq!(
bump_version(&BumpLevel::Major, &Version::parse("1.0.0").unwrap()).unwrap(),
Version::parse("2.0.0").unwrap()
);
assert_eq!(
bump_version(&BumpLevel::Major, &Version::parse("1.1.0").unwrap()).unwrap(),
Version::parse("2.0.0").unwrap()
);
assert_eq!(
bump_version(&BumpLevel::Major, &Version::parse("1.1.1").unwrap()).unwrap(),
Version::parse("2.0.0").unwrap()
);
}
#[test]
fn test_bump_version_minor() {
assert_eq!(
bump_version(&BumpLevel::Minor, &Version::parse("1.0.0").unwrap()).unwrap(),
Version::parse("1.1.0").unwrap()
);
assert_eq!(
bump_version(&BumpLevel::Minor, &Version::parse("1.2.1").unwrap()).unwrap(),
Version::parse("1.3.0").unwrap()
);
}
#[test]
fn test_bump_version_patch() {
assert_eq!(
bump_version(&BumpLevel::Patch, &Version::parse("1.0.0").unwrap()).unwrap(),
Version::parse("1.0.1").unwrap()
);
}
#[test]
fn test_bump_version_prerelease() {
assert_eq!(
bump_version(
&BumpLevel::PreRelease,
&Version::parse("1.2.3-alpha.0").unwrap()
)
.unwrap(),
Version::parse("1.2.3-alpha.1").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PreRelease,
&Version::parse("1.2.3-alpha.1").unwrap()
)
.unwrap(),
Version::parse("1.2.3-alpha.2").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PreRelease,
&Version::parse("1.2.3-beta.0").unwrap()
)
.unwrap(),
Version::parse("1.2.3-beta.1").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PreRelease,
&Version::parse("1.2.3-rc.0").unwrap()
)
.unwrap(),
Version::parse("1.2.3-rc.1").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PreRelease,
&Version::parse("1.2.3-alpha123").unwrap()
)
.unwrap_err()
.to_string(),
"unexpected prerelease format: alpha123",
);
assert_eq!(
bump_version(
&BumpLevel::PreRelease,
&Version::parse("1.2.3-alpha.custom").unwrap()
)
.unwrap_err()
.to_string(),
"unexpected prerelease format: alpha.custom",
);
}
#[test]
fn test_bump_version_promote_prerelease() {
assert_eq!(
bump_version(
&BumpLevel::PromotePreRelease,
&Version::parse("1.2.3-alpha.0").unwrap()
)
.unwrap(),
Version::parse("1.2.3-beta.0").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PromotePreRelease,
&Version::parse("1.2.3-alpha.1").unwrap()
)
.unwrap(),
Version::parse("1.2.3-beta.0").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PromotePreRelease,
&Version::parse("1.2.3-beta.0").unwrap()
)
.unwrap(),
Version::parse("1.2.3-rc.0").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PromotePreRelease,
&Version::parse("1.2.3-rc.0").unwrap()
)
.unwrap(),
Version::parse("1.2.3").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PromotePreRelease,
&Version::parse("1.2.3-alpha123").unwrap()
)
.unwrap_err()
.to_string(),
"unexpected prerelease format: alpha123",
);
assert_eq!(
bump_version(
&BumpLevel::PromotePreRelease,
&Version::parse("1.2.3-custom.1").unwrap()
)
.unwrap_err()
.to_string(),
"unexpected prerelease format: custom.1, only alpha, beta, and rc are supported"
);
}
#[test]
fn test_bump_version_patch_or_prerelease() {
assert_eq!(
bump_version(
&BumpLevel::PatchOrPreRelease,
&Version::parse("1.2.3-alpha.0").unwrap()
)
.unwrap(),
Version::parse("1.2.3-alpha.1").unwrap()
);
assert_eq!(
bump_version(
&BumpLevel::PatchOrPreRelease,
&Version::parse("1.2.3").unwrap()
)
.unwrap(),
Version::parse("1.2.4").unwrap()
);
}
}