diff --git a/Cargo.lock b/Cargo.lock index f88f7eb..a5328e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,15 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -191,6 +200,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "conventional_commit_parser" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58660f9e1d5eeeeec9c33d1473ea8bba000c673a2189edaeedb4523ec7d6f7cb" +dependencies = [ + "pest", + "pest_derive", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -207,12 +226,32 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cuddle-please" version = "0.1.0" dependencies = [ "anyhow", "clap", + "conventional_commit_parser", "dotenv", "reqwest", "semver", @@ -225,6 +264,16 @@ dependencies = [ "url", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -351,6 +400,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gimli" version = "0.27.3" @@ -740,6 +799,50 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pest" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f94bca7e7a599d89dea5dfa309e217e7906c3c007fb9c3299c40b10d6a315d3" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d490fe7e8556575ff6911e45567ab95e71617f43781e5c05490dc8d75c965c" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "pest_meta" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674c66ebb4b4d9036012091b537aae5878970d6999f81a265034d85b136b341" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.10" @@ -991,6 +1094,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1075,6 +1189,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + [[package]] name = "thread_local" version = "1.1.7" @@ -1253,6 +1387,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1309,6 +1455,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 17d7b67..ea3d5cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ url = {version = "*"} serde_yaml = {version = "*"} serde = {version = "*", features = ["derive"]} semver = "1.0.18" +conventional_commit_parser = "0.9.4" reqwest = {version = "*"} diff --git a/crates/cuddle-please/Cargo.toml b/crates/cuddle-please/Cargo.toml index de68749..8501045 100644 --- a/crates/cuddle-please/Cargo.toml +++ b/crates/cuddle-please/Cargo.toml @@ -15,6 +15,8 @@ serde.workspace = true reqwest = {workspace = true, features = ["blocking", "json"]} url.workspace = true semver.workspace = true +conventional_commit_parser.workspace = true + [dev-dependencies] tracing-test = {workspace = true, features = ["no-env-filter"]} diff --git a/crates/cuddle-please/src/versioning/conventional_parse.rs b/crates/cuddle-please/src/versioning/conventional_parse.rs new file mode 100644 index 0000000..a069e69 --- /dev/null +++ b/crates/cuddle-please/src/versioning/conventional_parse.rs @@ -0,0 +1,191 @@ +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(); + if commits.peek().is_none() { + return None; + } + if let Some(prerelease) = Self::is_prerelease(cur_version) { + return Some(prerelease); + } + + let commits: Vec = Self::parse_commits::(commits); + + return 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; + + use crate::{environment::get_from_environment, gitea_client::Commit}; + + #[test] + #[traced_test] + fn is_prerelease() { + let mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut version = Version::parse("0.1.0").unwrap(); + + let commits: Vec<&str> = Vec::new(); + + let actual = VersionIncrement::from(&version, commits).is_none(); + assert_eq!(true, actual); + } +} diff --git a/crates/cuddle-please/src/versioning/mod.rs b/crates/cuddle-please/src/versioning/mod.rs index b658cff..746e135 100644 --- a/crates/cuddle-please/src/versioning/mod.rs +++ b/crates/cuddle-please/src/versioning/mod.rs @@ -1 +1,3 @@ +pub mod conventional_parse; +pub mod next_version; pub mod semver; diff --git a/crates/cuddle-please/src/versioning/next_version.rs b/crates/cuddle-please/src/versioning/next_version.rs new file mode 100644 index 0000000..c4d8aa7 --- /dev/null +++ b/crates/cuddle-please/src/versioning/next_version.rs @@ -0,0 +1,170 @@ +use semver::{Prerelease, Version}; + +use super::conventional_parse::VersionIncrement; + +pub trait NextVersion { + fn next(&self, commits: I) -> Self + where + I: IntoIterator, + I::Item: AsRef; +} + +impl NextVersion for Version { + fn next(&self, commits: I) -> Self + where + I: IntoIterator, + I::Item: AsRef, + { + let increment = VersionIncrement::from(self, commits); + + match increment { + Some(increment) => match increment { + VersionIncrement::Major => Self { + major: self.major + 1, + minor: 0, + patch: 0, + pre: Prerelease::EMPTY, + ..self.clone() + }, + VersionIncrement::Minor => Self { + minor: self.minor + 1, + patch: 0, + pre: Prerelease::EMPTY, + ..self.clone() + }, + VersionIncrement::Patch => Self { + patch: self.patch + 1, + pre: Prerelease::EMPTY, + ..self.clone() + }, + VersionIncrement::Prerelease => Self { + pre: { + let release = &self.pre; + let release_version = match release.rsplit_once(".") { + Some((tag, version)) => match version.parse::() { + Ok(version) => format!("{tag}.{}", version + 1), + Err(_) => format!("{tag}.1"), + }, + None => format!("{release}.1"), + }; + Prerelease::new(&release_version).expect("prerelease is not valid semver") + }, + ..self.clone() + }, + }, + None => self.clone(), + } + } +} + +#[cfg(test)] +mod test { + use semver::Version; + use tracing_test::traced_test; + + use crate::versioning::next_version::NextVersion; + + #[test] + #[traced_test] + fn is_no_bump() { + let version = Version::parse("0.0.0-prerelease").unwrap(); + let commits: Vec<&str> = vec![]; + + let actual = version.next(commits); + + assert_eq!("0.0.0-prerelease", actual.to_string()) + } + + #[test] + #[traced_test] + fn is_prerelease_initial() { + let version = Version::parse("0.0.0-prerelease").unwrap(); + let commits: Vec<&str> = vec!["feat: something"]; + + let actual = version.next(commits); + + assert_eq!("0.0.0-prerelease.1", actual.to_string()) + } + + #[test] + #[traced_test] + fn is_prerelease_invalid() { + let version = Version::parse("0.0.0-prerelease.invalid").unwrap(); + let commits: Vec<&str> = vec!["feat: something"]; + + let actual = version.next(commits); + + assert_eq!("0.0.0-prerelease.1", actual.to_string()) + } + + #[test] + #[traced_test] + fn is_prerelease_next() { + let version = Version::parse("0.0.0-prerelease.1").unwrap(); + let commits: Vec<&str> = vec!["feat: something"]; + + let actual = version.next(commits); + + assert_eq!("0.0.0-prerelease.2", actual.to_string()) + } + + #[test] + #[traced_test] + fn is_patch() { + let version = Version::parse("0.0.0").unwrap(); + let commits: Vec<&str> = vec!["fix: something"]; + + let actual = version.next(commits); + + assert_eq!("0.0.1", actual.to_string()) + } + + #[test] + #[traced_test] + fn is_minor() { + let version = Version::parse("0.1.0").unwrap(); + let commits: Vec<&str> = vec!["feat: something"]; + + let actual = version.next(commits); + + assert_eq!("0.2.0", actual.to_string()) + } + #[test] + #[traced_test] + fn is_minor_clears_patch() { + let version = Version::parse("0.1.1").unwrap(); + let commits: Vec<&str> = vec!["feat: something"]; + + let actual = version.next(commits); + + assert_eq!("0.2.0", actual.to_string()) + } + #[test] + #[traced_test] + fn is_major() { + let version = Version::parse("0.0.0").unwrap(); + let commits: Vec<&str> = vec![ + "feat: something + + BREAKING CHANGE: something", + ]; + + let actual = version.next(commits); + + assert_eq!("1.0.0", actual.to_string()) + } + #[test] + #[traced_test] + fn is_major_clears_minor_patch() { + let version = Version::parse("1.2.3").unwrap(); + let commits: Vec<&str> = vec![ + "feat: something + + BREAKING CHANGE: something", + ]; + + let actual = version.next(commits); + + assert_eq!("2.0.0", actual.to_string()) + } +}