use crate::utils::{
cargo, change_versions, info, read_config, ChangeData, ChangeOpt, Error, GitOpt, Pkg, Result,
WorkspaceConfig, INTERNAL_ERR,
};
use cargo_metadata::Metadata;
use clap::{ArgEnum, Parser};
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
use oclif::{
console::Style,
term::{TERM_ERR, TERM_OUT},
};
use semver::{Identifier, Version};
use std::{collections::BTreeMap as Map, fs, process::exit};
#[derive(Debug, Clone, ArgEnum)]
pub enum Bump {
Major,
Minor,
Patch,
Premajor,
Preminor,
Prepatch,
Prerelease,
Custom,
}
impl Bump {
pub fn selected(&self) -> usize {
match self {
Bump::Major => 2,
Bump::Minor => 1,
Bump::Patch => 0,
Bump::Premajor => 5,
Bump::Preminor => 4,
Bump::Prepatch => 3,
Bump::Prerelease => 6,
Bump::Custom => 7,
}
}
}
#[derive(Debug, Parser)]
#[clap(next_help_heading = "VERSION OPTIONS")]
pub struct VersionOpt {
#[clap(arg_enum, help_heading = "VERSION ARGS")]
pub bump: Option<Bump>,
#[clap(required_if_eq("bump", "custom"), help_heading = "VERSION ARGS")]
pub custom: Option<Version>,
#[clap(long, value_name = "identifier", forbid_empty_values(true))]
pub pre_id: Option<String>,
#[clap(flatten)]
pub change: ChangeOpt,
#[clap(flatten)]
pub git: GitOpt,
#[clap(short, long)]
pub all: bool,
#[clap(long)]
pub exact: bool,
#[clap(short, long)]
pub yes: bool,
}
impl VersionOpt {
pub fn do_versioning(&self, metadata: &Metadata) -> Result<Map<String, Version>> {
let config: WorkspaceConfig = read_config(&metadata.workspace_metadata)?;
let branch = self.git.validate(&metadata.workspace_root, &config)?;
let change_data = ChangeData::new(metadata, &self.change)?;
if self.change.force.is_none() && change_data.count == "0" && !change_data.dirty {
TERM_OUT.write_line("Current HEAD is already released, skipping versioning")?;
return Ok(Map::new());
}
let (mut changed_p, mut unchanged_p) =
self.change
.get_changed_pkgs(metadata, &change_data.since, self.all)?;
if changed_p.is_empty() {
TERM_OUT.write_line("No changes detected, skipping versioning")?;
return Ok(Map::new());
}
let mut new_version = None;
let mut new_versions = vec![];
while !changed_p.is_empty() {
self.get_new_versions(metadata, changed_p, &mut new_version, &mut new_versions)?;
let pkgs = unchanged_p.into_iter().partition::<Vec<_>, _>(|p| {
let pkg = metadata
.packages
.iter()
.find(|x| x.name == p.name)
.expect(INTERNAL_ERR);
pkg.dependencies.iter().any(|x| {
if let Some(version) = new_versions.iter().find(|y| x.name == y.0).map(|y| &y.1)
{
!x.req.matches(version)
} else {
false
}
})
});
changed_p = pkgs.0;
unchanged_p = pkgs.1;
}
let new_versions = self.confirm_versions(new_versions)?;
for p in &metadata.packages {
if new_versions.get(&p.name).is_none()
&& p.dependencies
.iter()
.all(|x| new_versions.get(&x.name).is_none())
{
continue;
}
fs::write(
&p.manifest_path,
format!(
"{}\n",
change_versions(
fs::read_to_string(&p.manifest_path)?,
&p.name,
&new_versions,
self.exact,
)?
),
)?;
}
for pkg in new_versions.keys() {
let output = cargo(&metadata.workspace_root, &["update", "-p", pkg], &[])?;
if output.1.contains("error:") {
return Err(Error::Update);
}
}
self.git.commit(
&metadata.workspace_root,
&new_version,
&new_versions,
branch,
&config,
)?;
Ok(new_versions)
}
fn get_new_versions(
&self,
metadata: &Metadata,
pkgs: Vec<Pkg>,
new_version: &mut Option<Version>,
new_versions: &mut Vec<(String, Version, Version)>,
) -> Result {
let (independent_pkgs, same_pkgs) = pkgs
.into_iter()
.partition::<Vec<_>, _>(|p| p.config.independent.unwrap_or(false));
if !same_pkgs.is_empty() {
let cur_version = same_pkgs
.iter()
.map(|p| {
&metadata
.packages
.iter()
.find(|x| x.id == p.id)
.expect(INTERNAL_ERR)
.version
})
.max()
.expect(INTERNAL_ERR);
if new_version.is_none() {
info!("current common version", cur_version);
*new_version = Some(self.ask_version(cur_version, None)?);
}
for p in &same_pkgs {
new_versions.push((
p.name.to_string(),
new_version.as_ref().expect(INTERNAL_ERR).clone(),
p.version.clone(),
));
}
}
for p in &independent_pkgs {
let new_version = self.ask_version(&p.version, Some(&p.name))?;
new_versions.push((p.name.to_string(), new_version, p.version.clone()));
}
Ok(())
}
fn confirm_versions(
&self,
versions: Vec<(String, Version, Version)>,
) -> Result<Map<String, Version>> {
let mut new_versions = Map::new();
let style = Style::new().for_stderr();
TERM_ERR.write_line("\nChanges:")?;
for v in versions {
TERM_ERR.write_line(&format!(
" - {}: {} => {}",
style.clone().yellow().apply_to(&v.0),
v.2,
style.clone().cyan().apply_to(&v.1),
))?;
new_versions.insert(v.0, v.1);
}
TERM_ERR.write_line("")?;
TERM_ERR.flush()?;
let create = self.yes
|| Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Are you sure you want to create these versions?")
.default(false)
.interact_on(&TERM_ERR)?;
if !create {
exit(0);
}
Ok(new_versions)
}
fn ask_version(&self, cur_version: &Version, pkg_name: Option<&str>) -> Result<Version> {
let mut items = version_items(cur_version, &self.pre_id);
items.push(("Custom Prerelease".to_string(), None));
items.push(("Custom Version".to_string(), None));
let prompt = if let Some(name) = pkg_name {
format!("for {} ", name)
} else {
"".to_string()
};
let theme = ColorfulTheme::default();
let selected = if let Some(bump) = &self.bump {
bump.selected()
} else {
Select::with_theme(&theme)
.with_prompt(&format!(
"Select a new version {}(currently {})",
prompt, cur_version
))
.items(&items.iter().map(|x| &x.0).collect::<Vec<_>>())
.default(0)
.interact_on(&TERM_ERR)?
};
let new_version = if selected == 6 {
let custom = custom_pre(cur_version);
let preid = if let Some(preid) = &self.pre_id {
preid.clone()
} else {
Input::with_theme(&theme)
.with_prompt(&format!(
"Enter a prerelease identifier (default: '{}', yielding {})",
custom.0, custom.1
))
.default(custom.0.to_string())
.interact_on(&TERM_ERR)?
};
inc_preid(cur_version, Identifier::AlphaNumeric(preid))
} else if selected == 7 {
if let Some(version) = &self.custom {
version.clone()
} else {
Input::with_theme(&theme)
.with_prompt("Enter a custom version")
.interact_on(&TERM_ERR)?
}
} else {
items
.get(selected)
.expect(INTERNAL_ERR)
.clone()
.1
.expect(INTERNAL_ERR)
};
Ok(new_version)
}
}
fn inc_pre(pre: &[Identifier], preid: &Option<String>) -> Vec<Identifier> {
match pre.get(0) {
Some(Identifier::AlphaNumeric(id)) => {
vec![Identifier::AlphaNumeric(id.clone()), Identifier::Numeric(0)]
}
Some(Identifier::Numeric(_)) => vec![Identifier::Numeric(0)],
None => vec![
Identifier::AlphaNumeric(
preid
.as_ref()
.map_or_else(|| "alpha".to_string(), |x| x.clone()),
),
Identifier::Numeric(0),
],
}
}
fn inc_preid(cur_version: &Version, preid: Identifier) -> Version {
let mut version = cur_version.clone();
if cur_version.pre.is_empty() {
version.increment_patch();
version.pre = vec![preid, Identifier::Numeric(0)];
} else {
match cur_version.pre.get(0).expect(INTERNAL_ERR) {
Identifier::AlphaNumeric(id) => {
version.pre = vec![preid.clone()];
if preid.to_string() == *id {
match cur_version.pre.get(1) {
Some(Identifier::Numeric(n)) => {
version.pre.push(Identifier::Numeric(n + 1))
}
_ => version.pre.push(Identifier::Numeric(0)),
};
} else {
version.pre.push(Identifier::Numeric(0));
}
}
Identifier::Numeric(n) => {
if preid.to_string() == n.to_string() {
version.pre = cur_version.pre.clone();
if let Some(Identifier::Numeric(n)) = version
.pre
.iter_mut()
.rfind(|x| matches!(x, Identifier::Numeric(_)))
{
*n += 1;
}
} else {
version.pre = vec![preid, Identifier::Numeric(0)];
}
}
}
}
version
}
fn custom_pre(cur_version: &Version) -> (Identifier, Version) {
let id = if let Some(id) = cur_version.pre.get(0) {
id.clone()
} else {
Identifier::AlphaNumeric("alpha".to_string())
};
(id.clone(), inc_preid(cur_version, id))
}
fn inc_patch(mut cur_version: Version) -> Version {
if !cur_version.pre.is_empty() {
cur_version.pre.clear();
} else {
cur_version.increment_patch();
}
cur_version
}
fn inc_minor(mut cur_version: Version) -> Version {
if !cur_version.pre.is_empty() && cur_version.patch == 0 {
cur_version.pre.clear();
} else {
cur_version.increment_minor();
}
cur_version
}
fn inc_major(mut cur_version: Version) -> Version {
if !cur_version.pre.is_empty() && cur_version.patch == 0 && cur_version.minor == 0 {
cur_version.pre.clear();
} else {
cur_version.increment_major();
}
cur_version
}
fn version_items(cur_version: &Version, preid: &Option<String>) -> Vec<(String, Option<Version>)> {
let mut items = vec![];
let v = inc_patch(cur_version.clone());
items.push((format!("Patch ({})", &v), Some(v)));
let v = inc_minor(cur_version.clone());
items.push((format!("Minor ({})", &v), Some(v)));
let v = inc_major(cur_version.clone());
items.push((format!("Major ({})", &v), Some(v)));
let mut v = cur_version.clone();
v.increment_patch();
v.pre = inc_pre(&cur_version.pre, preid);
items.push((format!("Prepatch ({})", &v), Some(v)));
let mut v = cur_version.clone();
v.increment_minor();
v.pre = inc_pre(&cur_version.pre, preid);
items.push((format!("Preminor ({})", &v), Some(v)));
let mut v = cur_version.clone();
v.increment_major();
v.pre = inc_pre(&cur_version.pre, preid);
items.push((format!("Premajor ({})", &v), Some(v)));
items
}
#[cfg(test)]
mod test_super {
use super::*;
#[test]
fn test_inc_patch() {
let v = inc_patch(Version::parse("0.7.2").unwrap());
assert_eq!(v.to_string(), "0.7.3");
}
#[test]
fn test_inc_patch_on_prepatch() {
let v = inc_patch(Version::parse("0.7.2-rc.0").unwrap());
assert_eq!(v.to_string(), "0.7.2");
}
#[test]
fn test_inc_patch_on_preminor() {
let v = inc_patch(Version::parse("0.7.0-rc.0").unwrap());
assert_eq!(v.to_string(), "0.7.0");
}
#[test]
fn test_inc_patch_on_premajor() {
let v = inc_patch(Version::parse("1.0.0-rc.0").unwrap());
assert_eq!(v.to_string(), "1.0.0");
}
#[test]
fn test_inc_minor() {
let v = inc_minor(Version::parse("0.7.2").unwrap());
assert_eq!(v.to_string(), "0.8.0");
}
#[test]
fn test_inc_minor_on_prepatch() {
let v = inc_minor(Version::parse("0.7.2-rc.0").unwrap());
assert_eq!(v.to_string(), "0.8.0");
}
#[test]
fn test_inc_minor_on_preminor() {
let v = inc_minor(Version::parse("0.7.0-rc.0").unwrap());
assert_eq!(v.to_string(), "0.7.0");
}
#[test]
fn test_inc_minor_on_premajor() {
let v = inc_minor(Version::parse("1.0.0-rc.0").unwrap());
assert_eq!(v.to_string(), "1.0.0");
}
#[test]
fn test_inc_major() {
let v = inc_major(Version::parse("0.7.2").unwrap());
assert_eq!(v.to_string(), "1.0.0");
}
#[test]
fn test_inc_major_on_prepatch() {
let v = inc_major(Version::parse("0.7.2-rc.0").unwrap());
assert_eq!(v.to_string(), "1.0.0");
}
#[test]
fn test_inc_major_on_preminor() {
let v = inc_major(Version::parse("0.7.0-rc.0").unwrap());
assert_eq!(v.to_string(), "1.0.0");
}
#[test]
fn test_inc_major_on_premajor_with_patch() {
let v = inc_major(Version::parse("1.0.1-rc.0").unwrap());
assert_eq!(v.to_string(), "2.0.0");
}
#[test]
fn test_inc_major_on_premajor() {
let v = inc_major(Version::parse("1.0.0-rc.0").unwrap());
assert_eq!(v.to_string(), "1.0.0");
}
#[test]
fn test_inc_preid() {
let v = inc_preid(
&Version::parse("3.0.0").unwrap(),
Identifier::AlphaNumeric("beta".to_string()),
);
assert_eq!(v.to_string(), "3.0.1-beta.0");
}
#[test]
fn test_inc_preid_on_alpha() {
let v = inc_preid(
&Version::parse("3.0.0-alpha.19").unwrap(),
Identifier::AlphaNumeric("beta".to_string()),
);
assert_eq!(v.to_string(), "3.0.0-beta.0");
}
#[test]
fn test_inc_preid_on_num() {
let v = inc_preid(
&Version::parse("3.0.0-11.19").unwrap(),
Identifier::AlphaNumeric("beta".to_string()),
);
assert_eq!(v.to_string(), "3.0.0-beta.0");
}
#[test]
fn test_custom_pre() {
let v = custom_pre(&Version::parse("3.0.0").unwrap());
assert_eq!(v.0, Identifier::AlphaNumeric("alpha".to_string()));
assert_eq!(v.1.to_string(), "3.0.1-alpha.0");
}
#[test]
fn test_custom_pre_on_single_alpha() {
let v = custom_pre(&Version::parse("3.0.0-a").unwrap());
assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string()));
assert_eq!(v.1.to_string(), "3.0.0-a.0");
}
#[test]
fn test_custom_pre_on_single_alpha_with_second_num() {
let v = custom_pre(&Version::parse("3.0.0-a.11").unwrap());
assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string()));
assert_eq!(v.1.to_string(), "3.0.0-a.12");
}
#[test]
fn test_custom_pre_on_second_alpha() {
let v = custom_pre(&Version::parse("3.0.0-a.b").unwrap());
assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string()));
assert_eq!(v.1.to_string(), "3.0.0-a.0");
}
#[test]
fn test_custom_pre_on_second_alpha_with_num() {
let v = custom_pre(&Version::parse("3.0.0-a.b.1").unwrap());
assert_eq!(v.0, Identifier::AlphaNumeric("a".to_string()));
assert_eq!(v.1.to_string(), "3.0.0-a.0");
}
#[test]
fn test_custom_pre_on_single_num() {
let v = custom_pre(&Version::parse("3.0.0-11").unwrap());
assert_eq!(v.0, Identifier::Numeric(11));
assert_eq!(v.1.to_string(), "3.0.0-12");
}
#[test]
fn test_custom_pre_on_single_num_with_second_alpha() {
let v = custom_pre(&Version::parse("3.0.0-11.a").unwrap());
assert_eq!(v.0, Identifier::Numeric(11));
assert_eq!(v.1.to_string(), "3.0.0-12.a");
}
#[test]
fn test_custom_pre_on_second_num() {
let v = custom_pre(&Version::parse("3.0.0-11.20").unwrap());
assert_eq!(v.0, Identifier::Numeric(11));
assert_eq!(v.1.to_string(), "3.0.0-11.21");
}
#[test]
fn test_custom_pre_on_multiple_num() {
let v = custom_pre(&Version::parse("3.0.0-11.20.a.55.c").unwrap());
assert_eq!(v.0, Identifier::Numeric(11));
assert_eq!(v.1.to_string(), "3.0.0-11.20.a.56.c");
}
}