From 31fea9ebdbc6e9eaf53897bf550a1d2f0181246c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 20 Aug 2021 20:24:21 -0500 Subject: [PATCH] feat: MVP for `cargo set-version` The name is not `version` because `cargo version` reports cargo's version number. The UI and implementation is lifted from `cargo-release` Differences with `cargo-release`: - Relative version changes is behind `--bump` rather than overloading the positional argument - Motivation: It didn't jive with `set-` in the name - Side benefit: Better error reporting Differences with #414: - `--bump ` vs `--` - `--bump` scales better - This PR supports `--metadata`, `--exclude` Required future work - Update workspace dependents when modifying versions Possible future work - `--at-least` flag to ignore downgrades, rather than error - A flag to upgrade everything to the highest requested version (keep things in lockstep with `--bump patch`) - A flag to upgrade min requirements on dependents (rather than just updating if semver is broken) - `--pre` flag so you can do `--bump major --pre alpha` (even `1.0 --pre alpha` since I always forget that syntax) --- Cargo.toml | 7 + README.md | 51 ++++- src/bin/set-version/args.rs | 64 ++++++ src/bin/set-version/errors.rs | 19 ++ src/bin/set-version/main.rs | 219 +++++++++++++++++++ src/bin/set-version/version.rs | 128 +++++++++++ src/errors.rs | 8 + src/manifest.rs | 56 ++++- src/version.rs | 198 +++++++++++++++++ tests/cargo-set-version.rs | 55 +++++ tests/fixtures/set-version/Cargo.toml.sample | 9 + tests/utils.rs | 1 + 12 files changed, 810 insertions(+), 5 deletions(-) create mode 100644 src/bin/set-version/args.rs create mode 100644 src/bin/set-version/errors.rs create mode 100644 src/bin/set-version/main.rs create mode 100644 src/bin/set-version/version.rs create mode 100644 tests/cargo-set-version.rs create mode 100644 tests/fixtures/set-version/Cargo.toml.sample diff --git a/Cargo.toml b/Cargo.toml index 844e3ca74d..146c2ea12f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,11 @@ name = "cargo-upgrade" path = "src/bin/upgrade/main.rs" required-features = ["upgrade"] +[[bin]] +name = "cargo-set-version" +path = "src/bin/set-version/main.rs" +required-features = ["set-version"] + [dependencies] atty = { version = "0.2.14", optional = true } cargo_metadata = "0.14.0" @@ -79,10 +84,12 @@ default = [ "add", "rm", "upgrade", + "set-version", ] add = ["cli"] rm = ["cli"] upgrade = ["cli"] +set-version = ["cli"] cli = ["atty", "structopt"] test-external-apis = [] vendored-openssl = ["git2/vendored-openssl"] diff --git a/README.md b/README.md index 8c83e012c6..c404bdda49 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Currently available subcommands: - [`cargo add`](#cargo-add) - [`cargo rm`](#cargo-rm) - [`cargo upgrade`](#cargo-upgrade) +- [`cargo set-version`](#cargo-set-version) [![Build Status](https://github.com/killercup/cargo-edit/workflows/build/badge.svg)](https://github.com/killercup/cargo-edit/actions) [![Build Status](https://travis-ci.org/killercup/cargo-edit.svg?branch=master)](https://travis-ci.org/killercup/cargo-edit) @@ -227,7 +228,7 @@ upgrade to for each can be specified with e.g. `docopt@0.8.0` or `serde@>=0.9,<2 Dev, build, and all target dependencies will also be upgraded. Only dependencies from crates.io are supported. Git/path dependencies will be ignored. -All packages in the workspace will be upgraded if the `--workspace` flag is supplied. +All packages in the workspace will be upgraded if the `--workspace` flag is supplied. The `--workspace` flag may be supplied in the presence of a virtual manifest. If the '--to-lockfile' flag is supplied, all dependencies will be upgraded to the currently locked @@ -236,6 +237,54 @@ up-to-date. If the lock file is missing, or it needs to be updated, cargo-upgrad error. If the '--to-lockfile' flag is supplied then the network won't be accessed. ``` +### `cargo set-version` + +Set the version in your `Cargo.toml`. + +#### Examples + +```sh +# Set the version to the version 1.0.0 +$ cargo set-version 1.0.0 +# Bump the version to the next major +$ cargo set-version --bump major +# Bump version to the next minor +$ cargo set-version --bump minor +# Bump version to the next patch +$ cargo set-version --bump patch +``` + +#### Usage + +```plain +cargo-set-version 0.7.0 +Change a package's version in the local manifest file (i.e. Cargo.toml) + +USAGE: + cargo set-version [FLAGS] [OPTIONS] [--] [target] + +FLAGS: + --all [deprecated in favor of `--workspace`] + --dry-run Print changes to be made without making them + -h, --help Prints help information + -V, --version Prints version information + --workspace Modify all packages in the workspace + +OPTIONS: + --bump Increment manifest version [possible values: major, minor, patch, + release, rc, beta, alpha] + --exclude ... Crates to exclude and not modify + --manifest-path Path to the manifest to upgrade + -m, --metadata Specify the version metadata field (e.g. a wrapped libraries version) + -p, --package Package id of the crate to change the version of + +ARGS: + Version to change manifests to +``` + +For more on `metadata`, see the +[semver crate's documentation](https://docs.rs/semver/1.0.4/semver/struct.BuildMetadata.html). + ## License Apache-2.0/MIT diff --git a/src/bin/set-version/args.rs b/src/bin/set-version/args.rs new file mode 100644 index 0000000000..57e8fc5fa0 --- /dev/null +++ b/src/bin/set-version/args.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; + +use structopt::{clap::AppSettings, StructOpt}; + +#[derive(Debug, StructOpt)] +#[structopt(bin_name = "cargo")] +pub(crate) enum Command { + /// Change a package's version in the local manifest file (i.e. Cargo.toml). + #[structopt(name = "set-version")] + Version(Args), +} + +#[derive(Debug, StructOpt)] +#[structopt(setting = AppSettings::ColoredHelp)] +#[structopt(group = structopt::clap::ArgGroup::with_name("version").multiple(false))] +pub(crate) struct Args { + /// Version to change manifests to + #[structopt(parse(try_from_str), group = "version")] + pub(crate) target: Option, + + /// Increment manifest version + #[structopt(long, possible_values(&crate::version::BumpLevel::variants()), group = "version")] + pub(crate) bump: Option, + + /// Specify the version metadata field (e.g. a wrapped libraries version) + #[structopt(short = "m", long)] + pub metadata: Option, + + /// Path to the manifest to upgrade + #[structopt(long = "manifest-path", value_name = "path", conflicts_with = "pkgid")] + pub(crate) manifest_path: Option, + + /// Package id of the crate to change the version of. + #[structopt( + long = "package", + short = "p", + value_name = "pkgid", + conflicts_with = "path", + conflicts_with = "all", + conflicts_with = "workspace" + )] + pub(crate) pkgid: Option, + + /// Modify all packages in the workspace. + #[structopt( + long = "all", + help = "[deprecated in favor of `--workspace`]", + conflicts_with = "workspace", + conflicts_with = "pkgid" + )] + pub(crate) all: bool, + + /// Modify all packages in the workspace. + #[structopt(long = "workspace", conflicts_with = "all", conflicts_with = "pkgid")] + pub(crate) workspace: bool, + + /// Print changes to be made without making them. + #[structopt(long = "dry-run")] + pub(crate) dry_run: bool, + + /// Crates to exclude and not modify. + #[structopt(long)] + pub(crate) exclude: Vec, +} diff --git a/src/bin/set-version/errors.rs b/src/bin/set-version/errors.rs new file mode 100644 index 0000000000..b2db5ec5a2 --- /dev/null +++ b/src/bin/set-version/errors.rs @@ -0,0 +1,19 @@ +error_chain! { + errors { + /// We do not support this version requirement at this time + UnsupportedVersionReq(req: String) { + display("Support for modifying {} is currently unsupported", req) + } + /// User requested to downgrade a crate + VersionDowngreade(current: semver::Version, requested: semver::Version) { + display("Cannot downgrade from {} to {}", current, requested) + } + } + links { + CargoEditLib(::cargo_edit::Error, ::cargo_edit::ErrorKind); + } + foreign_links { + CargoMetadata(::failure::Compat<::cargo_metadata::Error>); + Version(::semver::Error)#[doc = "An error from the semver crate"]; + } +} diff --git a/src/bin/set-version/main.rs b/src/bin/set-version/main.rs new file mode 100644 index 0000000000..7e01fd731a --- /dev/null +++ b/src/bin/set-version/main.rs @@ -0,0 +1,219 @@ +//! `cargo version` +#![warn( + missing_docs, + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces, + unused_qualifications +)] +#![allow(clippy::comparison_chain)] + +#[macro_use] +extern crate error_chain; + +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process; + +use cargo_edit::{find, manifest_from_pkgid, LocalManifest}; +use failure::Fail; +use structopt::StructOpt; +use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; + +mod args; +mod errors; +mod version; +use crate::args::*; +use crate::errors::*; + +fn main() { + let args = Command::from_args(); + let Command::Version(args) = args; + + if let Err(err) = process(args) { + eprintln!("Command failed due to unhandled error: {}\n", err); + + for e in err.iter().skip(1) { + eprintln!("Caused by: {}", e); + } + + if let Some(backtrace) = err.backtrace() { + eprintln!("Backtrace: {:?}", backtrace); + } + + process::exit(1); + } +} + +/// Main processing function. Allows us to return a `Result` so that `main` can print pretty error +/// messages. +fn process(args: Args) -> Result<()> { + let Args { + target, + bump, + metadata, + manifest_path, + pkgid, + all, + dry_run, + workspace, + exclude, + } = args; + + let target = match (target, bump) { + (None, None) => version::TargetVersion::Relative(version::BumpLevel::Release), + (None, Some(level)) => version::TargetVersion::Relative(level), + (Some(version), None) => version::TargetVersion::Absolute(version), + (Some(_), Some(_)) => unreachable!("clap groups should prevent this"), + }; + + if all { + deprecated_message("The flag `--all` has been deprecated in favor of `--workspace`")?; + } + let all = workspace || all; + let manifests = if all { + Manifests::get_all(&manifest_path) + } else if let Some(ref pkgid) = pkgid { + Manifests::get_pkgid(pkgid) + } else { + Manifests::get_local_one(&manifest_path) + }?; + + if dry_run { + dry_run_message()?; + } + + for (mut manifest, package) in manifests.0 { + if exclude.contains(&package.name) { + continue; + } + let current = &package.version; + let next = target.bump(current, metadata.as_deref())?; + if let Some(next) = next { + manifest.set_package_version(&next); + + upgrade_message(package.name.as_str(), current, &next)?; + if !dry_run { + manifest.write()?; + } + } + } + + Ok(()) +} + +/// A collection of manifests. +struct Manifests(Vec<(LocalManifest, cargo_metadata::Package)>); + +impl Manifests { + /// Get all manifests in the workspace. + fn get_all(manifest_path: &Option) -> Result { + let mut cmd = cargo_metadata::MetadataCommand::new(); + cmd.no_deps(); + if let Some(path) = manifest_path { + cmd.manifest_path(path); + } + let result = cmd.exec().map_err(|e| { + Error::from(e.compat()).chain_err(|| "Failed to get workspace metadata") + })?; + result + .packages + .into_iter() + .map(|package| { + Ok(( + LocalManifest::try_new(Path::new(&package.manifest_path))?, + package, + )) + }) + .collect::>>() + .map(Manifests) + } + + fn get_pkgid(pkgid: &str) -> Result { + let package = manifest_from_pkgid(pkgid)?; + let manifest = LocalManifest::try_new(Path::new(&package.manifest_path))?; + Ok(Manifests(vec![(manifest, package)])) + } + + /// Get the manifest specified by the manifest path. Try to make an educated guess if no path is + /// provided. + fn get_local_one(manifest_path: &Option) -> Result { + let resolved_manifest_path: String = find(manifest_path)?.to_string_lossy().into(); + + let manifest = LocalManifest::find(manifest_path)?; + + let mut cmd = cargo_metadata::MetadataCommand::new(); + cmd.no_deps(); + if let Some(path) = manifest_path { + cmd.manifest_path(path); + } + let result = cmd + .exec() + .map_err(|e| Error::from(e.compat()).chain_err(|| "Invalid manifest"))?; + let packages = result.packages; + let package = packages + .iter() + .find(|p| p.manifest_path == resolved_manifest_path) + // If we have successfully got metadata, but our manifest path does not correspond to a + // package, we must have been called against a virtual manifest. + .chain_err(|| { + "Found virtual manifest, but this command requires running against an \ + actual package in this workspace. Try adding `--workspace`." + })?; + + Ok(Manifests(vec![(manifest, package.to_owned())])) + } +} + +fn dry_run_message() -> Result<()> { + let bufwtr = BufferWriter::stdout(ColorChoice::Always); + let mut buffer = bufwtr.buffer(); + buffer + .set_color(ColorSpec::new().set_fg(Some(Color::Cyan)).set_bold(true)) + .chain_err(|| "Failed to set output colour")?; + write!(&mut buffer, "Starting dry run. ").chain_err(|| "Failed to write dry run message")?; + buffer + .set_color(&ColorSpec::new()) + .chain_err(|| "Failed to clear output colour")?; + writeln!(&mut buffer, "Changes will not be saved.") + .chain_err(|| "Failed to write dry run message")?; + bufwtr + .print(&buffer) + .chain_err(|| "Failed to print dry run message") +} + +fn deprecated_message(message: &str) -> Result<()> { + let bufwtr = BufferWriter::stderr(ColorChoice::Always); + let mut buffer = bufwtr.buffer(); + buffer + .set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true)) + .chain_err(|| "Failed to set output colour")?; + writeln!(&mut buffer, "{}", message).chain_err(|| "Failed to write dry run message")?; + buffer + .set_color(&ColorSpec::new()) + .chain_err(|| "Failed to clear output colour")?; + bufwtr + .print(&buffer) + .chain_err(|| "Failed to print dry run message") +} + +fn upgrade_message(name: &str, from: &semver::Version, to: &semver::Version) -> Result<()> { + let bufwtr = BufferWriter::stdout(ColorChoice::Always); + let mut buffer = bufwtr.buffer(); + buffer + .set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true)) + .chain_err(|| "Failed to print dry run message")?; + write!(&mut buffer, "{:>12}", "Upgraded").chain_err(|| "Failed to print dry run message")?; + buffer + .reset() + .chain_err(|| "Failed to print dry run message")?; + writeln!(&mut buffer, " {} from {} to {}", name, from, to) + .chain_err(|| "Failed to print dry run message")?; + bufwtr + .print(&buffer) + .chain_err(|| "Failed to print dry run message") +} diff --git a/src/bin/set-version/version.rs b/src/bin/set-version/version.rs new file mode 100644 index 0000000000..251bbed0eb --- /dev/null +++ b/src/bin/set-version/version.rs @@ -0,0 +1,128 @@ +use std::str::FromStr; + +use cargo_edit::VersionExt; + +use crate::{Error, ErrorKind}; + +#[derive(Clone, Debug)] +pub enum TargetVersion { + Relative(BumpLevel), + Absolute(semver::Version), +} + +impl TargetVersion { + pub fn bump( + &self, + current: &semver::Version, + metadata: Option<&str>, + ) -> Result, Error> { + match self { + TargetVersion::Relative(bump_level) => { + let mut potential_version = current.to_owned(); + bump_level.bump_version(&mut potential_version, metadata)?; + if potential_version != *current { + let version = potential_version; + Ok(Some(version)) + } else { + Ok(None) + } + } + TargetVersion::Absolute(version) => { + if current < version { + Ok(Some(version.clone())) + } else if current == version { + Ok(None) + } else { + Err(ErrorKind::VersionDowngreade(current.clone(), version.clone()).into()) + } + } + } + } +} + +impl Default for TargetVersion { + fn default() -> Self { + TargetVersion::Relative(BumpLevel::Release) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum BumpLevel { + Major, + Minor, + Patch, + /// Strip all pre-release flags + Release, + Rc, + Beta, + Alpha, +} + +impl BumpLevel { + pub fn variants() -> &'static [&'static str] { + &["major", "minor", "patch", "release", "rc", "beta", "alpha"] + } +} + +impl FromStr for BumpLevel { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "major" => Ok(BumpLevel::Major), + "minor" => Ok(BumpLevel::Minor), + "patch" => Ok(BumpLevel::Patch), + "release" => Ok(BumpLevel::Release), + "rc" => Ok(BumpLevel::Rc), + "beta" => Ok(BumpLevel::Beta), + "alpha" => Ok(BumpLevel::Alpha), + _ => Err(String::from( + "[valid values: major, minor, patch, rc, beta, alpha]", + )), + } + } +} + +impl BumpLevel { + pub fn bump_version( + self, + version: &mut semver::Version, + metadata: Option<&str>, + ) -> Result<(), Error> { + match self { + BumpLevel::Major => { + version.increment_major(); + } + BumpLevel::Minor => { + version.increment_minor(); + } + BumpLevel::Patch => { + if !version.is_prerelease() { + version.increment_patch(); + } else { + version.pre = semver::Prerelease::EMPTY; + } + } + BumpLevel::Release => { + if version.is_prerelease() { + version.pre = semver::Prerelease::EMPTY; + } + } + BumpLevel::Rc => { + version.increment_rc()?; + } + BumpLevel::Beta => { + version.increment_beta()?; + } + BumpLevel::Alpha => { + version.increment_alpha()?; + } + }; + + if let Some(metadata) = metadata { + version.metadata(metadata)?; + } + + Ok(()) + } +} diff --git a/src/errors.rs b/src/errors.rs index 91bdf0cb1f..1d45daac19 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -89,5 +89,13 @@ error_chain! { // this is because git2 function takes &str instead of something like AsRef description("Path to cargos registry contains non unicode characters") } + /// An unsupported pre-release version was used. + UnsupportedPrereleaseVersionScheme { + display("This version scheme is not supported. Use format like `pre`, `dev` or `alpha.1` for prerelease symbol") + } + /// Cannot increment the specified field of the version. + InvalidReleaseLevel(actual: &'static str, version: semver::Version) { + display("Cannot increment the {} field for {}", actual, version) + } } } diff --git a/src/manifest.rs b/src/manifest.rs index 194f095718..acc55d57ce 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,6 +1,6 @@ use std::fs::{self, File, OpenOptions}; use std::io::{Read, Write}; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use std::{env, str}; @@ -422,6 +422,11 @@ impl Manifest { }) .map(|dep| (dep.0.into(), dep.1)) } + + /// Override the manifest's version + pub fn set_package_version(&mut self, version: &Version) { + self.data["package"]["version"] = toml_edit::value(version.to_string()); + } } impl str::FromStr for Manifest { @@ -435,6 +440,13 @@ impl str::FromStr for Manifest { } } +impl std::fmt::Display for Manifest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = self.data.to_string_in_original_order(); + s.fmt(f) + } +} + /// A Cargo manifest that is available locally. #[derive(Debug)] pub struct LocalManifest { @@ -452,6 +464,12 @@ impl Deref for LocalManifest { } } +impl DerefMut for LocalManifest { + fn deref_mut(&mut self) -> &mut Manifest { + &mut self.manifest + } +} + impl LocalManifest { /// Construct a `LocalManifest`. If no path is provided, make an educated guess as to which one /// the user means. @@ -469,6 +487,13 @@ impl LocalManifest { }) } + /// Write changes back to the file + pub fn write(&self) -> Result<()> { + let mut file = self.get_file()?; + self.write_to_file(&mut file) + .chain_err(|| "Failed to write new manifest contents") + } + /// Get the `File` corresponding to this manifest. fn get_file(&self) -> Result { Manifest::find_file(&Some(self.path.clone())) @@ -507,9 +532,7 @@ impl LocalManifest { } } - let mut file = self.get_file()?; - self.write_to_file(&mut file) - .chain_err(|| "Failed to write new manifest contents") + self.write() } } @@ -592,6 +615,31 @@ mod tests { .is_err()); } + #[test] + fn set_package_version_overrides() { + let original = r#" +[package] +name = "simple" +version = "0.1.0" +edition = "2015" + +[dependencies] +"#; + let expected = r#" +[package] +name = "simple" +version = "2.0.0" +edition = "2015" + +[dependencies] +"#; + let mut manifest = original.parse::().unwrap(); + manifest.set_package_version(&semver::Version::parse("2.0.0").unwrap()); + let actual = manifest.to_string(); + + assert_eq!(expected, actual); + } + #[test] fn old_version_is_compatible() -> Result<()> { let with_version = Dependency::new("foo").set_version("2.3.4"); diff --git a/src/version.rs b/src/version.rs index 1dba934093..26926994d9 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,11 +1,209 @@ +use std::str::FromStr; + +use crate::errors::*; + /// Additional version functionality pub trait VersionExt { + /// Increments the major version number for this Version. + fn increment_major(&mut self); + /// Increments the minor version number for this Version. + fn increment_minor(&mut self); + /// Increments the patch version number for this Version. + fn increment_patch(&mut self); + /// Increment the alpha pre-release number for this Version. + /// + /// If this isn't alpha, switch to it. + /// + /// Errors if this would decrement the pre-release phase. + fn increment_alpha(&mut self) -> Result<()>; + /// Increment the beta pre-release number for this Version. + /// + /// If this isn't beta, switch to it. + /// + /// Errors if this would decrement the pre-release phase. + fn increment_beta(&mut self) -> Result<()>; + /// Increment the rc pre-release number for this Version. + /// + /// If this isn't rc, switch to it. + /// + /// Errors if this would decrement the pre-release phase. + fn increment_rc(&mut self) -> Result<()>; + /// Append informational-only metadata. + fn metadata(&mut self, metadata: &str) -> Result<()>; /// Checks to see if the current Version is in pre-release status fn is_prerelease(&self) -> bool; } impl VersionExt for semver::Version { + fn increment_major(&mut self) { + self.major += 1; + self.minor = 0; + self.patch = 0; + self.pre = semver::Prerelease::EMPTY; + self.build = semver::BuildMetadata::EMPTY; + } + + fn increment_minor(&mut self) { + self.minor += 1; + self.patch = 0; + self.pre = semver::Prerelease::EMPTY; + self.build = semver::BuildMetadata::EMPTY; + } + + fn increment_patch(&mut self) { + self.patch += 1; + self.pre = semver::Prerelease::EMPTY; + self.build = semver::BuildMetadata::EMPTY; + } + + fn increment_alpha(&mut self) -> Result<()> { + if let Some((pre_ext, pre_ext_ver)) = prerelease_id_version(self)? { + if pre_ext == VERSION_BETA || pre_ext == VERSION_RC { + Err(ErrorKind::InvalidReleaseLevel(VERSION_ALPHA, self.clone()).into()) + } else { + let new_ext_ver = if pre_ext == VERSION_ALPHA { + pre_ext_ver.unwrap_or(0) + 1 + } else { + 1 + }; + self.pre = semver::Prerelease::new(&format!("{}.{}", VERSION_ALPHA, new_ext_ver))?; + Ok(()) + } + } else { + self.increment_patch(); + self.pre = semver::Prerelease::new(&format!("{}.1", VERSION_ALPHA))?; + Ok(()) + } + } + + fn increment_beta(&mut self) -> Result<()> { + if let Some((pre_ext, pre_ext_ver)) = prerelease_id_version(self)? { + if pre_ext == VERSION_RC { + Err(ErrorKind::InvalidReleaseLevel(VERSION_BETA, self.clone()).into()) + } else { + let new_ext_ver = if pre_ext == VERSION_BETA { + pre_ext_ver.unwrap_or(0) + 1 + } else { + 1 + }; + self.pre = semver::Prerelease::new(&format!("{}.{}", VERSION_BETA, new_ext_ver))?; + Ok(()) + } + } else { + self.increment_patch(); + self.pre = semver::Prerelease::new(&format!("{}.1", VERSION_BETA))?; + Ok(()) + } + } + + fn increment_rc(&mut self) -> Result<()> { + if let Some((pre_ext, pre_ext_ver)) = prerelease_id_version(self)? { + let new_ext_ver = if pre_ext == VERSION_RC { + pre_ext_ver.unwrap_or(0) + 1 + } else { + 1 + }; + self.pre = semver::Prerelease::new(&format!("{}.{}", VERSION_RC, new_ext_ver))?; + Ok(()) + } else { + self.increment_patch(); + self.pre = semver::Prerelease::new(&format!("{}.1", VERSION_RC))?; + Ok(()) + } + } + + fn metadata(&mut self, build: &str) -> Result<()> { + self.build = semver::BuildMetadata::new(build)?; + Ok(()) + } + fn is_prerelease(&self) -> bool { !self.pre.is_empty() } } + +static VERSION_ALPHA: &str = "alpha"; +static VERSION_BETA: &str = "beta"; +static VERSION_RC: &str = "rc"; + +fn prerelease_id_version(version: &semver::Version) -> Result)>> { + if !version.pre.is_empty() { + if let Some((alpha, numeric)) = version.pre.as_str().split_once(".") { + let alpha = alpha.to_owned(); + let numeric = u64::from_str(numeric) + .map_err(|_| ErrorKind::UnsupportedPrereleaseVersionScheme)?; + Ok(Some((alpha, Some(numeric)))) + } else { + Ok(Some((version.pre.as_str().to_owned(), None))) + } + } else { + Ok(None) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn alpha() { + let mut v = semver::Version::parse("1.0.0").unwrap(); + v.increment_alpha().unwrap(); + assert_eq!(v, semver::Version::parse("1.0.1-alpha.1").unwrap()); + + let mut v2 = semver::Version::parse("1.0.1-dev").unwrap(); + v2.increment_alpha().unwrap(); + assert_eq!(v2, semver::Version::parse("1.0.1-alpha.1").unwrap()); + + let mut v3 = semver::Version::parse("1.0.1-alpha.1").unwrap(); + v3.increment_alpha().unwrap(); + assert_eq!(v3, semver::Version::parse("1.0.1-alpha.2").unwrap()); + + let mut v4 = semver::Version::parse("1.0.1-beta.1").unwrap(); + assert!(v4.increment_alpha().is_err()); + } + + #[test] + fn beta() { + let mut v = semver::Version::parse("1.0.0").unwrap(); + v.increment_beta().unwrap(); + assert_eq!(v, semver::Version::parse("1.0.1-beta.1").unwrap()); + + let mut v2 = semver::Version::parse("1.0.1-dev").unwrap(); + v2.increment_beta().unwrap(); + assert_eq!(v2, semver::Version::parse("1.0.1-beta.1").unwrap()); + + let mut v2 = semver::Version::parse("1.0.1-alpha.1").unwrap(); + v2.increment_beta().unwrap(); + assert_eq!(v2, semver::Version::parse("1.0.1-beta.1").unwrap()); + + let mut v3 = semver::Version::parse("1.0.1-beta.1").unwrap(); + v3.increment_beta().unwrap(); + assert_eq!(v3, semver::Version::parse("1.0.1-beta.2").unwrap()); + + let mut v4 = semver::Version::parse("1.0.1-rc.1").unwrap(); + assert!(v4.increment_beta().is_err()); + } + + #[test] + fn rc() { + let mut v = semver::Version::parse("1.0.0").unwrap(); + v.increment_rc().unwrap(); + assert_eq!(v, semver::Version::parse("1.0.1-rc.1").unwrap()); + + let mut v2 = semver::Version::parse("1.0.1-dev").unwrap(); + v2.increment_rc().unwrap(); + assert_eq!(v2, semver::Version::parse("1.0.1-rc.1").unwrap()); + + let mut v3 = semver::Version::parse("1.0.1-rc.1").unwrap(); + v3.increment_rc().unwrap(); + assert_eq!(v3, semver::Version::parse("1.0.1-rc.2").unwrap()); + } + + #[test] + fn metadata() { + let mut v = semver::Version::parse("1.0.0").unwrap(); + v.metadata("git.123456").unwrap(); + assert_eq!(v, semver::Version::parse("1.0.0+git.123456").unwrap()); + } +} diff --git a/tests/cargo-set-version.rs b/tests/cargo-set-version.rs new file mode 100644 index 0000000000..318476421e --- /dev/null +++ b/tests/cargo-set-version.rs @@ -0,0 +1,55 @@ +#[macro_use] +extern crate pretty_assertions; + +mod utils; +use crate::utils::{clone_out_test, execute_bad_command, execute_command, get_toml}; + +#[test] +fn set_absolute_version() { + let (_tmpdir, manifest) = clone_out_test("tests/fixtures/set-version/Cargo.toml.sample"); + + let toml = get_toml(&manifest); + let val = &toml["package"]["version"]; + assert_eq!(val.as_str().unwrap(), "0.1.0"); + + execute_command(&["set-version", "2.0.0"], &manifest); + + // dependency present afterwards + let toml = get_toml(&manifest); + let val = &toml["package"]["version"]; + assert_eq!(val.as_str().unwrap(), "2.0.0"); +} + +#[test] +fn set_relative_version() { + let (_tmpdir, manifest) = clone_out_test("tests/fixtures/set-version/Cargo.toml.sample"); + + let toml = get_toml(&manifest); + let val = &toml["package"]["version"]; + assert_eq!(val.as_str().unwrap(), "0.1.0"); + + execute_command(&["set-version", "--bump", "major"], &manifest); + + // dependency present afterwards + let toml = get_toml(&manifest); + let val = &toml["package"]["version"]; + assert_eq!(val.as_str().unwrap(), "1.0.0"); +} + +#[test] +fn relative_absolute_conflict() { + let (_tmpdir, manifest) = clone_out_test("tests/fixtures/set-version/Cargo.toml.sample"); + + execute_bad_command(&["set-version", "1.0.0", "--bump", "major"], &manifest); +} + +#[test] +fn downgrade_error() { + let (_tmpdir, manifest) = clone_out_test("tests/fixtures/set-version/Cargo.toml.sample"); + + let toml = get_toml(&manifest); + let val = &toml["package"]["version"]; + assert_eq!(val.as_str().unwrap(), "0.1.0"); + + execute_bad_command(&["set-version", "0.0.1"], &manifest); +} diff --git a/tests/fixtures/set-version/Cargo.toml.sample b/tests/fixtures/set-version/Cargo.toml.sample new file mode 100644 index 0000000000..55d0195fc7 --- /dev/null +++ b/tests/fixtures/set-version/Cargo.toml.sample @@ -0,0 +1,9 @@ +[package] +name = "sample" +version = "0.1.0" +edition = "2015" + +[lib] +path = "dummy.rs" + +[dependencies] diff --git a/tests/utils.rs b/tests/utils.rs index 86a1dbcf17..5a734e600f 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -194,6 +194,7 @@ pub fn get_command_path(s: impl AsRef) -> &'static str { Some("add") => env!("CARGO_BIN_EXE_cargo-add"), Some("rm") => env!("CARGO_BIN_EXE_cargo-rm"), Some("upgrade") => env!("CARGO_BIN_EXE_cargo-upgrade"), + Some("set-version") => env!("CARGO_BIN_EXE_cargo-set-version"), _ => panic!("Unsupported subcommand"), } }