1use std::fmt;
2
3use semver::Version;
4
5use crate::commit::{CommitClassifier, ConventionalCommit};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
9#[serde(rename_all = "lowercase")]
10pub enum BumpLevel {
11 Patch,
12 Minor,
13 Major,
14}
15
16impl fmt::Display for BumpLevel {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 BumpLevel::Patch => write!(f, "patch"),
20 BumpLevel::Minor => write!(f, "minor"),
21 BumpLevel::Major => write!(f, "major"),
22 }
23 }
24}
25
26pub fn determine_bump(
30 commits: &[ConventionalCommit],
31 classifier: &dyn CommitClassifier,
32) -> Option<BumpLevel> {
33 commits
34 .iter()
35 .filter_map(|c| classifier.bump_level(&c.r#type, c.breaking))
36 .max()
37}
38
39pub fn apply_bump(version: &Version, bump: BumpLevel) -> Version {
41 match bump {
42 BumpLevel::Major => Version::new(version.major + 1, 0, 0),
43 BumpLevel::Minor => Version::new(version.major, version.minor + 1, 0),
44 BumpLevel::Patch => Version::new(version.major, version.minor, version.patch + 1),
45 }
46}
47
48pub fn apply_prerelease_bump(
57 version: &Version,
58 bump: BumpLevel,
59 prerelease_id: &str,
60 existing_tags: &[Version],
61) -> Version {
62 let base = apply_bump(version, bump);
63
64 let max_n = existing_tags
66 .iter()
67 .filter(|v| v.major == base.major && v.minor == base.minor && v.patch == base.patch)
68 .filter_map(|v| {
69 let pre = v.pre.as_str();
70 let suffix = pre.strip_prefix(prerelease_id)?.strip_prefix('.')?;
71 suffix.parse::<u64>().ok()
72 })
73 .max()
74 .unwrap_or(0);
75
76 let mut result = base;
77 result.pre = semver::Prerelease::new(&format!("{prerelease_id}.{}", max_n + 1)).unwrap();
78 result
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::commit::{ConventionalCommit, DefaultCommitClassifier};
85
86 fn commit(type_: &str, breaking: bool) -> ConventionalCommit {
87 ConventionalCommit {
88 sha: "abc1234".into(),
89 r#type: type_.into(),
90 scope: None,
91 description: "test".into(),
92 body: None,
93 breaking,
94 }
95 }
96
97 fn classifier() -> DefaultCommitClassifier {
98 DefaultCommitClassifier::default()
99 }
100
101 #[test]
102 fn patch_bump() {
103 let v = Version::new(1, 2, 3);
104 assert_eq!(apply_bump(&v, BumpLevel::Patch), Version::new(1, 2, 4));
105 }
106
107 #[test]
108 fn minor_bump_resets_patch() {
109 let v = Version::new(1, 2, 3);
110 assert_eq!(apply_bump(&v, BumpLevel::Minor), Version::new(1, 3, 0));
111 }
112
113 #[test]
114 fn major_bump_resets_minor_and_patch() {
115 let v = Version::new(1, 2, 3);
116 assert_eq!(apply_bump(&v, BumpLevel::Major), Version::new(2, 0, 0));
117 }
118
119 #[test]
120 fn no_commits_returns_none() {
121 assert_eq!(determine_bump(&[], &classifier()), None);
122 }
123
124 #[test]
125 fn non_releasable_types_return_none() {
126 let commits = vec![
127 commit("chore", false),
128 commit("docs", false),
129 commit("ci", false),
130 ];
131 assert_eq!(determine_bump(&commits, &classifier()), None);
132 }
133
134 #[test]
135 fn single_fix_returns_patch() {
136 assert_eq!(
137 determine_bump(&[commit("fix", false)], &classifier()),
138 Some(BumpLevel::Patch)
139 );
140 }
141
142 #[test]
143 fn single_feat_returns_minor() {
144 assert_eq!(
145 determine_bump(&[commit("feat", false)], &classifier()),
146 Some(BumpLevel::Minor)
147 );
148 }
149
150 #[test]
151 fn perf_returns_patch() {
152 assert_eq!(
153 determine_bump(&[commit("perf", false)], &classifier()),
154 Some(BumpLevel::Patch)
155 );
156 }
157
158 #[test]
159 fn breaking_returns_major() {
160 assert_eq!(
161 determine_bump(&[commit("feat", true)], &classifier()),
162 Some(BumpLevel::Major)
163 );
164 }
165
166 #[test]
167 fn highest_bump_wins() {
168 let commits = vec![
169 commit("fix", false),
170 commit("feat", false),
171 commit("feat", true),
172 ];
173 assert_eq!(
174 determine_bump(&commits, &classifier()),
175 Some(BumpLevel::Major)
176 );
177 }
178
179 #[test]
180 fn feat_beats_fix() {
181 let commits = vec![commit("fix", false), commit("feat", false)];
182 assert_eq!(
183 determine_bump(&commits, &classifier()),
184 Some(BumpLevel::Minor)
185 );
186 }
187
188 #[test]
191 fn prerelease_first_alpha() {
192 let v = Version::new(1, 0, 0);
193 let result = apply_prerelease_bump(&v, BumpLevel::Minor, "alpha", &[]);
194 assert_eq!(result.to_string(), "1.1.0-alpha.1");
195 }
196
197 #[test]
198 fn prerelease_increments_counter() {
199 let v = Version::new(1, 0, 0);
200 let existing = vec![
201 Version::parse("1.1.0-alpha.1").unwrap(),
202 Version::parse("1.1.0-alpha.2").unwrap(),
203 ];
204 let result = apply_prerelease_bump(&v, BumpLevel::Minor, "alpha", &existing);
205 assert_eq!(result.to_string(), "1.1.0-alpha.3");
206 }
207
208 #[test]
209 fn prerelease_different_id_starts_at_1() {
210 let v = Version::new(1, 0, 0);
211 let existing = vec![Version::parse("1.1.0-alpha.5").unwrap()];
212 let result = apply_prerelease_bump(&v, BumpLevel::Minor, "beta", &existing);
213 assert_eq!(result.to_string(), "1.1.0-beta.1");
214 }
215
216 #[test]
217 fn prerelease_different_base_starts_at_1() {
218 let v = Version::new(1, 0, 0);
219 let existing = vec![Version::parse("1.1.0-alpha.3").unwrap()];
220 let result = apply_prerelease_bump(&v, BumpLevel::Major, "alpha", &existing);
222 assert_eq!(result.to_string(), "2.0.0-alpha.1");
223 }
224
225 #[test]
226 fn prerelease_rc_identifier() {
227 let v = Version::new(2, 3, 0);
228 let result = apply_prerelease_bump(&v, BumpLevel::Patch, "rc", &[]);
229 assert_eq!(result.to_string(), "2.3.1-rc.1");
230 }
231}