use regex::Regex; use semver::{BuildMetadata, Prerelease, Version}; #[derive(PartialEq, Debug)] pub enum BumpType { Major, Minor, Patch, } pub fn get_most_significant_bump(commits: &[String]) -> Option { let bump_regex = Regex::new( r"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|breaking)(\([a-zA-Z-_ ]+\))?: .*", ) .unwrap(); let mut major_bump = None; let mut minor_bump = None; let mut patch_bump = None; for commit in commits { if let Some(captures) = bump_regex.captures(commit) { let bump_type = captures.get(1).unwrap().as_str(); match bump_type { "breaking" => { if major_bump.is_none() { major_bump = Some(1); } } "feat" => { if minor_bump.is_none() && major_bump.is_none() { minor_bump = Some(1); } else if let Some(mut count) = minor_bump { count += 1; minor_bump = Some(count); } } "chore" | "docs" | "ci" | "test" => {} _ => { if patch_bump.is_none() && minor_bump.is_none() && major_bump.is_none() { patch_bump = Some(1); } else if let Some(mut count) = patch_bump { count += 1; patch_bump = Some(count); } } } } } let most_significant_bump = match (major_bump, minor_bump, patch_bump) { (Some(count), _, _) if count > 0 => Some(BumpType::Major), (_, Some(count), _) if count > 0 => Some(BumpType::Minor), (_, _, Some(count)) if count > 0 => Some(BumpType::Patch), _ => None, }; most_significant_bump } fn increment_patch(v: &mut Version) { v.patch += 1; v.pre = Prerelease::EMPTY; v.build = BuildMetadata::EMPTY; } fn increment_minor(v: &mut Version) { v.minor += 1; v.patch = 0; v.pre = Prerelease::EMPTY; v.build = BuildMetadata::EMPTY; } fn increment_major(v: &mut Version) { v.major += 1; v.minor = 0; v.patch = 0; v.pre = Prerelease::EMPTY; v.build = BuildMetadata::EMPTY; } pub fn bump_semver(semver: &str, bump_type: BumpType) -> Result { let mut contains_start_v = false; let semver = if semver.starts_with("v") { contains_start_v = true; semver.replace("v", "") } else { semver.to_string() }; let mut version = Version::parse(&semver)?; match bump_type { BumpType::Major => increment_major(&mut version), BumpType::Minor => increment_minor(&mut version), BumpType::Patch => increment_patch(&mut version), } if contains_start_v { Ok(format!("v{version}")) } else { Ok(version.to_string()) } } #[cfg(test)] mod tests { use super::{get_most_significant_bump, BumpType}; #[test] fn test_no_commits() { let commits = vec![]; assert_eq!(get_most_significant_bump(&commits), None); } #[test] fn test_major_bump() { let commits = vec![ "breaking: new feature".to_string(), "fix: bug fix".to_string(), "chore: some chore".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Major)); } #[test] fn test_minor_bump() { let commits = vec!["feat: bug fix".to_string(), "chore: some chore".to_string()]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Minor)); } #[test] fn test_patch_bump() { let commits = vec![ "chore: some chore".to_string(), "style: code style change".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Patch)); } #[test] fn test_no_significant_bumps() { let commits = vec![ "docs: documentation update".to_string(), "chore: another chore".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), None); } #[test] fn test_combined_bumps() { let commits = vec![ "breaking: new breaking change".to_string(), "feat: new feature".to_string(), "fix: bug fix".to_string(), "style: code style change".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Major)); } #[test] fn test_major_bump_with_scope() { let commits = vec![ "breaking(some-system): new feature".to_string(), "fix(another-system): bug fix".to_string(), "chore(third-system): some chore".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Major)); } #[test] fn test_minor_bump_with_scope() { let commits = vec![ "feat(some-system): bug fix".to_string(), "chore(another-system): some chore".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Minor)); } #[test] fn test_patch_bump_with_scope() { let commits = vec![ "chore(some-system): some chore".to_string(), "style(another-system): code style change".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Patch)); } #[test] fn test_combined_bumps_with_scope() { let commits = vec![ "breaking(some-system): new feature".to_string(), "feat(another-system): bug fix".to_string(), "style(third-system): code style change".to_string(), ]; assert_eq!(get_most_significant_bump(&commits), Some(BumpType::Major)); } use super::bump_semver; #[test] fn test_bump_semver_major() { let semver = "1.2.3"; let bumped_version = bump_semver(semver, BumpType::Major).unwrap(); assert_eq!(bumped_version, "2.0.0"); } #[test] fn test_bump_semver_minor() { let semver = "1.2.3"; let bumped_version = bump_semver(semver, BumpType::Minor).unwrap(); assert_eq!(bumped_version, "1.3.0"); } #[test] fn test_bump_semver_patch() { let semver = "1.2.3"; let bumped_version = bump_semver(semver, BumpType::Patch).unwrap(); assert_eq!(bumped_version, "1.2.4"); } #[test] fn test_bump_semver_invalid() { let semver = "1.2.invalid"; let bumped_version = bump_semver(semver, BumpType::Patch); assert!(bumped_version.is_err()); } }