diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 1cee297a..27f270fb 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -9,10 +9,10 @@ publish = false
[dependencies]
isahc = { version = "1.2.0", features = ["json"] }
-itertools = "0.10.0"
semver = { version = "0.11.0", features = ["serde"] }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.60"
toml = "0.5.8"
+toml_edit = "0.2.0"
xflags = "0.2.1"
xshell = "0.1.9"
diff --git a/xtask/README.md b/xtask/README.md
index f2090073..0fdecb21 100644
--- a/xtask/README.md
+++ b/xtask/README.md
@@ -1,6 +1,6 @@
# Ruma xtasks
-This crate is a helper bin for repetitive tasks during Ruma development, based on [cargo-xtask].
+This crate is a helper bin for repetitive tasks during Ruma development, based on [cargo xtask][xtask].
To use it, run `cargo xtask [command]` anywhere in the workspace.
@@ -9,7 +9,10 @@ the appropriate fields.
## Commands
-- `release [crate]`: Publish `crate`, create a signed tag based on its name and version and create
- a release on GitHub. **Requires all `github` fields in `config.toml`.**
+- `release [crate] [version]`: Publish `crate` at given `version`, if applicable[1](#ref-1), create a
+ signed tag based on its name and version and create a release on GitHub.
+ **Requires all `github` fields in `config.toml`.**
-[cargo-xtask] : https://github.com/matklad/cargo-xtask
+1 if `crate` is a user-facing crate and `version` is not a pre-release.
+
+[xtask]: https://github.com/matklad/cargo-xtask
diff --git a/xtask/src/cargo.rs b/xtask/src/cargo.rs
new file mode 100644
index 00000000..3b62d51a
--- /dev/null
+++ b/xtask/src/cargo.rs
@@ -0,0 +1,192 @@
+use std::path::PathBuf;
+
+use isahc::{HttpClient, ReadResponseExt};
+use semver::Version;
+use serde::{de::IgnoredAny, Deserialize};
+use serde_json::from_str as from_json_str;
+use toml_edit::{value, Document};
+use xshell::{cmd, pushd, read_file, write_file};
+
+use crate::{util::ask_yes_no, Result};
+
+const CRATESIO_API: &str = "https://crates.io/api/v1/crates";
+
+/// The metadata of a cargo workspace.
+#[derive(Clone, Debug, Deserialize)]
+pub struct Metadata {
+ pub workspace_root: PathBuf,
+ pub packages: Vec,
+}
+
+impl Metadata {
+ /// Load a new `Metadata` from the command line.
+ pub fn load() -> Result {
+ let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
+ Ok(from_json_str(&metadata_json)?)
+ }
+}
+
+/// A cargo package.
+#[derive(Clone, Debug, Deserialize)]
+pub struct Package {
+ /// The package name
+ pub name: String,
+
+ /// The package version.
+ pub version: Version,
+
+ /// The package's manifest path.
+ pub manifest_path: PathBuf,
+
+ /// A map of the package dependencies.
+ #[serde(default)]
+ pub dependencies: Vec,
+}
+
+impl Package {
+ /// Update the version of this crate.
+ pub fn update_version(&mut self, version: &Version) -> Result<()> {
+ println!("Updating {} to version {}…", self.name, version);
+
+ let mut document = read_file(&self.manifest_path)?.parse::()?;
+
+ document["package"]["version"] = value(version.to_string());
+
+ write_file(&self.manifest_path, document.to_string())?;
+
+ self.version = version.clone();
+
+ Ok(())
+ }
+
+ /// Update the version of this crate in dependant crates' manifests, with the given version
+ /// prefix.
+ pub fn update_dependants(&self, metadata: &Metadata) -> Result<()> {
+ for package in metadata.packages.iter().filter(|p| {
+ p.manifest_path.starts_with(&metadata.workspace_root)
+ && p.dependencies.iter().any(|d| d.name == self.name)
+ }) {
+ println!("Updating dependency in {} crate…", package.name);
+
+ let mut document = read_file(&package.manifest_path)?.parse::()?;
+
+ for dependency in package.dependencies.iter().filter(|d| d.name == self.name) {
+ let version = if self.version.is_prerelease() || self.name.ends_with("-macros") {
+ format!("={}", self.version)
+ } else {
+ self.version.to_string()
+ };
+
+ let kind = match dependency.kind {
+ Some(DependencyKind::Dev) => "dev-dependencies",
+ Some(DependencyKind::Build) => "build-dependencies",
+ None => "dependencies",
+ };
+
+ document[kind][&self.name]["version"] = value(version.as_str());
+ }
+
+ write_file(&package.manifest_path, document.to_string())?;
+ }
+
+ Ok(())
+ }
+
+ /// Get the changes for the version. If `update` is `true`, update the changelog for the release
+ /// of the given version.
+ pub fn changes(&self, update: bool) -> Result {
+ let mut changelog_path = self.manifest_path.clone();
+ changelog_path.set_file_name("CHANGELOG.md");
+
+ let changelog = read_file(&changelog_path)?;
+
+ if !changelog.starts_with(&format!("# {}\n", self.version))
+ && !changelog.starts_with(&format!("# {} (unreleased)\n", self.version))
+ && !changelog.starts_with("# [unreleased]\n")
+ {
+ return Err("Could not find version title in changelog".into());
+ };
+
+ let changes_start = match changelog.find('\n') {
+ Some(p) => p + 1,
+ None => {
+ return Err("Could not find end of version title in changelog".into());
+ }
+ };
+
+ let changes_end = match changelog[changes_start..].find("\n# ") {
+ Some(p) => changes_start + p,
+ None => changelog.len(),
+ };
+
+ let changes = match changelog[changes_start..changes_end].trim() {
+ s if s.is_empty() => "No changes for this version",
+ s => s,
+ };
+
+ if update {
+ let changelog = format!(
+ "# [unreleased]\n\n# {}\n\n{}\n{}",
+ self.version,
+ changes,
+ &changelog[changes_end..]
+ );
+
+ write_file(&changelog_path, changelog)?;
+ }
+
+ Ok(changes.to_owned())
+ }
+
+ /// Check if the current version of the crate is published on crates.io.
+ pub fn is_published(&self, client: &HttpClient) -> Result {
+ let response: CratesIoCrate =
+ client.get(format!("{}/{}/{}", CRATESIO_API, self.name, self.version))?.json()?;
+
+ Ok(response.version.is_some())
+ }
+
+ /// Publish this package on crates.io.
+ pub fn publish(&self, client: &HttpClient) -> Result {
+ println!("Publishing {} {} on crates.io…", self.name, self.version);
+ let _dir = pushd(&self.manifest_path.parent().unwrap())?;
+
+ if self.is_published(client)? {
+ if ask_yes_no("This version is already published. Skip this step and continue?")? {
+ Ok(false)
+ } else {
+ Err("Release interrupted by user.".into())
+ }
+ } else {
+ cmd!("cargo publish").run()?;
+ Ok(true)
+ }
+ }
+}
+
+/// A cargo package dependency.
+#[derive(Clone, Debug, Deserialize)]
+pub struct Dependency {
+ /// The package name.
+ pub name: String,
+
+ /// The kind of the dependency.
+ pub kind: Option,
+}
+
+/// The kind of a cargo package dependency.
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum DependencyKind {
+ /// A dev dependency.
+ Dev,
+
+ /// A build dependency.
+ Build,
+}
+
+/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
+#[derive(Deserialize)]
+struct CratesIoCrate {
+ version: Option,
+}
diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs
index 85fc7d57..4e3cc7fd 100644
--- a/xtask/src/ci.rs
+++ b/xtask/src/ci.rs
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use xshell::pushd;
-use crate::{cmd, Result};
+use crate::{cargo::Metadata, cmd, Result};
const MSRV: &str = "1.45";
@@ -18,8 +18,9 @@ pub struct CiTask {
}
impl CiTask {
- pub(crate) fn new(version: Option, project_root: PathBuf) -> Self {
- Self { version, project_root }
+ pub(crate) fn new(version: Option) -> Result {
+ let project_root = Metadata::load()?.workspace_root;
+ Ok(Self { version, project_root })
}
pub(crate) fn run(self) -> Result<()> {
diff --git a/xtask/src/flags.rs b/xtask/src/flags.rs
index a0bfe6c5..117fe47e 100644
--- a/xtask/src/flags.rs
+++ b/xtask/src/flags.rs
@@ -1,5 +1,7 @@
#![allow(dead_code)] // silence never-used warning for from_vec in generated code
+use semver::Version;
+
xflags::xflags! {
src "./src/flags.rs"
@@ -14,6 +16,18 @@ xflags::xflags! {
cmd release
/// The crate to release
required name: String
+
+ /// The new version of the crate
+ required version: Version
+ {}
+
+ /// Alias for release.
+ cmd publish
+ /// The crate to release
+ required name: String
+
+ /// The new version of the crate
+ required version: Version
{}
/// Run CI tests.
@@ -34,6 +48,7 @@ pub struct Xtask {
pub enum XtaskCmd {
Help(Help),
Release(Release),
+ Publish(Publish),
Ci(Ci),
}
@@ -45,6 +60,13 @@ pub struct Help {
#[derive(Debug)]
pub struct Release {
pub name: String,
+ pub version: Version,
+}
+
+#[derive(Debug)]
+pub struct Publish {
+ pub name: String,
+ pub version: Version,
}
#[derive(Debug)]
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 5f9940b1..c4da2062 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -3,16 +3,13 @@
//! This binary is integrated into the `cargo` command line by using an alias in
//! `.cargo/config`. Run commands as `cargo xtask [command]`.
-use std::{
- env,
- path::{Path, PathBuf},
-};
+use std::{env, path::Path};
use serde::Deserialize;
-use serde_json::from_str as from_json_str;
use toml::from_str as from_toml_str;
use xshell::read_file;
+mod cargo;
mod ci;
mod flags;
mod release;
@@ -30,8 +27,6 @@ fn main() {
}
fn try_main() -> Result<()> {
- let project_root = project_root()?;
-
let flags = flags::Xtask::from_env()?;
match flags.subcommand {
flags::XtaskCmd::Help(_) => {
@@ -39,34 +34,35 @@ fn try_main() -> Result<()> {
Ok(())
}
flags::XtaskCmd::Release(cmd) => {
- let task = ReleaseTask::new(cmd.name, project_root)?;
+ let mut task = ReleaseTask::new(cmd.name, cmd.version)?;
+ task.run()
+ }
+ flags::XtaskCmd::Publish(cmd) => {
+ let mut task = ReleaseTask::new(cmd.name, cmd.version)?;
task.run()
}
flags::XtaskCmd::Ci(ci) => {
- let task = CiTask::new(ci.version, project_root);
+ let task = CiTask::new(ci.version)?;
task.run()
}
}
}
-#[derive(Debug, Deserialize)]
-struct CargoMetadata {
- workspace_root: PathBuf,
-}
-
-/// Get the project workspace root.
-fn project_root() -> Result {
- let metadata_json = cmd!("cargo metadata --format-version 1").read()?;
- let metadata: CargoMetadata = from_json_str(&metadata_json)?;
- Ok(metadata.workspace_root)
-}
-
#[derive(Debug, Deserialize)]
struct Config {
/// Credentials to authenticate to GitHub.
github: GithubConfig,
}
+impl Config {
+ /// Load a new `Config` from `config.toml`.
+ fn load() -> Result {
+ let path = Path::new(&env!("CARGO_MANIFEST_DIR")).join("config.toml");
+ let config = read_file(path)?;
+ Ok(from_toml_str(&config)?)
+ }
+}
+
#[derive(Debug, Deserialize)]
struct GithubConfig {
/// The username to use for authentication.
@@ -76,13 +72,6 @@ struct GithubConfig {
token: String,
}
-/// Load the config from `config.toml`.
-fn config() -> Result {
- let path = Path::new(&env!("CARGO_MANIFEST_DIR")).join("config.toml");
- let config = read_file(path)?;
- Ok(from_toml_str(&config)?)
-}
-
#[macro_export]
macro_rules! cmd {
($cmd:tt) => {
diff --git a/xtask/src/release.rs b/xtask/src/release.rs
index 3aa19347..d502acba 100644
--- a/xtask/src/release.rs
+++ b/xtask/src/release.rs
@@ -1,5 +1,5 @@
use std::{
- path::{Path, PathBuf},
+ io::{stdin, stdout, BufRead, Write},
thread::sleep,
time::Duration,
};
@@ -10,26 +10,30 @@ use isahc::{
http::StatusCode,
HttpClient, ReadResponseExt, Request,
};
-use itertools::Itertools;
-use semver::Version;
-use serde::{de::IgnoredAny, Deserialize};
+use semver::{Identifier, Version};
+use serde::Deserialize;
use serde_json::json;
-use toml::from_str as from_toml_str;
-use xshell::{pushd, read_file};
-use crate::{cmd, util::ask_yes_no, GithubConfig, Result};
+use crate::{
+ cargo::{Metadata, Package},
+ cmd,
+ util::ask_yes_no,
+ GithubConfig, Result,
+};
-const CRATESIO_API: &str = "https://crates.io/api/v1/crates";
const GITHUB_API_RUMA: &str = "https://api.github.com/repos/ruma/ruma";
/// Task to create a new release of the given crate.
#[derive(Debug)]
pub struct ReleaseTask {
- /// The crate to release.
- local_crate: LocalCrate,
+ /// The metadata of the cargo workspace.
+ metadata: Metadata,
- /// The root of the workspace.
- project_root: PathBuf,
+ /// The crate to release.
+ package: Package,
+
+ /// The new version of the crate.
+ version: Version,
/// The http client to use for requests.
http_client: HttpClient,
@@ -39,19 +43,30 @@ pub struct ReleaseTask {
}
impl ReleaseTask {
- /// Create a new `ReleaseTask` with the given `name` and `project_root`.
- pub(crate) fn new(name: String, project_root: PathBuf) -> Result {
- let local_crate = LocalCrate::new(name, &project_root)?;
- let config = crate::config()?.github;
+ /// Create a new `ReleaseTask` with the given `name` and `version`.
+ pub(crate) fn new(name: String, version: Version) -> Result {
+ let metadata = Metadata::load()?;
+
+ let package = metadata
+ .packages
+ .clone()
+ .into_iter()
+ .find(|p| p.name == name)
+ .ok_or(format!("Package {} not found in cargo metadata", name))?;
+
+ let config = crate::Config::load()?.github;
+
let http_client = HttpClient::new()?;
- Ok(Self { local_crate, project_root, http_client, config })
+ Ok(Self { metadata, package, version, http_client, config })
}
/// Run the task to effectively create a release.
- pub(crate) fn run(self) -> Result<()> {
+ pub(crate) fn run(&mut self) -> Result<()> {
let title = &self.title();
- let prerelease = self.local_crate.version.is_prerelease();
+ let prerelease = self.version.is_prerelease();
+ let publish_only = self.package.name == "ruma-identifiers-validation";
+
println!(
"Starting {} for {}…",
match prerelease {
@@ -69,15 +84,41 @@ impl ReleaseTask {
println!("Checking status of git repository…");
if !cmd!("git status -s -uno").read()?.is_empty()
- && !ask_yes_no("This git repository contains untracked files. Continue?")?
+ && !ask_yes_no("This git repository contains uncommitted changes. Continue?")?
{
return Ok(());
}
- if let Some(macros) = self.macros() {
- print!("Found macros crate. ");
- let _dir = pushd(¯os.path)?;
- let published = macros.publish(&self.http_client)?;
+ if self.package.version != self.version
+ && !self.package.version.is_next(&self.version)
+ && !ask_yes_no(&format!(
+ "Version {} should not follow version {}. Do you really want to continue?",
+ self.version, self.package.version,
+ ))?
+ {
+ return Ok(());
+ }
+
+ let mut macros = self.macros();
+
+ if let Some(m) = macros.as_mut() {
+ println!("Found macros crate {}.", m.name);
+
+ m.update_version(&self.version)?;
+ m.update_dependants(&self.metadata)?;
+
+ println!("Resuming release of {}…", self.title());
+ }
+
+ self.package.update_version(&self.version)?;
+ self.package.update_dependants(&self.metadata)?;
+
+ let changes = &self.package.changes(!prerelease)?;
+
+ self.commit()?;
+
+ if let Some(m) = macros {
+ let published = m.publish(&self.http_client)?;
if published {
// Crate was published, instead of publishing skipped (because release already
@@ -85,21 +126,15 @@ impl ReleaseTask {
println!("Waiting 10 seconds for the release to make it into the crates.io index…");
sleep(Duration::from_secs(10));
}
-
- println!("Resuming release of {}…", self.title());
}
- let _dir = pushd(&self.local_crate.path)?;
+ self.package.publish(&self.http_client)?;
- self.local_crate.publish(&self.http_client)?;
-
- if prerelease {
- println!("Pre-release created successfully!");
+ if publish_only {
+ println!("Crate published successfully!");
return Ok(());
}
- let changes = &self.local_crate.changes()?;
-
let tag = &self.tag_name();
println!("Creating git tag…");
@@ -116,6 +151,11 @@ impl ReleaseTask {
return Ok(());
}
+ if prerelease {
+ println!("Pre-release created successfully!");
+ return Ok(());
+ }
+
println!("Creating release on GitHub…");
let request_body = &json!({
"tag_name": tag,
@@ -132,13 +172,22 @@ impl ReleaseTask {
}
/// Get the associated `-macros` crate of the current crate, if any.
- fn macros(&self) -> Option {
- LocalCrate::new(format!("{}-macros", self.local_crate.name), &self.project_root).ok()
+ fn macros(&self) -> Option {
+ self.metadata
+ .packages
+ .clone()
+ .into_iter()
+ .find(|p| p.name == format!("{}-macros", self.package.name))
}
/// Get the title of this release.
fn title(&self) -> String {
- format!("{} {}", self.local_crate.name, self.local_crate.version)
+ format!("{} {}", self.package.name, self.version)
+ }
+
+ /// Get the tag name for this release.
+ fn tag_name(&self) -> String {
+ format!("{}-{}", self.package.name, self.version)
}
/// Load the GitHub config from the config file.
@@ -153,9 +202,47 @@ impl ReleaseTask {
Ok(remote)
}
- /// Get the tag name for this release.
- fn tag_name(&self) -> String {
- format!("{}-{}", self.local_crate.name, self.local_crate.version)
+ /// Commit and push all the changes in the git repository.
+ fn commit(&self) -> Result<()> {
+ let mut input = String::new();
+ let stdin = stdin();
+
+ let instructions = "Ready to commit the changes. [continue/abort/diff]: ";
+ print!("{}", instructions);
+ stdout().flush()?;
+
+ let mut handle = stdin.lock();
+
+ while let _ = handle.read_line(&mut input)? {
+ match input.trim().to_ascii_lowercase().as_str() {
+ "c" | "continue" => {
+ break;
+ }
+ "a" | "abort" => {
+ return Err("User aborted commit".into());
+ }
+ "d" | "diff" => {
+ cmd!("git diff").run()?;
+ }
+ _ => {
+ println!("Unknown command.");
+ }
+ }
+ print!("{}", instructions);
+ stdout().flush()?;
+
+ input.clear();
+ }
+
+ let message = format!("Release {}", self.title());
+
+ println!("Creating commit…");
+ cmd!("git commit -a -m {message}").read()?;
+
+ println!("Pushing commit…");
+ cmd!("git push").read()?;
+
+ Ok(())
}
/// Check if the tag for the current version of the crate has been pushed on GitHub.
@@ -187,104 +274,6 @@ impl ReleaseTask {
}
}
-/// A local Rust crate.
-#[derive(Debug)]
-struct LocalCrate {
- /// The name of the crate.
- name: String,
-
- /// The version of the crate.
- version: Version,
-
- /// The local path of the crate.
- path: PathBuf,
-}
-
-impl LocalCrate {
- /// Creates a new `Crate` with the given name and project root.
- pub fn new(name: String, project_root: &Path) -> Result {
- let path = project_root.join(&name);
-
- let version = Self::version(&path)?;
-
- Ok(Self { name, version, path })
- }
-
- /// The current version of the crate at `path` from its manifest.
- fn version(path: &Path) -> Result {
- let manifest_toml = read_file(path.join("Cargo.toml"))?;
- let manifest: CargoManifest = from_toml_str(&manifest_toml)?;
-
- Ok(manifest.package.version)
- }
-
- /// The changes of the given version from the changelog.
- fn changes(&self) -> Result {
- let changelog = read_file(self.path.join("CHANGELOG.md"))?;
- let lines_nb = changelog.lines().count();
- let mut lines = changelog.lines();
-
- let start = match lines.position(|l| l.starts_with(&format!("# {}", self.version))) {
- Some(p) => p + 1,
- None => {
- return Err("Could not find version title in changelog".into());
- }
- };
-
- let length = match lines.position(|l| l.starts_with("# ")) {
- Some(p) => p,
- None => lines_nb,
- };
-
- let changes = changelog.lines().skip(start).take(length).join("\n");
-
- Ok(changes.trim().to_owned())
- }
-
- /// Check if the current version of the crate is published on crates.io.
- fn is_published(&self, client: &HttpClient) -> Result {
- let response: CratesIoCrate =
- client.get(format!("{}/{}/{}", CRATESIO_API, self.name, self.version))?.json()?;
-
- Ok(response.version.is_some())
- }
-
- /// Publish this package on crates.io.
- fn publish(&self, client: &HttpClient) -> Result {
- println!("Publishing {} {} on crates.io…", self.name, self.version);
- if self.is_published(client)? {
- if ask_yes_no("This version is already published. Skip this step and continue?")? {
- Ok(false)
- } else {
- Err("Release interrupted by user.".into())
- }
- } else {
- cmd!("cargo publish").run()?;
- Ok(true)
- }
- }
-}
-
-/// The required cargo manifest data of a crate.
-#[derive(Debug, Deserialize)]
-struct CargoManifest {
- /// The package information.
- package: CargoPackage,
-}
-
-/// The required package information from a crate's cargo manifest.
-#[derive(Debug, Deserialize)]
-struct CargoPackage {
- /// The package version.
- version: Version,
-}
-
-/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
-#[derive(Deserialize)]
-struct CratesIoCrate {
- version: Option,
-}
-
/// A tag from the `GET /repos/{owner}/{repo}/tags` endpoint of GitHub REST API.
#[derive(Debug, Deserialize)]
struct GithubTag {
@@ -343,3 +332,103 @@ impl StrExt for str {
string + s
}
}
+
+/// Extra Version increment methods for crate release.
+trait VersionExt {
+ /// Adds a pre-release label and number if there is none.
+ fn add_pre_release(&mut self);
+
+ /// Increments the pre-release number, if this is a pre-release.
+ fn increment_pre_number(&mut self);
+
+ /// Increments the pre-release label from `alpha` to `beta` if this is a pre-release and it is
+ /// possible, otherwise does nothing.
+ fn increment_pre_label(&mut self);
+
+ /// If the given version can be the next after this one.
+ ///
+ /// This checks all the version bumps of the format MAJOR.MINOR.PATCH-PRE_LABEL.PRE_NUMBER, with
+ /// PRE_LABEL = alpha or beta.
+ fn is_next(&self, version: &Version) -> bool;
+}
+
+impl VersionExt for Version {
+ fn add_pre_release(&mut self) {
+ if !self.is_prerelease() {
+ self.pre = vec![Identifier::AlphaNumeric("alpha".into()), Identifier::Numeric(1)];
+ }
+ }
+
+ fn increment_pre_number(&mut self) {
+ if self.is_prerelease() {
+ if let Identifier::Numeric(n) = self.pre[1] {
+ self.pre[1] = Identifier::Numeric(n + 1);
+ }
+ }
+ }
+
+ fn increment_pre_label(&mut self) {
+ if self.is_prerelease() {
+ match &self.pre[0] {
+ Identifier::AlphaNumeric(n) if n == "alpha" => {
+ self.pre =
+ vec![Identifier::AlphaNumeric("beta".into()), Identifier::Numeric(1)];
+ }
+ _ => {}
+ }
+ }
+ }
+
+ fn is_next(&self, version: &Version) -> bool {
+ let mut next = self.clone();
+
+ if self.is_prerelease() {
+ next.increment_pre_number();
+ if next == *version {
+ return true;
+ }
+
+ next.increment_pre_label();
+ if next == *version {
+ return true;
+ }
+
+ next.pre = vec![];
+ if next == *version {
+ return true;
+ }
+ } else {
+ next.increment_patch();
+ if next == *version {
+ return true;
+ }
+
+ next.add_pre_release();
+ if next == *version {
+ return true;
+ }
+
+ next.increment_minor();
+ if next == *version {
+ return true;
+ }
+
+ next.add_pre_release();
+ if next == *version {
+ return true;
+ }
+
+ next.increment_major();
+ if next == *version {
+ return true;
+ }
+
+ next.add_pre_release();
+ if next == *version {
+ return true;
+ }
+ }
+
+ false
+ }
+}