Repository: MarcoIeni/release-plz
Pull request #425 (2023-01-09 21:11:15)

feat: handle pre-releases
<!-- Please explain the changes you made -->

<!--
Please, make sure:
- you have read the contributing guidelines:
  https://github.com/MarcoIeni/release-plz/blob/main/CONTRIBUTING.md
- you have formatted the code using rustfmt:
  https://github.com/rust-lang/rustfmt
- you have checked that all tests pass, by running `cargo test --all`
-->
Closes https://github.com/MarcoIeni/release-plz/issues/174


Patch:
diff --git a/crates/next_version/src/lib.rs b/crates/next_version/src/lib.rs
index ab218013..2c9a8214 100644
--- a/crates/next_version/src/lib.rs
+++ b/crates/next_version/src/lib.rs
@@ -41,6 +41,25 @@
 //!
 //! assert_eq!(Version::new(0, 4, 4).next(&commits), Version::new(0, 5, 0));
 //! ```
+//!
+//! Pre-release versions are incremented in the same way, independently
+//! by the type of commits:
+//!
+//! ```rust
+//! use semver::Version;
+//! use next_version::NextVersion;
+//!
+//! let commits = vec!["feat!: break user"];
+//! let version = Version::parse("1.0.0-alpha.1.2").unwrap();
+//! let expected = Version::parse("1.0.0-alpha.1.3").unwrap();
+//! assert_eq!(version.next(commits.clone()), expected);
+//!
+//! // If the pre-release doesn't contain a version, `.1` is appended.
+//! let version = Version::parse("1.0.0-beta").unwrap();
+//! let expected = Version::parse("1.0.0-beta.1").unwrap();
+//! assert_eq!(version.next(commits), expected);
+//!
+//! ```
 
 mod next_version;
 mod version_increment;
diff --git a/crates/next_version/src/next_version.rs b/crates/next_version/src/next_version.rs
index f661e6e7..c79438eb 100644
--- a/crates/next_version/src/next_version.rs
+++ b/crates/next_version/src/next_version.rs
@@ -8,12 +8,14 @@ pub trait NextVersion {
         I: IntoIterator,
         I::Item: AsRef<str>;
 
-    /// Increments the major version number for this Version.
+    /// Increments the major version number.
     fn increment_major(&self) -> Self;
-    /// Increments the minor version number for this Version.
+    /// Increments the minor version number.
     fn increment_minor(&self) -> Self;
-    /// Increments the patch version number for this Version.
+    /// Increments the patch version number.
     fn increment_patch(&self) -> Self;
+    /// Increments the prerelease version number.
+    fn increment_prerelease(&self) -> Self;
 }
 
 impl NextVersion for Version {
@@ -78,4 +80,29 @@ impl NextVersion for Version {
             build: semver::BuildMetadata::EMPTY,
         }
     }
+
+    fn increment_prerelease(&self) -> Self {
+        let next_pre = increment_last_identifier(self.pre.as_str());
+        let next_pre = semver::Prerelease::new(&next_pre).expect("pre release increment failed. Please report this issue to https://github.com/MarcoIeni/release-plz/issues");
+        Self {
+            major: self.major,
+            minor: self.minor,
+            patch: self.patch,
+            pre: next_pre,
+            build: semver::BuildMetadata::EMPTY,
+        }
+    }
+}
+
+fn increment_last_identifier(release: &str) -> String {
+    match release.rsplit_once('.') {
+        Some((left, right)) => {
+            if let Ok(right_num) = right.parse::<u32>() {
+                format!("{left}.{}", right_num + 1)
+            } else {
+                format!("{release}.1")
+            }
+        }
+        None => format!("{release}.1"),
+    }
 }
