Repository: MarcoIeni/release-plz (rust)

Issue:
allow to modify the logic of next_version to add more prefixes that will bump versions
## Motivations

<!--
If your feature request is related to a problem, please describe it.
-->

Our project uses conventional commits, and with git-cliff, it's awesome. now we added a new prefix (let's say `abc`) that should also bump the minor (and it makes sense in the context of the repo).

now we can't do that because it's hard coded in the repo that only feat and breaking change will bump the minor.

- Would you like to implement this feature? maybe

## Solution

I can see how it can be solved by receiving a regex for major and minor that will check against the `Custom` [conventional_commit_parser](https://docs.rs/conventional_commit_parser/latest/conventional_commit_parser/index.html)::[commit](https://docs.rs/conventional_commit_parser/latest/conventional_commit_parser/commit/index.html)::[CommitType](https://docs.rs/conventional_commit_parser/latest/conventional_commit_parser/commit/enum.CommitType.html#)
<!-- Describe the solution you'd like. -->

## Alternatives

<!-- Describe any alternative solutions or features you've considered. -->

## Additional context

I would like this so git-cliff will be able to configure the regex from its config and then I could use it in my repo to release and bump automatically 
<!-- Add any other context or screenshots about the feature request here. -->


Pull request description:
feat(next_version): support bumping version based on configurable custom pattern
this MR closes #1510 

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 57062f4e..a9d43c12 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3114,6 +3114,7 @@ name = "next_version"
 version = "0.2.17"
 dependencies = [
  "conventional_commit_parser",
+ "regex",
  "semver",
 ]
 
diff --git a/crates/next_version/Cargo.toml b/crates/next_version/Cargo.toml
index 717b0554..c3761259 100644
--- a/crates/next_version/Cargo.toml
+++ b/crates/next_version/Cargo.toml
@@ -12,5 +12,6 @@ categories = ["development-tools", "parsing"]
 workspace = true
 
 [dependencies]
+regex.workspace = true
 semver.workspace = true
 conventional_commit_parser.workspace = true
diff --git a/crates/next_version/src/version_increment.rs b/crates/next_version/src/version_increment.rs
index 38d19f10..3a1d4568 100644
--- a/crates/next_version/src/version_increment.rs
+++ b/crates/next_version/src/version_increment.rs
@@ -1,4 +1,5 @@
 use conventional_commit_parser::commit::{CommitType, ConventionalCommit};
+use regex::Regex;
 use semver::Version;
 
 use crate::{NextVersion, VersionUpdater};
@@ -11,6 +12,24 @@ pub enum VersionIncrement {
     Prerelease,
 }
 
+fn is_there_a_custom_match(regex_option: Option<&Regex>, commits: &[ConventionalCommit]) -> bool {
+    if let Some(regex) = regex_option {
+        commits
+            .iter()
+            .any(|commit| custom_commit_matches_regex(regex, commit))
+    } else {
+        false
+    }
+}
+
+fn custom_commit_matches_regex(regex: &Regex, commit: &ConventionalCommit) -> bool {
+    if let CommitType::Custom(custom_type) = &commit.commit_type {
+        regex.is_match(custom_type)
+    } else {
+        false
+    }
+}
+
 impl VersionIncrement {
     /// Analyze commits and determine which part of version to increment based on
     /// [conventional commits](https://www.conventionalcommits.org/) and
@@ -102,7 +121,8 @@ impl VersionIncrement {
         let is_there_a_breaking_change = commits.iter().any(|commit| commit.is_breaking_change);
 
         let is_major_bump = || {
-            is_there_a_breaking_change
+            (is_there_a_breaking_change
+                || is_there_a_custom_match(updater.custom_major_increment_regex.as_ref(), commits))
                 && (current.major != 0 || updater.breaking_always_increment_major)
         };
 
@@ -113,7 +133,9 @@ impl VersionIncrement {
             };
             let is_breaking_bump =
                 || current.major == 0 && current.minor != 0 && is_there_a_breaking_change;
-            is_feat_bump() || is_breaking_bump()
+            is_feat_bump()
+                || is_breaking_bump()
+                || is_there_a_custom_match(updater.custom_minor_increment_regex.as_ref(), commits)
         };
 
         if is_major_bump() {
@@ -136,3 +158,62 @@ impl VersionIncrement {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use conventional_commit_parser::commit::{CommitType, ConventionalCommit};
+    use regex::Regex;
+    #[test]
+    fn returns_true_for_matching_custom_type() {
+        let regex = Regex::new(r"custom").unwrap();
+        let commits = vec![ConventionalCommit {
+            commit_type: CommitType::Custom("custom".to_string()),
+            is_breaking_change: false,
+            summary: "A custom commit".to_string(),
+            body: None,
+            scope: None,
+            footers: vec![],
+        }];
+
+        assert!(is_there_a_custom_match(Some(&regex), &commits));
+    }
+
+    #[test]
+    fn returns_false_for_non_custom_commit_types() {
+        let regex = Regex::new(r"custom").unwrap();
+        let commits = vec![ConventionalCommit {
+            commit_type: CommitType::Feature,
+            is_breaking_change: false,
+            summary: "A feature commit".to_string(),
+            body: None,
+            scope: None,
+            footers: vec![],
+        }];
+
+        assert!(!is_there_a_custom_match(Some(&regex), &commits));
+    }
+
+    #[test]
+    fn returns_false_for_empty_commits_list() {
+        let regex = Regex::new(r"custom").unwrap();
+        let commits: Vec<ConventionalCommit> = Vec::new();
+
+        assert!(!is_there_a_custom_match(Some(&regex), &commits));
+    }
+
+    #[test]
+    fn handles_commits_with_empty_custom_types() {
+        let regex = Regex::new(r"custom").unwrap();
+        let commits = vec![ConventionalCommit {
+            commit_type: CommitType::Custom("".to_string()),
+            is_breaking_change: false,
+            summary: "A custom commit".to_string(),
+            body: None,
+            scope: None,
+            footers: vec![],
+        }];
+
+        assert!(!is_there_a_custom_match(Some(&regex), &commits));
+    }
+}
diff --git a/crates/next_version/src/version_updater.rs b/crates/next_version/src/version_updater.rs
index e44bbfcb..2039bf29 100644
--- a/crates/next_version/src/version_updater.rs
+++ b/crates/next_version/src/version_updater.rs
@@ -1,3 +1,4 @@
+use regex::Regex;
 use semver::Version;
 
 use crate::VersionIncrement;
@@ -24,6 +25,8 @@ use crate::VersionIncrement;
 pub struct VersionUpdater {
     pub(crate) features_always_increment_minor: bool,
     pub(crate) breaking_always_increment_major: bool,
+    pub(crate) custom_major_increment_regex: Option<Regex>,
+    pub(crate) custom_minor_increment_regex: Option<Regex>,
 }
 
 impl Default for VersionUpdater {
@@ -54,6 +57,8 @@ impl VersionUpdater {
         Self {
             features_always_increment_minor: false,
             breaking_always_increment_major: false,
+            custom_major_increment_regex: None,
+            custom_minor_increment_regex: None,
         }
     }
 
@@ -128,6 +133,82 @@ impl VersionUpdater {
         self
     }
 
+    /// Configure a custom regex pattern for major version increments.
+    /// This will check only the type of the commit against the given pattern.
+    ///
+    /// Default: `None`.
+    ///
+    /// ### Note
+    /// `commit type` according to the spec is only `[a-zA-Z]+`
+    ///
+    /// ### Example
+    ///
+    /// ```rust
+    /// use semver::Version;
+    /// use next_version::VersionUpdater;
+    ///
+    /// let commits = ["abc: incompatible change"];
+    /// let version = Version::new(1, 2, 3);
+    /// assert_eq!(
+    ///     VersionUpdater::new()
+    ///         .with_custom_major_increment_regex("abc")
+    ///         .expect("invalid regex")
+    ///         .increment(&version, &commits),
+    ///     Version::new(2, 0, 0)
+    /// );
+    /// assert_eq!(
+    ///     VersionUpdater::new()
+    ///         .increment(&version, &commits),
+    ///     Version::new(1, 2, 4)
+    /// );
+    /// ```
+    pub fn with_custom_major_increment_regex(
+        mut self,
+        custom_major_increment_regex: &str,
+    ) -> Result<Self, regex::Error> {
+        let regex = Regex::new(custom_major_increment_regex)?;
+        self.custom_major_increment_regex = Some(regex);
+        Ok(self)
+    }
+
+    /// Configures a custom regex pattern for minor version increments.
+    /// This will check only the type of the commit against the given pattern.
+    ///
+    /// Default: `None`.
+    ///
+    /// ### Note
+    /// `commit type` according to the spec is only `[a-zA-Z]+`
+    ///
+    /// ### Example
+    ///
+    /// ```rust
+    /// use semver::Version;
+    /// use next_version::VersionUpdater;
+    ///
+    /// let commits = ["bbb: make coffee"];
+    /// let version = Version::new(0, 2, 3);
+    /// assert_eq!(
+    ///     VersionUpdater::new()
+    ///         .with_custom_minor_increment_regex("abc|bbb")
+    ///         .expect("invalid regex")
+    ///         .increment(&version, &commits),
+    ///     Version::new(0, 3, 0)
+    /// );
+    /// assert_eq!(
+    ///     VersionUpdater::new()
+    ///         .increment(&version, &commits),
+    ///     Version::new(0, 2, 4)
+    /// );
+    /// ```
+    pub fn with_custom_minor_increment_regex(
+        mut self,
+        custom_minor_increment_regex: &str,
+    ) -> Result<Self, regex::Error> {
+        let regex = Regex::new(custom_minor_increment_regex)?;
+        self.custom_minor_increment_regex = Some(regex);
+        Ok(self)
+    }
+
     /// Analyze commits and determine the next version.
     pub fn increment<I>(self, version: &Version, commits: I) -> Version
     where


Test patch:
diff --git a/crates/next_version/tests/all/normal.rs b/crates/next_version/tests/all/normal.rs
index de79d07b..60556e26 100644
--- a/crates/next_version/tests/all/normal.rs
+++ b/crates/next_version/tests/all/normal.rs
@@ -68,3 +68,29 @@ fn commit_with_breaking_change_increments_major_version_when_major_is_zero() {
         Version::new(1, 0, 0)
     );
 }
+
+#[test]
+fn commit_with_custom_major_increment_regex_increments_major_version() {
+    let commits = ["major: some changes"];
+    let version = Version::new(1, 2, 3);
+    assert_eq!(
+        VersionUpdater::new()
+            .with_custom_major_increment_regex("another|major")
+            .unwrap()
+            .increment(&version, commits),
+        Version::new(2, 0, 0)
+    );
+}
+
+#[test]
+fn commit_with_custom_minor_increment_regex_increments_minor_version() {
+    let commits = ["minor: some changes"];
+    let version = Version::new(1, 2, 3);
+    assert_eq!(
+        VersionUpdater::new()
+            .with_custom_minor_increment_regex("minor")
+            .unwrap()
+            .increment(&version, commits),
+        Version::new(1, 3, 0)
+    );
+}