Skip to main content

sr_core/
version.rs

1use std::fmt;
2
3use semver::Version;
4
5use crate::commit::{CommitClassifier, ConventionalCommit};
6
7/// The kind of version bump to apply.
8#[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
25/// Determine the highest bump level from a set of conventional commits.
26///
27/// Returns `None` if no commits warrant a release.
28pub 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
38/// Apply a bump level to a version, returning the new version.
39pub 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}