diff --git a/crates/next_version/src/version_increment.rs b/crates/next_version/src/version_increment.rs
index a0734972..c9466636 100644
--- a/crates/next_version/src/version_increment.rs
+++ b/crates/next_version/src/version_increment.rs
@@ -7,6 +7,7 @@ pub enum VersionIncrement {
     Major,
     Minor,
     Patch,
+    Prerelease,
 }
 
 impl VersionIncrement {
@@ -14,6 +15,8 @@ impl VersionIncrement {
     /// [conventional commits](https://www.conventionalcommits.org/) and
     /// [Semantic versioning](https://semver.org/).
     /// - If no commits are present, [`Option::None`] is returned, because the version should not be incremented.
+    /// - If some commits are present and [`semver::Prerelease`] is not empty, the version increment is
+    ///   [`VersionIncrement::Prerelease`].
     /// - If some commits are present, but none of them match conventional commits specification,
     ///   the version increment is [`VersionIncrement::Patch`].
     /// - If some commits match conventional commits, then the next version is calculated by using
@@ -28,6 +31,9 @@ impl VersionIncrement {
         if !are_commits_present {
             None
         } else {
+            if !current_version.pre.is_empty() {
+                return Some(VersionIncrement::Prerelease);
+            }
             // Parse commits and keep only the ones that follow conventional commits specification.
             let commits: Vec<ConventionalCommit> = commits
                 .filter_map(|c| conventional_commit_parser::parse(c.as_ref()).ok())
@@ -70,6 +76,7 @@ impl VersionIncrement {
             Self::Major => version.increment_major(),
             Self::Minor => version.increment_minor(),
             Self::Patch => version.increment_patch(),
+            Self::Prerelease => version.increment_prerelease(),
         }
     }
 }


Test patch:
diff --git a/crates/next_version/tests/all/main.rs b/crates/next_version/tests/all/main.rs
index 855cabd1..8435f382 100644
--- a/crates/next_version/tests/all/main.rs
+++ b/crates/next_version/tests/all/main.rs
@@ -1,37 +1,2 @@
-use next_version::NextVersion;
-use semver::Version;
-
-#[test]
-fn commit_without_semver_prefix_increments_patch_version() {
-    let commits = vec!["my change"];
-    let version = Version::new(1, 2, 3);
-    assert_eq!(version.next(commits), Version::new(1, 2, 4));
-}
-
-#[test]
-fn commit_with_fix_semver_prefix_increments_patch_version() {
-    let commits = vec!["my change", "fix: serious bug"];
-    let version = Version::new(1, 2, 3);
-    assert_eq!(version.next(commits), Version::new(1, 2, 4));
-}
-
-#[test]
-fn commit_with_feat_semver_prefix_increments_patch_version() {
-    let commits = vec!["feat: make coffe"];
-    let version = Version::new(1, 3, 3);
-    assert_eq!(version.next(commits), Version::new(1, 4, 0));
-}
-
-#[test]
-fn commit_with_breaking_change_increments_major_version() {
-    let commits = vec!["feat!: break user"];
-    let version = Version::new(1, 2, 3);
-    assert_eq!(version.next(commits), Version::new(2, 0, 0));
-}
-
-#[test]
-fn commit_with_breaking_change_increments_minor_version_when_major_is_zero() {
-    let commits = vec!["feat!: break user"];
-    let version = Version::new(0, 2, 3);
-    assert_eq!(version.next(commits), Version::new(0, 3, 0));
-}
+mod normal;
+mod pre_release;
diff --git a/crates/next_version/tests/all/normal.rs b/crates/next_version/tests/all/normal.rs
new file mode 100644
index 00000000..855cabd1
--- /dev/null
+++ b/crates/next_version/tests/all/normal.rs
@@ -0,0 +1,37 @@
+use next_version::NextVersion;
+use semver::Version;
+
+#[test]
+fn commit_without_semver_prefix_increments_patch_version() {
+    let commits = vec!["my change"];
+    let version = Version::new(1, 2, 3);
+    assert_eq!(version.next(commits), Version::new(1, 2, 4));
+}
+
+#[test]
+fn commit_with_fix_semver_prefix_increments_patch_version() {
+    let commits = vec!["my change", "fix: serious bug"];
+    let version = Version::new(1, 2, 3);
+    assert_eq!(version.next(commits), Version::new(1, 2, 4));
+}
+
+#[test]
+fn commit_with_feat_semver_prefix_increments_patch_version() {
+    let commits = vec!["feat: make coffe"];
+    let version = Version::new(1, 3, 3);
+    assert_eq!(version.next(commits), Version::new(1, 4, 0));
+}
+
+#[test]
+fn commit_with_breaking_change_increments_major_version() {
+    let commits = vec!["feat!: break user"];
+    let version = Version::new(1, 2, 3);
+    assert_eq!(version.next(commits), Version::new(2, 0, 0));
+}
+
+#[test]
+fn commit_with_breaking_change_increments_minor_version_when_major_is_zero() {
+    let commits = vec!["feat!: break user"];
+    let version = Version::new(0, 2, 3);
+    assert_eq!(version.next(commits), Version::new(0, 3, 0));
+}
diff --git a/crates/next_version/tests/all/pre_release.rs b/crates/next_version/tests/all/pre_release.rs
new file mode 100644
index 00000000..8a77ef1f
--- /dev/null
+++ b/crates/next_version/tests/all/pre_release.rs
@@ -0,0 +1,42 @@
+use next_version::NextVersion;
+use semver::Version;
+
+#[test]
+fn commit_without_semver_prefix_increments_pre_release_version() {
+    let commits = vec!["my change"];
+    let version = Version::parse("1.0.0-alpha.2").unwrap();
+    let expected = Version::parse("1.0.0-alpha.3").unwrap();
+    assert_eq!(version.next(commits), expected);
+}
+
+#[test]
+fn commit_with_breaking_change_increments_pre_release_version() {
+    let commits = vec!["feat!: break user"];
+    let version = Version::parse("1.0.0-alpha.2").unwrap();
+    let expected = Version::parse("1.0.0-alpha.3").unwrap();
+    assert_eq!(version.next(commits), expected);
+}
+
+#[test]
+fn dot_1_is_added_to_unversioned_pre_release() {
+    let commits = vec!["feat!: break user"];
+    let version = Version::parse("1.0.0-alpha").unwrap();
+    let expected = Version::parse("1.0.0-alpha.1").unwrap();
+    assert_eq!(version.next(commits), expected);
+}
+
+#[test]
+fn dot_1_is_added_to_last_identifier_in_pre_release() {
+    let commits = vec!["feat!: break user"];
+    let version = Version::parse("1.0.0-beta.1.2").unwrap();
+    let expected = Version::parse("1.0.0-beta.1.3").unwrap();
+    assert_eq!(version.next(commits), expected);
+}
+
+#[test]
+fn dot_1_is_added_to_character_identifier_in_pre_release() {
+    let commits = vec!["feat!: break user"];
+    let version = Version::parse("1.0.0-beta.1.a").unwrap();
+    let expected = Version::parse("1.0.0-beta.1.a.1").unwrap();
+    assert_eq!(version.next(commits), expected);
+}