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
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::commit::{ConventionalCommit, DefaultCommitClassifier};
51
52 fn commit(type_: &str, breaking: bool) -> ConventionalCommit {
53 ConventionalCommit {
54 sha: "abc1234".into(),
55 r#type: type_.into(),
56 scope: None,
57 description: "test".into(),
58 body: None,
59 breaking,
60 }
61 }
62
63 fn classifier() -> DefaultCommitClassifier {
64 DefaultCommitClassifier::default()
65 }
66
67 #[test]
68 fn patch_bump() {
69 let v = Version::new(1, 2, 3);
70 assert_eq!(apply_bump(&v, BumpLevel::Patch), Version::new(1, 2, 4));
71 }
72
73 #[test]
74 fn minor_bump_resets_patch() {
75 let v = Version::new(1, 2, 3);
76 assert_eq!(apply_bump(&v, BumpLevel::Minor), Version::new(1, 3, 0));
77 }
78
79 #[test]
80 fn major_bump_resets_minor_and_patch() {
81 let v = Version::new(1, 2, 3);
82 assert_eq!(apply_bump(&v, BumpLevel::Major), Version::new(2, 0, 0));
83 }
84
85 #[test]
86 fn no_commits_returns_none() {
87 assert_eq!(determine_bump(&[], &classifier()), None);
88 }
89
90 #[test]
91 fn non_releasable_types_return_none() {
92 let commits = vec![
93 commit("chore", false),
94 commit("docs", false),
95 commit("ci", false),
96 ];
97 assert_eq!(determine_bump(&commits, &classifier()), None);
98 }
99
100 #[test]
101 fn single_fix_returns_patch() {
102 assert_eq!(
103 determine_bump(&[commit("fix", false)], &classifier()),
104 Some(BumpLevel::Patch)
105 );
106 }
107
108 #[test]
109 fn single_feat_returns_minor() {
110 assert_eq!(
111 determine_bump(&[commit("feat", false)], &classifier()),
112 Some(BumpLevel::Minor)
113 );
114 }
115
116 #[test]
117 fn perf_returns_patch() {
118 assert_eq!(
119 determine_bump(&[commit("perf", false)], &classifier()),
120 Some(BumpLevel::Patch)
121 );
122 }
123
124 #[test]
125 fn breaking_returns_major() {
126 assert_eq!(
127 determine_bump(&[commit("feat", true)], &classifier()),
128 Some(BumpLevel::Major)
129 );
130 }
131
132 #[test]
133 fn highest_bump_wins() {
134 let commits = vec![
135 commit("fix", false),
136 commit("feat", false),
137 commit("feat", true),
138 ];
139 assert_eq!(
140 determine_bump(&commits, &classifier()),
141 Some(BumpLevel::Major)
142 );
143 }
144
145 #[test]
146 fn feat_beats_fix() {
147 let commits = vec![commit("fix", false), commit("feat", false)];
148 assert_eq!(
149 determine_bump(&commits, &classifier()),
150 Some(BumpLevel::Minor)
151 );
152 }
153}