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