diff --git a/.github/fixtures/test-bump-version-keep-zerover/cliff.toml b/.github/fixtures/test-bump-version-keep-zerover/cliff.toml new file mode 100644 index 0000000000..15414304ae --- /dev/null +++ b/.github/fixtures/test-bump-version-keep-zerover/cliff.toml @@ -0,0 +1,31 @@ +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing whitespace from the templates +trim = true + +[bump] +features_always_bump_minor = false +breaking_always_bump_major = false diff --git a/.github/fixtures/test-bump-version-keep-zerover/commit.sh b/.github/fixtures/test-bump-version-keep-zerover/commit.sh new file mode 100755 index 0000000000..0fd84c20f6 --- /dev/null +++ b/.github/fixtures/test-bump-version-keep-zerover/commit.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +GIT_COMMITTER_DATE="2021-01-23 01:23:45" git commit --allow-empty -m "feat: add feature 1" +GIT_COMMITTER_DATE="2021-01-23 01:23:45" git commit --allow-empty -m "feat: add feature 2" +git tag v0.1.0 + +GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "feat!: add breaking feature" +GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "fix: fix feature 2" diff --git a/.github/fixtures/test-bump-version-keep-zerover/expected.md b/.github/fixtures/test-bump-version-keep-zerover/expected.md new file mode 100644 index 0000000000..b222642be6 --- /dev/null +++ b/.github/fixtures/test-bump-version-keep-zerover/expected.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.2.0] + +### Feat + +- [**breaking**] Add breaking feature + +### Fix + +- Fix feature 2 + +## [0.1.0] + +### Feat + +- Add feature 1 +- Add feature 2 + + diff --git a/.github/fixtures/test-bump-version/commit.sh b/.github/fixtures/test-bump-version/commit.sh index 3d7ef4718b..0fd84c20f6 100755 --- a/.github/fixtures/test-bump-version/commit.sh +++ b/.github/fixtures/test-bump-version/commit.sh @@ -5,5 +5,5 @@ GIT_COMMITTER_DATE="2021-01-23 01:23:45" git commit --allow-empty -m "feat: add GIT_COMMITTER_DATE="2021-01-23 01:23:45" git commit --allow-empty -m "feat: add feature 2" git tag v0.1.0 -GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "fix: fix feature 1" +GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "feat!: add breaking feature" GIT_COMMITTER_DATE="2021-01-23 01:23:46" git commit --allow-empty -m "fix: fix feature 2" diff --git a/.github/fixtures/test-bump-version/expected.md b/.github/fixtures/test-bump-version/expected.md index a42c65aca8..5fbc9b8aeb 100644 --- a/.github/fixtures/test-bump-version/expected.md +++ b/.github/fixtures/test-bump-version/expected.md @@ -2,11 +2,14 @@ All notable changes to this project will be documented in this file. -## [0.1.1] +## [1.0.0] + +### Feat + +- [**breaking**] Add breaking feature ### Fix -- Fix feature 1 - Fix feature 2 ## [0.1.0] diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs index 358d7d4d04..ac0998ea1f 100644 --- a/git-cliff-core/src/changelog.rs +++ b/git-cliff-core/src/changelog.rs @@ -200,7 +200,8 @@ impl<'a> Changelog<'a> { pub fn bump_version(&mut self) -> Result> { if let Some(ref mut last_release) = self.releases.iter_mut().next() { if last_release.version.is_none() { - let next_version = last_release.calculate_next_version()?; + let next_version = last_release + .calculate_next_version_with_config(&self.config.bump)?; debug!("Bumping the version to {next_version}"); last_release.version = Some(next_version.to_string()); last_release.timestamp = SystemTime::now() @@ -307,6 +308,7 @@ impl<'a> Changelog<'a> { mod test { use super::*; use crate::config::{ + Bump, ChangelogConfig, CommitParser, GitConfig, @@ -485,6 +487,7 @@ mod test { token: None, }, }, + bump: Bump::default(), }; let test_release = Release { version: Some(String::from("v1.0.0")), diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs index f0f5be9805..90caea4ff0 100644 --- a/git-cliff-core/src/config.rs +++ b/git-cliff-core/src/config.rs @@ -33,6 +33,9 @@ pub struct Config { /// Configuration values about remote. #[serde(default)] pub remote: RemoteConfig, + /// Configuration values about bump version. + #[serde(default)] + pub bump: Bump, } /// Changelog configuration. @@ -136,6 +139,29 @@ impl Remote { } } +/// Bump version configuration. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Bump { + /// Configures automatic minor version increments for feature changes. + /// + /// When `true`, a feature will always trigger a minor version update. + /// When `false`, a feature will trigger: + /// + /// - A patch version update if the major version is 0. + /// - A minor version update otherwise. + pub features_always_bump_minor: Option, + + /// Configures 0 -> 1 major version increments for breaking changes. + /// + /// When `true`, a breaking change commit will always trigger a major + /// version update (including the transition from version 0 to 1) + /// When `false`, a breaking change commit will trigger: + /// + /// - A minor version update if the major version is 0. + /// - A major version update otherwise. + pub breaking_always_bump_major: Option, +} + /// Parser for grouping commits. #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct CommitParser { diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs index b1930daf3b..34a5c40557 100644 --- a/git-cliff-core/src/release.rs +++ b/git-cliff-core/src/release.rs @@ -1,4 +1,5 @@ use crate::commit::Commit; +use crate::config::Bump; use crate::error::Result; #[cfg(feature = "github")] use crate::github::{ @@ -98,7 +99,21 @@ impl<'a> Release<'a> { } /// Calculates the next version based on the commits. + /// + /// It uses the default bump version configuration to calculate the next + /// version. pub fn calculate_next_version(&self) -> Result { + self.calculate_next_version_with_config(&Bump::default()) + } + + /// Calculates the next version based on the commits. + /// + /// It uses the given bump version configuration to calculate the next + /// version. + pub(super) fn calculate_next_version_with_config( + &self, + config: &Bump, + ) -> Result { match self .previous .as_ref() @@ -126,8 +141,12 @@ impl<'a> Release<'a> { } } let next_version = VersionUpdater::new() - .with_features_always_increment_minor(true) - .with_breaking_always_increment_major(true) + .with_features_always_increment_minor( + config.features_always_bump_minor.unwrap_or(true), + ) + .with_breaking_always_increment_major( + config.breaking_always_bump_major.unwrap_or(true), + ) .increment( &semver?, self.commits @@ -169,7 +188,27 @@ mod test { use super::*; #[test] fn bump_version() -> Result<()> { - for (version, expected_version, commits) in [ + fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> { + Release { + version: None, + commits: commits + .iter() + .map(|v| Commit::from(v.to_string())) + .collect(), + commit_id: None, + timestamp: 0, + previous: Some(Box::new(Release { + version: Some(String::from(version)), + ..Default::default() + })), + #[cfg(feature = "github")] + github: crate::github::GitHubReleaseMetadata { + contributors: vec![], + }, + } + } + + let test_shared = [ ("1.0.0", "1.1.0", vec!["feat: add xyz", "fix: fix xyz"]), ("1.0.0", "1.0.1", vec!["fix: add xyz", "fix: aaaaaa"]), ("1.0.0", "2.0.0", vec!["feat!: add xyz", "feat: zzz"]), @@ -202,27 +241,87 @@ mod test { "aaa#/@#$@9384!#%^#@#@!#!239432413-idk-9999.2200.5932-alpha.420", vec!["feat: damn this is working"], ), - ] { - let release = Release { - version: None, - commits: commits - .into_iter() - .map(|v| Commit::from(v.to_string())) - .collect(), - commit_id: None, - timestamp: 0, - previous: Some(Box::new(Release { - version: Some(String::from(version)), - ..Default::default() - })), - #[cfg(feature = "github")] - github: crate::github::GitHubReleaseMetadata { - contributors: vec![], - }, - }; + ]; + + for (version, expected_version, commits) in test_shared.iter().chain( + [ + ("0.0.1", "0.0.2", vec!["fix: fix xyz"]), + ("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]), + ("0.1.0", "0.1.1", vec!["fix: fix xyz"]), + ("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]), + ] + .iter(), + ) { + let release = build_release(version, commits); let next_version = release.calculate_next_version()?; - assert_eq!(expected_version, next_version); + assert_eq!(expected_version, &next_version); + let next_version = + release.calculate_next_version_with_config(&Bump::default())?; + assert_eq!(expected_version, &next_version); + } + + for (version, expected_version, commits) in test_shared.iter().chain( + [ + ("0.0.1", "0.0.2", vec!["fix: fix xyz"]), + ("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.0.1", "0.0.2", vec!["feat!: add xyz", "feat: zzz"]), + ("0.1.0", "0.1.1", vec!["fix: fix xyz"]), + ("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]), + ] + .iter(), + ) { + let release = build_release(version, commits); + let next_version = + release.calculate_next_version_with_config(&Bump { + features_always_bump_minor: Some(false), + breaking_always_bump_major: Some(false), + })?; + assert_eq!(expected_version, &next_version); + } + + for (version, expected_version, commits) in test_shared.iter().chain( + [ + ("0.0.1", "0.0.2", vec!["fix: fix xyz"]), + ("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.0.1", "0.1.0", vec!["feat!: add xyz", "feat: zzz"]), + ("0.1.0", "0.1.1", vec!["fix: fix xyz"]), + ("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]), + ] + .iter(), + ) { + let release = build_release(version, commits); + let next_version = + release.calculate_next_version_with_config(&Bump { + features_always_bump_minor: Some(true), + breaking_always_bump_major: Some(false), + })?; + assert_eq!(expected_version, &next_version); + } + + for (version, expected_version, commits) in test_shared.iter().chain( + [ + ("0.0.1", "0.0.2", vec!["fix: fix xyz"]), + ("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]), + ("0.1.0", "0.1.1", vec!["fix: fix xyz"]), + ("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]), + ("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]), + ] + .iter(), + ) { + let release = build_release(version, commits); + let next_version = + release.calculate_next_version_with_config(&Bump { + features_always_bump_minor: Some(false), + breaking_always_bump_major: Some(true), + })?; + assert_eq!(expected_version, &next_version); } + let empty_release = Release { previous: Some(Box::new(Release { version: None, @@ -230,8 +329,18 @@ mod test { })), ..Default::default() }; - let next_version = empty_release.calculate_next_version()?; - assert_eq!("0.1.0", next_version); + assert_eq!("0.1.0", empty_release.calculate_next_version()?); + for (features_always_bump_minor, breaking_always_bump_major) in + [(true, true), (true, false), (false, true), (false, false)] + { + assert_eq!( + "0.1.0", + empty_release.calculate_next_version_with_config(&Bump { + features_always_bump_minor: Some(features_always_bump_minor), + breaking_always_bump_major: Some(breaking_always_bump_major), + })? + ); + } Ok(()) } diff --git a/website/docs/configuration/bump.md b/website/docs/configuration/bump.md new file mode 100644 index 0000000000..7bda75e63a --- /dev/null +++ b/website/docs/configuration/bump.md @@ -0,0 +1,28 @@ +# `bump` + +This section contains the bump version related configuration options. + +```toml +[bump] +features_always_bump_minor = true +breaking_always_bump_major = true +``` + +### features_always_bump_minor + +Configures automatic minor version increments for feature changes. +When `true`, a feature will always trigger a minor version update. +When `false`, a feature will trigger: + +- A patch version update if the major version is 0. +- A minor version update otherwise. + +### breaking_always_bump_major + +Configures 0 -> 1 major version increments for breaking changes. +When `true`, a breaking change commit will always trigger a major version update +(including the transition from version 0 to 1) +When `false`, a breaking change commit will trigger: + +- A minor version update if the major version is 0. +- A major version update otherwise. diff --git a/website/docs/usage/bump-version.md b/website/docs/usage/bump-version.md index c7f1389c8d..902d92bc87 100644 --- a/website/docs/usage/bump-version.md +++ b/website/docs/usage/bump-version.md @@ -29,3 +29,12 @@ Tip: you can also get the bumped version [from the context](/docs/usage/print-co ```bash git cliff --unreleased --bump --context | jq -r .[0].version ``` + +## Zero-based versioning scheme + +When working with a zero-based versioning scheme (i.e., `0.x.y` or `0.0.x`), +it is often desirable to preserve the leading zero even when introducing a breaking change. +A switch from `0` to `1` should indicate a higher API stability level. + +You can modify the bumping rules to preserve the zero-based versioning scheme in the +[configuration file](/docs/configuration/bump).