use conventional_commit_parser::commit::{CommitType, ConventionalCommit}; use semver::Version; #[derive(Debug, Clone, PartialEq)] pub enum VersionIncrement { Major, Minor, Patch, Prerelease, } impl VersionIncrement { pub fn from(cur_version: &Version, commits: C) -> Option where C: IntoIterator, C::Item: AsRef, { let mut commits = commits.into_iter().peekable(); commits.peek()?; if let Some(prerelease) = Self::is_prerelease(cur_version) { return Some(prerelease); } let commits: Vec = Self::parse_commits::(commits); Some(Self::from_conventional_commits(commits)) } #[inline] fn parse_commits( commits: std::iter::Peekable<::IntoIter>, ) -> Vec where C: IntoIterator, C::Item: AsRef, { commits .filter_map(|c| conventional_commit_parser::parse(c.as_ref()).ok()) .collect() } // Find most significant change fn from_conventional_commits(commits: Vec) -> VersionIncrement { let found_breaking = || commits.iter().any(|c| c.is_breaking_change); let found_feature = || { commits .iter() .any(|c| matches!(c.commit_type, CommitType::Feature)) }; match (found_breaking(), found_feature()) { (true, _) => Self::Major, (_, true) => Self::Minor, (_, false) => Self::Patch, } } fn is_prerelease(cur_version: &Version) -> Option { if !cur_version.pre.is_empty() { return Some(Self::Prerelease); } None } } #[cfg(test)] mod tests { use crate::versioning::conventional_parse::VersionIncrement; use semver::Version; use tracing_test::traced_test; #[test] #[traced_test] fn is_prerelease() { let version = Version::parse("0.0.0-alpha.1").unwrap(); let commits = vec![ "feat: something", "fix: something", "feat(something): something", "feat(breaking): some BREAKING CHANGE: something", ]; let actual = VersionIncrement::from(&version, commits).unwrap(); assert_eq!(VersionIncrement::Prerelease, actual); } #[test] #[traced_test] fn is_patch() { let version = Version::parse("0.0.1").unwrap(); let commits = vec![ "fix: something", "fix: something", "fix: something", "fix: something", "fix: something", "fix: something", ]; let actual = VersionIncrement::from(&version, commits).unwrap(); assert_eq!(VersionIncrement::Patch, actual); } #[test] #[traced_test] fn is_minor() { let version = Version::parse("0.1.0").unwrap(); let commits = vec![ "feat: something", "feat: something", "fix: something", "fix: something", "fix: something", "fix: something", ]; let actual = VersionIncrement::from(&version, commits).unwrap(); assert_eq!(VersionIncrement::Minor, actual); } #[test] #[traced_test] fn is_major() { let version = Version::parse("0.1.0").unwrap(); let commits = vec![ "feat: something", "feat: something BREAKING CHANGE: something", "fix: something", "fix: something", "fix: something", "fix: something", ]; let actual = VersionIncrement::from(&version, commits).unwrap(); assert_eq!(VersionIncrement::Major, actual); } #[test] #[traced_test] fn chore_is_patch() { let version = Version::parse("0.1.0").unwrap(); let commits = vec!["chore: something"]; let actual = VersionIncrement::from(&version, commits).unwrap(); assert_eq!(VersionIncrement::Patch, actual); } #[test] #[traced_test] fn refactor_is_patch() { let version = Version::parse("0.1.0").unwrap(); let commits = vec!["refactor: something"]; let actual = VersionIncrement::from(&version, commits).unwrap(); assert_eq!(VersionIncrement::Patch, actual); } #[test] #[traced_test] fn unknown_commits_are_patch() { let version = Version::parse("0.1.0").unwrap(); let commits = vec!["blablabla some commit"]; let actual = VersionIncrement::from(&version, commits).unwrap(); assert_eq!(VersionIncrement::Patch, actual); } #[test] #[traced_test] fn nothing_returns_none() { let version = Version::parse("0.1.0").unwrap(); let commits: Vec<&str> = Vec::new(); let actual = VersionIncrement::from(&version, commits).is_none(); assert!(actual); } }