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