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            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}