next_version-0.2.26/.cargo_vcs_info.json0000644000000001610000000000100136440ustar { "git": { "sha1": "7301a1468f3100413e5c49cf300358beaada1bf3" }, "path_in_vcs": "crates/next_version" }next_version-0.2.26/CHANGELOG.md000064400000000000000000000146721046102023000142610ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.2.26](https://github.com/release-plz/release-plz/compare/next_version-v0.2.25...next_version-v0.2.26) - 2025-11-24 ### Other - address code quality issues ([#2498](https://github.com/release-plz/release-plz/pull/2498)) ## [0.2.25](https://github.com/release-plz/release-plz/compare/next_version-v0.2.24...next_version-v0.2.25) - 2025-09-13 ### Other - update Cargo.toml dependencies ## [0.2.24](https://github.com/release-plz/release-plz/compare/next_version-v0.2.23...next_version-v0.2.24) - 2025-03-17 ### Other - add more rust lints ([#2135](https://github.com/release-plz/release-plz/pull/2135)) ## [0.2.23](https://github.com/release-plz/release-plz/compare/next_version-v0.2.22...next_version-v0.2.23) - 2025-03-01 ### Other - add new line to changelog header (#2098) ## [0.2.22](https://github.com/release-plz/release-plz/compare/next_version-v0.2.21...next_version-v0.2.22) - 2025-02-02 ### Other - *(next-version)* add test for scope with whitespace (#1997) - use `git-conventional` library to parse commits (#2021) ## [0.2.21](https://github.com/release-plz/release-plz/compare/next_version-v0.2.20...next_version-v0.2.21) - 2024-12-07 ### Other - update Cargo.toml dependencies ## [0.2.20](https://github.com/release-plz/release-plz/compare/next_version-v0.2.19...next_version-v0.2.20) - 2024-11-21 ### Other - move `MarcoIeni/release-plz` to `release-plz/release-plz` ([#1850](https://github.com/release-plz/release-plz/pull/1850)) ## [0.2.19](https://github.com/release-plz/release-plz/compare/next_version-v0.2.18...next_version-v0.2.19) - 2024-08-04 ### Other - update Cargo.toml dependencies ## [0.2.18](https://github.com/release-plz/release-plz/compare/next_version-v0.2.17...next_version-v0.2.18) - 2024-06-18 ### Added - support bumping version based on configurable custom pattern ([#1511](https://github.com/release-plz/release-plz/pull/1511)) ## [0.2.17](https://github.com/release-plz/release-plz/compare/next_version-v0.2.16...next_version-v0.2.17) - 2024-05-27 ### Other - update Cargo.toml dependencies ## [0.2.16](https://github.com/release-plz/release-plz/compare/next_version-v0.2.15...next_version-v0.2.16) - 2024-05-05 ### Other - add `if_not_else` clippy lint ([#1438](https://github.com/release-plz/release-plz/pull/1438)) ## [0.2.15](https://github.com/release-plz/release-plz/compare/next_version-v0.2.14...next_version-v0.2.15) - 2024-03-08 ### Other - use edition and license workspace ([#1329](https://github.com/release-plz/release-plz/pull/1329)) ## [0.2.14](https://github.com/release-plz/release-plz/compare/next_version-v0.2.13...next_version-v0.2.14) - 2024-02-23 ### Other - update Cargo.toml dependencies ## [0.2.13](https://github.com/release-plz/release-plz/compare/next_version-v0.2.12...next_version-v0.2.13) - 2024-01-16 ### Other - update Cargo.toml dependencies ## [0.2.12](https://github.com/release-plz/release-plz/compare/next_version-v0.2.11...next_version-v0.2.12) - 2023-12-30 ### Added - *(next_version)* support configuration ([#1168](https://github.com/release-plz/release-plz/pull/1168)) ## [0.2.11](https://github.com/release-plz/release-plz/compare/next_version-v0.2.10...next_version-v0.2.11) - 2023-12-13 ### Other - better document bump strategy ([#1132](https://github.com/release-plz/release-plz/pull/1132)) ## [0.2.10](https://github.com/release-plz/release-plz/compare/next_version-v0.2.9...next_version-v0.2.10) - 2023-11-08 ### Other - *(next-version)* clarify patch increment ([#1065](https://github.com/release-plz/release-plz/pull/1065)) ## [0.2.9](https://github.com/release-plz/release-plz/compare/next_version-v0.2.8...next_version-v0.2.9) - 2023-10-15 ### Other - update dependencies ## [0.2.8](https://github.com/release-plz/release-plz/compare/next_version-v0.2.7...next_version-v0.2.8) - 2023-09-30 ### Other - update dependencies ## [0.2.7](https://github.com/release-plz/release-plz/compare/next_version-v0.2.6...next_version-v0.2.7) - 2023-09-16 ### Other - add additional clippy lints ([#965](https://github.com/release-plz/release-plz/pull/965)) ## [0.2.6](https://github.com/release-plz/release-plz/compare/next_version-v0.2.5...next_version-v0.2.6) - 2023-09-08 ### Other - *(ci)* check links ([#941](https://github.com/release-plz/release-plz/pull/941)) ## [0.2.5](https://github.com/release-plz/release-plz/compare/next_version-v0.2.4...next_version-v0.2.5) - 2023-07-25 ### Other - update dependencies ## [0.2.4](https://github.com/release-plz/release-plz/compare/next_version-v0.2.3...next_version-v0.2.4) - 2023-04-16 ### Other - add test for breaking change in footer (#690) ## [0.2.3](https://github.com/release-plz/release-plz/compare/next_version-v0.2.2...next_version-v0.2.3) - 2023-03-13 ### Other - update dependencies ## [0.2.2](https://github.com/release-plz/release-plz/compare/next_version-v0.2.1...next_version-v0.2.2) - 2023-03-04 ### Fixed - always bump patch in 0.0.x versions (#578) ## [0.2.1](https://github.com/release-plz/release-plz/compare/next_version-v0.2.0...next_version-v0.2.1) - 2023-02-26 ### Added - add function to force breaking changes (#568) ## [0.2.0](https://github.com/release-plz/release-plz/compare/next_version-v0.1.10...next_version-v0.2.0) - 2023-01-11 ### Added - [**breaking**] don't remove build metadata (#433) - [**breaking**] handle pre-releases (#425) ## [0.1.10](https://github.com/release-plz/release-plz/compare/next_version-v0.1.9...next_version-v0.1.10) - 2022-12-16 ### Other - Add functions to increment major, minor and patch version (#375) ## [0.1.9] - 2022-11-04 ### Other - use workspace dependencies (#333) ## [0.1.8] - 2022-10-24 ### Other - *(deps)* bump semver from 1.0.12 to 1.0.14 (#283) ## [0.1.7] - 2022-07-25 ### Other - inline the increment* functions to cut down dependencies (#220) ## [0.1.6] - 2022-07-16 ### Other - update to cargo edit 10 (#211) - *(deps)* bump semver from 1.0.10 to 1.0.12 (#204) ## [0.1.5] - 2022-06-18 ### Other - *(deps)* bump semver from 1.0.9 to 1.0.10 (#180) ## [0.1.4] - 2022-05-19 ### Other - upgrade dependencies (#133) ## [0.1.3] - 2022-05-01 ### Other - remove default features from cargo-edit dependency (#101) ## [0.1.2] - 2022-04-27 ### Other - update dependencies (#91) ## [0.1.1] - 2022-03-27 ### Fixed - fix repository link next_version-0.2.26/Cargo.lock0000644000000042440000000000100116250ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "git-conventional" version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6a949b7fcc81df22526032dcddb006e78c8575e47b0e7ba57d9960570a57bc4" dependencies = [ "unicase", "winnow", ] [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "next_version" version = "0.2.26" dependencies = [ "git-conventional", "regex", "semver", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "unicase" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] next_version-0.2.26/Cargo.toml0000644000000077310000000000100116540ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" name = "next_version" version = "0.2.26" build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Determine next semantic version based on conventional commits" readme = "README.md" keywords = [ "semver", "semantic", "versioning", "conventional", "commits", ] categories = [ "development-tools", "parsing", ] license = "MIT OR Apache-2.0" repository = "https://github.com/release-plz/release-plz/tree/main/crates/next_version" [lib] name = "next_version" path = "src/lib.rs" [[test]] name = "all" path = "tests/all/main.rs" [dependencies.git-conventional] version = "0.12.9" [dependencies.regex] version = "1.11.2" [dependencies.semver] version = "1.0.26" [lints.clippy] arbitrary_source_item_ordering = "allow" as_underscore = "warn" await_holding_lock = "warn" await_holding_refcell_ref = "warn" cargo_common_metadata = "warn" cast_lossless = "warn" cast_possible_wrap = "warn" cast_precision_loss = "warn" cast_sign_loss = "warn" decimal_literal_representation = "allow" default_trait_access = "warn" doc_markdown = "warn" exit = "warn" expl_impl_clone_on_copy = "warn" explicit_into_iter_loop = "warn" explicit_iter_loop = "warn" filetype_is_file = "warn" filter_map_next = "warn" float_cmp_const = "warn" fn_params_excessive_bools = "warn" get_unwrap = "warn" if_not_else = "warn" if_then_some_else_none = "warn" implicit_clone = "warn" large_digit_groups = "warn" large_stack_arrays = "warn" large_types_passed_by_value = "warn" let_underscore_must_use = "warn" let_unit_value = "warn" linkedlist = "warn" lossy_float_literal = "warn" macro_use_imports = "warn" manual_ignore_case_cmp = "warn" manual_let_else = "warn" manual_ok_or = "warn" map_all_any_identity = "warn" map_err_ignore = "warn" map_flatten = "warn" map_unwrap_or = "allow" map_with_unused_argument_over_ranges = "warn" match_bool = "warn" match_same_arms = "warn" match_wildcard_for_single_variants = "warn" mem_forget = "warn" mut_mut = "warn" needless_as_bytes = "warn" needless_borrow = "warn" needless_continue = "warn" needless_pass_by_value = "warn" needless_raw_string_hashes = "allow" option_option = "warn" ptr_as_ptr = "warn" range_minus_one = "warn" range_plus_one = "warn" rc_buffer = "warn" redundant_else = "allow" ref_option_ref = "warn" regex_creation_in_loops = "warn" rest_pat_in_fully_bound_structs = "warn" same_functions_in_if_condition = "warn" semicolon_if_nothing_returned = "warn" trait_duplication_in_bounds = "warn" trivially_copy_pass_by_ref = "warn" type_repetition_in_bounds = "warn" unicode_not_nfc = "warn" uninlined_format_args = "warn" unnecessary_literal_bound = "warn" unnecessary_map_or = "warn" unneeded_field_pattern = "warn" unreadable_literal = "warn" unsafe_derive_deserialize = "warn" unseparated_literal_suffix = "warn" unused_self = "warn" use_self = "warn" used_underscore_binding = "warn" verbose_file_reads = "warn" zero_sized_map_values = "warn" [lints.rust] ambiguous_negative_literals = "warn" closure_returning_async_block = "warn" explicit_outlives_requirements = "warn" if_let_rescope = "warn" impl_trait_overcaptures = "warn" impl_trait_redundant_captures = "warn" let_underscore_drop = "warn" macro_use_extern_crate = "warn" missing_debug_implementations = "warn" non_ascii_idents = "warn" redundant_imports = "warn" redundant_lifetimes = "warn" single_use_lifetimes = "warn" trivial_casts = "warn" trivial_numeric_casts = "warn" unit_bindings = "warn" unsafe_attr_outside_unsafe = "warn" unused = "warn" unused_import_braces = "warn" next_version-0.2.26/Cargo.toml.orig000064400000000000000000000007771046102023000153400ustar 00000000000000[package] name = "next_version" version = "0.2.26" edition.workspace = true description = "Determine next semantic version based on conventional commits" license.workspace = true repository = "https://github.com/release-plz/release-plz/tree/main/crates/next_version" keywords = ["semver", "semantic", "versioning", "conventional", "commits"] categories = ["development-tools", "parsing"] [lints] workspace = true [dependencies] regex.workspace = true semver.workspace = true git-conventional.workspace = true next_version-0.2.26/README.md000064400000000000000000000004761046102023000137240ustar 00000000000000# next_version Library to calculate next semantic version based on [conventional commits](https://www.conventionalcommits.org/). It does not analyze git history, the list of commits must be provided by the user. ## Credits Parts of the codebase are inspired by [cocogitto](https://github.com/cocogitto/cocogitto). next_version-0.2.26/src/lib.rs000064400000000000000000000103461046102023000143450ustar 00000000000000//! Library to calculate next semantic version based on //! [conventional commits](https://www.conventionalcommits.org/). //! //! It does not analyze git history, the list of commits must be provided by the user. //! //! # Version changes //! //! ## Non conventional commits //! //! If conventional commits are not used, the patch is incremented. //! //! ```rust //! use semver::Version; //! use next_version::NextVersion; //! //! let commits = ["my change"]; //! assert_eq!(Version::new(1, 2, 3).next(commits), Version::new(1, 2, 4)); //! ``` //! //! ## `0.0.x` versions //! //! In `0.0.x` versions the patch is always incremented: //! //! ```rust //! use semver::Version; //! use next_version::NextVersion; //! //! let commits = ["my change"]; //! assert_eq!(Version::new(0, 0, 4).next(&commits), Version::new(0, 0, 5)); //! //! let commits = ["feat!: break user"]; //! assert_eq!(Version::new(0, 0, 1).next(&commits), Version::new(0, 0, 2)); //! ``` //! //!
We don't increase the minor version because the bump //! from 0.0.x to 0.1.x //! should be intentional (not automated) because the author communicates an higher level of //! API stability to the user.
//! //! ## Features //! //! If a feature comment is present: //! - If the major number is `0`: the patch is incremented. //! - Otherwise: the minor is incremented. //! //! ```rust //! use semver::Version; //! use next_version::NextVersion; //! //! let commits = ["my change", "feat: make coffe"]; //! assert_eq!(Version::new(1, 2, 4).next(&commits), Version::new(1, 3, 0)); //! //! assert_eq!(Version::new(0, 2, 4).next(&commits), Version::new(0, 2, 5)); //! ``` //! //!
When the major number is 0, //! we don't increase the minor version because the bump from 0.x.y to 0.(x+1).0 //! indicates a breaking change.
//! //! ## Breaking changes //! //! Breaking changes will increment: //! - major if major is not `0`. //! - minor if major is `0`. //! //! ```rust //! use semver::Version; //! use next_version::NextVersion; //! //! let commits = ["feat!: break user"]; //! assert_eq!(Version::new(1, 2, 4).next(&commits), Version::new(2, 0, 0)); //! //! assert_eq!(Version::new(0, 4, 4).next(&commits), Version::new(0, 5, 0)); //! ``` //! //! According to the [conventional commits specification](https://www.conventionalcommits.org/), //! breaking changes can also be specified in the footer: //! //! ```rust //! use semver::Version; //! use next_version::NextVersion; //! //! let breaking_commit = r#"feat: make coffe //! //! my change //! //! BREAKING CHANGE: user will be broken //! "#; //! //! let commits = [breaking_commit]; //! assert_eq!(Version::new(1, 2, 4).next(&commits), Version::new(2, 0, 0)); //! ``` //! //! ## Pre-release //! //! 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 = ["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); //! //! ``` //! //! ## Build metadata //! //! Build metadata isn't modified. //! //! ```rust //! use semver::Version; //! use next_version::NextVersion; //! //! let commits = ["my change"]; //! let version = Version::parse("1.0.0-beta.1+1.1.0").unwrap(); //! let expected = Version::parse("1.0.0-beta.2+1.1.0").unwrap(); //! assert_eq!(version.next(commits.clone()), expected); //! //! let version = Version::parse("1.0.0+abcd").unwrap(); //! let expected = Version::parse("1.0.1+abcd").unwrap(); //! assert_eq!(version.next(commits.clone()), expected); //! ``` //! //! # Custom version increment //! //! If you don't like the default increment rules of the crate, //! you can customize them by using [`VersionUpdater`]. mod next_version; mod version_increment; mod version_updater; pub use crate::{next_version::*, version_increment::*, version_updater::*}; next_version-0.2.26/src/next_version.rs000064400000000000000000000064121046102023000163210ustar 00000000000000use semver::Version; use crate::VersionUpdater; pub trait NextVersion { fn next(&self, commits: I) -> Self where I: IntoIterator, I::Item: AsRef; /// Increments the major version number. fn increment_major(&self) -> Self; /// Increments the minor version number. fn increment_minor(&self) -> Self; /// Increments the patch version number. fn increment_patch(&self) -> Self; /// Increments the prerelease version number. fn increment_prerelease(&self) -> Self; } impl NextVersion for Version { /// Analyze commits and determine the next version based on /// [conventional commits](https://www.conventionalcommits.org/) and /// [semantic versioning](https://semver.org/): /// - If no commits are passed, the version is unchanged. /// - If some commits are present, but none of them match conventional commits specification, /// the version is incremented as a 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. /// /// ```rust /// use next_version::NextVersion; /// use semver::Version; /// /// let commits = ["feat: make coffe"]; /// let version = Version::new(0, 3, 3); /// assert_eq!(version.next(commits), Version::new(0, 3, 4)); /// ``` fn next(&self, commits: I) -> Self where I: IntoIterator, I::Item: AsRef, { VersionUpdater::default().increment(self, commits) } // taken from https://github.com/killercup/cargo-edit/blob/643e9253a84db02c52a7fa94f07d786d281362ab/src/version.rs#L38 fn increment_major(&self) -> Self { Self { major: self.major + 1, minor: 0, patch: 0, pre: semver::Prerelease::EMPTY, build: self.build.clone(), } } // taken from https://github.com/killercup/cargo-edit/blob/643e9253a84db02c52a7fa94f07d786d281362ab/src/version.rs#L46 fn increment_minor(&self) -> Self { Self { minor: self.minor + 1, patch: 0, pre: semver::Prerelease::EMPTY, ..self.clone() } } // taken from https://github.com/killercup/cargo-edit/blob/643e9253a84db02c52a7fa94f07d786d281362ab/src/version.rs#L53 fn increment_patch(&self) -> Self { Self { patch: self.patch + 1, pre: semver::Prerelease::EMPTY, ..self.clone() } } 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/release-plz/release-plz/issues"); Self { pre: next_pre, ..self.clone() } } } fn increment_last_identifier(release: &str) -> String { match release.rsplit_once('.') { Some((left, right)) => { if let Ok(right_num) = right.parse::() { format!("{left}.{}", right_num + 1) } else { format!("{release}.1") } } None => format!("{release}.1"), } } next_version-0.2.26/src/version_increment.rs000064400000000000000000000140221046102023000173230ustar 00000000000000use git_conventional::Commit; use regex::Regex; use semver::Version; use crate::{NextVersion, VersionUpdater}; #[derive(Debug, PartialEq, Eq)] pub enum VersionIncrement { Major, Minor, Patch, Prerelease, } fn is_there_a_custom_match(regex: Option<&Regex>, commits: &[Commit]) -> bool { regex.is_some_and(|r| commits.iter().any(|commit| r.is_match(&commit.type_()))) } impl VersionIncrement { /// Analyze commits and determine which part of version to increment based on /// [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 /// [these](https://www.conventionalcommits.org/en/v1.0.0/#how-does-this-relate-to-semverare) rules. pub fn from_commits(current_version: &Version, commits: I) -> Option where I: IntoIterator, I::Item: AsRef, { let updater = VersionUpdater::default(); Self::from_commits_with_updater(&updater, current_version, commits) } pub(crate) fn from_commits_with_updater( updater: &VersionUpdater, current_version: &Version, commits: I, ) -> Option where I: IntoIterator, I::Item: AsRef, { let mut commits = commits.into_iter().peekable(); let are_commits_present = commits.peek().is_some(); if are_commits_present { if !current_version.pre.is_empty() { return Some(Self::Prerelease); } // Parse commits and keep only the ones that follow conventional commits specification. let commit_messages: Vec = commits.map(|c| c.as_ref().to_string()).collect(); let commits: Vec = commit_messages .iter() .filter_map(|c| Commit::parse(c).ok()) .collect(); Some(Self::from_conventional_commits( current_version, &commits, updater, )) } else { None } } /// Increments the version to take into account breaking changes. /// ```rust /// use next_version::VersionIncrement; /// use semver::Version; /// /// let increment = VersionIncrement::breaking(&Version::new(0, 3, 3)); /// assert_eq!(increment, VersionIncrement::Minor); /// /// let increment = VersionIncrement::breaking(&Version::new(1, 3, 3)); /// assert_eq!(increment, VersionIncrement::Major); /// /// let increment = VersionIncrement::breaking(&Version::parse("1.3.3-alpha.1").unwrap()); /// assert_eq!(increment, VersionIncrement::Prerelease); /// ``` pub fn breaking(current_version: &Version) -> Self { if !current_version.pre.is_empty() { Self::Prerelease } else if current_version.major == 0 && current_version.minor == 0 { Self::Patch } else if current_version.major == 0 { Self::Minor } else { Self::Major } } /// If no conventional commits are present, the version is incremented as a Patch fn from_conventional_commits( current: &Version, commits: &[Commit], updater: &VersionUpdater, ) -> Self { let is_there_a_feature = || { commits .iter() .any(|commit| commit.type_() == git_conventional::Type::FEAT) }; let is_there_a_breaking_change = commits.iter().any(|commit| commit.breaking()); let is_major_bump = || { (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) }; let is_minor_bump = || { let is_feat_bump = || { is_there_a_feature() && (current.major != 0 || updater.features_always_increment_minor) }; let is_breaking_bump = || current.major == 0 && current.minor != 0 && is_there_a_breaking_change; is_feat_bump() || is_breaking_bump() || is_there_a_custom_match(updater.custom_minor_increment_regex.as_ref(), commits) }; if is_major_bump() { Self::Major } else if is_minor_bump() { Self::Minor } else { Self::Patch } } } impl VersionIncrement { pub fn bump(&self, version: &Version) -> Version { match self { Self::Major => version.increment_major(), Self::Minor => version.increment_minor(), Self::Patch => version.increment_patch(), Self::Prerelease => version.increment_prerelease(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn returns_true_for_matching_custom_type() { let regex = Regex::new(r"custom").unwrap(); let commits = vec![Commit::parse("custom: A custom commit").unwrap()]; assert!(is_there_a_custom_match(Some(®ex), &commits)); } #[test] fn returns_false_for_non_custom_commit_types() { let regex = Regex::new(r"custom").unwrap(); let commits = vec![Commit::parse("feat: A feature commit").unwrap()]; assert!(!is_there_a_custom_match(Some(®ex), &commits)); } #[test] fn returns_false_for_empty_commits_list() { let regex = Regex::new(r"custom").unwrap(); let commits: Vec = Vec::new(); assert!(!is_there_a_custom_match(Some(®ex), &commits)); } } next_version-0.2.26/src/version_updater.rs000064400000000000000000000175021046102023000170110ustar 00000000000000use regex::Regex; use semver::Version; use crate::VersionIncrement; /// This struct allows to increment a version by /// specifying a configuration. /// /// Useful if you don't like the default increment rules of the crate. /// /// # Example /// /// ``` /// use next_version::VersionUpdater; /// use semver::Version; /// /// let updated_version = VersionUpdater::new() /// .with_features_always_increment_minor(false) /// .with_breaking_always_increment_major(true) /// .increment(&Version::new(1, 2, 3), ["feat: commit 1", "fix: commit 2"]); /// /// assert_eq!(Version::new(1, 3, 0), updated_version); /// ``` #[derive(Debug)] pub struct VersionUpdater { pub(crate) features_always_increment_minor: bool, pub(crate) breaking_always_increment_major: bool, pub(crate) custom_major_increment_regex: Option, pub(crate) custom_minor_increment_regex: Option, } impl Default for VersionUpdater { fn default() -> Self { Self::new() } } impl VersionUpdater { /// Constructs a new instance with the default rules of the crate. /// /// If you don't customize the struct further, it is equivalent to /// calling [`crate::NextVersion::next`]. /// /// ``` /// use next_version::{NextVersion, VersionUpdater}; /// use semver::Version; /// /// let version = Version::new(1, 2, 3); /// let commits = ["feat: commit 1", "fix: commit 2"]; /// let updated_version1 = VersionUpdater::new() /// .increment(&version, &commits); /// let updated_version2 = version.next(&commits); /// /// assert_eq!(updated_version1, updated_version2); /// ``` pub fn new() -> Self { Self { features_always_increment_minor: false, breaking_always_increment_major: false, custom_major_increment_regex: None, custom_minor_increment_regex: None, } } /// Configures automatic minor version increments for feature changes. /// /// - When `true` is passed, a feature will always trigger a minor version update. /// - When `false` is passed, a feature will trigger: /// - a patch version update if the major version is 0. /// - a minor version update otherwise. /// /// Default: `false`. /// /// ```rust /// use semver::Version; /// use next_version::VersionUpdater; /// /// let commits = ["feat: make coffee"]; /// let version = Version::new(0, 2, 3); /// assert_eq!( /// VersionUpdater::new() /// .with_features_always_increment_minor(true) /// .increment(&version, &commits), /// Version::new(0, 3, 0) /// ); /// assert_eq!( /// VersionUpdater::new() /// .increment(&version, &commits), /// Version::new(0, 2, 4) /// ); /// ``` pub fn with_features_always_increment_minor( mut self, features_always_increment_minor: bool, ) -> Self { self.features_always_increment_minor = features_always_increment_minor; self } /// Configures `0 -> 1` major version increments for breaking changes. /// /// - When `true` is passed, a breaking change commit will always trigger a major version update /// (including the transition from version 0 to 1) /// - When `false` is passed, a breaking change commit will trigger: /// - a minor version update if the major version is 0. /// - a major version update otherwise. /// /// Default: `false`. /// /// ```rust /// use semver::Version; /// use next_version::VersionUpdater; /// /// let commits = ["feat!: incompatible change"]; /// let version = Version::new(0, 2, 3); /// assert_eq!( /// VersionUpdater::new() /// .with_breaking_always_increment_major(true) /// .increment(&version, &commits), /// Version::new(1, 0, 0) /// ); /// assert_eq!( /// VersionUpdater::new() /// .increment(&version, &commits), /// Version::new(0, 3, 0) /// ); /// ``` pub fn with_breaking_always_increment_major( mut self, breaking_always_increment_major: bool, ) -> Self { self.breaking_always_increment_major = breaking_always_increment_major; 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) /// ); /// ``` /// /// ### Overriding default behavior /// /// You can also override the default behavior of conventional commits types. /// For example, you can make a `feat` commit trigger a _major_ version increment /// instead of _minor_: /// /// ```rust /// use semver::Version; /// use next_version::VersionUpdater; /// /// let commits = ["feat: incompatible change"]; /// let version = Version::new(1, 2, 3); /// assert_eq!( /// VersionUpdater::new() /// .with_custom_major_increment_regex("feat") /// .expect("invalid regex") /// .increment(&version, &commits), /// Version::new(2, 0, 0) /// ); /// assert_eq!( /// VersionUpdater::new() /// .increment(&version, &commits), /// Version::new(1, 3, 0) /// ); /// ``` pub fn with_custom_major_increment_regex( mut self, custom_major_increment_regex: &str, ) -> Result { 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 { 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(self, version: &Version, commits: I) -> Version where I: IntoIterator, I::Item: AsRef, { let increment = VersionIncrement::from_commits_with_updater(&self, version, commits); match increment { Some(increment) => increment.bump(version), None => version.clone(), } } } next_version-0.2.26/tests/all/main.rs000064400000000000000000000000351046102023000156400ustar 00000000000000mod normal; mod pre_release; next_version-0.2.26/tests/all/normal.rs000064400000000000000000000070261046102023000162130ustar 00000000000000use next_version::{NextVersion, VersionUpdater}; use semver::Version; #[test] fn commit_without_semver_prefix_increments_patch_version() { let commits = ["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 = ["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 = ["feat: make coffe"]; let version = Version::new(1, 3, 3); assert_eq!(version.next(commits), Version::new(1, 4, 0)); } #[test] fn commit_with_feat_semver_prefix_increments_patch_version_when_major_is_zero() { let commits = ["feat: make coffee"]; let version = Version::new(0, 2, 3); assert_eq!(version.next(commits), Version::new(0, 2, 4)); } #[test] fn commit_with_feat_semver_prefix_increments_minor_version_when_major_is_zero() { let commits = ["feat: make coffee"]; let version = Version::new(0, 2, 3); assert_eq!( VersionUpdater::new() .with_features_always_increment_minor(true) .with_breaking_always_increment_major(false) .increment(&version, commits), Version::new(0, 3, 0) ); } #[test] fn commit_with_breaking_change_increments_major_version() { let commits = ["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 = ["feat!: break user"]; let version = Version::new(0, 2, 3); assert_eq!(version.next(commits), Version::new(0, 3, 0)); } #[test] fn commit_with_breaking_change_increments_major_version_when_major_is_zero() { let commits = ["feat!: break user"]; let version = Version::new(0, 2, 3); assert_eq!( VersionUpdater::new() .with_features_always_increment_minor(false) .with_breaking_always_increment_major(true) .increment(&version, commits), 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) ); } #[test] fn commit_with_scope() { let commits = ["feat(my_scope)!: this is a test commit"]; let version = Version::new(1, 0, 0); assert_eq!(version.next(commits), Version::new(2, 0, 0)); } #[test] fn commit_with_scope_whitespace() { let commits = ["feat(my scope)!: this is a test commit"]; let version = Version::new(1, 0, 0); assert_eq!(version.next(commits), Version::new(2, 0, 0)); } #[test] fn commit_with_scope_minor() { let commits = ["feat(my scope): this is a test commit"]; let version = Version::new(1, 0, 0); assert_eq!(version.next(commits), Version::new(1, 1, 0)); } next_version-0.2.26/tests/all/pre_release.rs000064400000000000000000000026611046102023000172110ustar 00000000000000use next_version::NextVersion; use semver::Version; #[test] fn commit_without_semver_prefix_increments_pre_release_version() { let commits = ["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 = ["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 = ["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 = ["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 = ["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); }