Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(next_version): support configuration #1168

Merged
merged 18 commits into from
Dec 30, 2023
26 changes: 26 additions & 0 deletions crates/next_version/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// Configuration for version incrementation rules.
#[derive(Debug)]
pub struct NextVersionConfig {
/// Indicates whether uncontrolled minor version bumps are enabled.
/// When false, minor version increments are not performed and they
/// should be manually done to signal higher API stability to the user.
orhun marked this conversation as resolved.
Show resolved Hide resolved
///
/// Default value is `false`.
orhun marked this conversation as resolved.
Show resolved Hide resolved
pub uncontrolled_minor_bump: bool,

/// Indicates whether initial major version increments are enabled.
/// When true, allows for a major version increment from 0 to 1,
/// indicating a breaking change in the API.
///
/// Default value is `false`.
pub initial_major_increment: bool,
orhun marked this conversation as resolved.
Show resolved Hide resolved
}

impl Default for NextVersionConfig {
fn default() -> Self {
NextVersionConfig {
uncontrolled_minor_bump: false,
initial_major_increment: false,
}
}
}
3 changes: 2 additions & 1 deletion crates/next_version/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
//! assert_eq!(version.next(commits.clone()), expected);
//! ```

mod config;
mod next_version;
mod version_increment;

pub use crate::{next_version::*, version_increment::*};
pub use crate::{config::NextVersionConfig, next_version::*, version_increment::*};
8 changes: 4 additions & 4 deletions crates/next_version/src/next_version.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use semver::Version;

use crate::VersionIncrement;
use crate::{NextVersionConfig, VersionIncrement};

pub trait NextVersion {
fn next<I>(&self, commits: I) -> Self
fn next<I>(&self, commits: I, config: Option<NextVersionConfig>) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>;
Expand Down Expand Up @@ -36,12 +36,12 @@ impl NextVersion for Version {
/// let version = Version::new(0, 3, 3);
/// assert_eq!(version.next(commits), Version::new(0, 3, 4));
/// ```
fn next<I>(&self, commits: I) -> Self
fn next<I>(&self, commits: I, config: Option<NextVersionConfig>) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let increment = VersionIncrement::from_commits(self, commits);
let increment = VersionIncrement::from_commits(self, commits, config);
match increment {
Some(increment) => increment.bump(self),
None => self.clone(),
Expand Down
38 changes: 31 additions & 7 deletions crates/next_version/src/version_increment.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use conventional_commit_parser::commit::{CommitType, ConventionalCommit};
use semver::Version;

use crate::NextVersion;
use crate::{NextVersion, NextVersionConfig};

#[derive(Debug, PartialEq, Eq)]
pub enum VersionIncrement {
Expand All @@ -22,7 +22,11 @@ impl VersionIncrement {
/// the version increment is [`VersionIncrement::Patch`].
/// - If some commits match conventional commits, then the next version is calculated by using
/// [these](https://www.conventionalcommits.org/en/v1.0.0/#how-does-this-relate-to-semverare) rules.
pub fn from_commits<I>(current_version: &Version, commits: I) -> Option<Self>
pub fn from_commits<I>(
current_version: &Version,
commits: I,
config: Option<NextVersionConfig>,
) -> Option<Self>
where
I: IntoIterator,
I::Item: AsRef<str>,
Expand All @@ -40,7 +44,11 @@ impl VersionIncrement {
.filter_map(|c| conventional_commit_parser::parse(c.as_ref()).ok())
.collect();

Some(Self::from_conventional_commits(current_version, &commits))
Some(Self::from_conventional_commits(
current_version,
&commits,
config,
))
}
}

Expand Down Expand Up @@ -71,7 +79,13 @@ impl VersionIncrement {
}

/// If no conventional commits are present, the version is incremented as a Patch
fn from_conventional_commits(current: &Version, commits: &[ConventionalCommit]) -> Self {
fn from_conventional_commits(
current: &Version,
commits: &[ConventionalCommit],
config: Option<NextVersionConfig>,
) -> Self {
let config = config.unwrap_or_default();

let is_there_a_feature = || {
commits
.iter()
Expand All @@ -80,11 +94,21 @@ impl VersionIncrement {

let is_there_a_breaking_change = || commits.iter().any(|commit| commit.is_breaking_change);

let is_major_bump = || current.major != 0 && is_there_a_breaking_change();
let is_major_bump = || {
if config.initial_major_increment {
is_there_a_breaking_change()
} else {
current.major != 0 && is_there_a_breaking_change()
}
};

let is_minor_bump = || {
(current.major != 0 && is_there_a_feature())
|| (current.major == 0 && current.minor != 0 && is_there_a_breaking_change())
if !config.uncontrolled_minor_bump {
(current.major != 0 && is_there_a_feature())
|| (current.major == 0 && current.minor != 0 && is_there_a_breaking_change())
} else {
(is_there_a_feature()) || (current.minor != 0 && is_there_a_breaking_change())
}
};

if is_major_bump() {
Expand Down
51 changes: 45 additions & 6 deletions crates/next_version/tests/all/normal.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,76 @@
use next_version::NextVersion;
use next_version::{NextVersion, NextVersionConfig};
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));
assert_eq!(version.next(commits, None), 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));
assert_eq!(version.next(commits, None), 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));
assert_eq!(version.next(commits, None), Version::new(1, 4, 0));
}

#[test]
fn commit_with_feat_semver_prefix_increments_patch_version_when_major_is_zero() {
let commits = vec!["feat: make coffee"];
let version = Version::new(0, 2, 3);
assert_eq!(version.next(commits, None), Version::new(0, 2, 4));
}

#[test]
fn commit_with_feat_semver_prefix_increments_minor_version_when_major_is_zero() {
let commits = vec!["feat: make coffee"];
let version = Version::new(0, 2, 3);
assert_eq!(
version.next(
commits,
Some(NextVersionConfig {
uncontrolled_minor_bump: true,
initial_major_increment: false
})
),
Version::new(0, 3, 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));
assert_eq!(version.next(commits, None), 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));
assert_eq!(version.next(commits, None), Version::new(0, 3, 0));
}

#[test]
fn commit_with_breaking_change_increments_major_version_when_major_is_zero() {
let commits = vec!["feat!: break user"];
let version = Version::new(0, 2, 3);
assert_eq!(
version.next(
commits,
Some(NextVersionConfig {
uncontrolled_minor_bump: false,
initial_major_increment: true
})
),
Version::new(1, 0, 0)
);
}
10 changes: 5 additions & 5 deletions crates/next_version/tests/all/pre_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,37 @@ 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);
assert_eq!(version.next(commits, None), 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);
assert_eq!(version.next(commits, None), 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);
assert_eq!(version.next(commits, None), 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);
assert_eq!(version.next(commits, None), 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);
assert_eq!(version.next(commits, None), expected);
}
Loading