knope_versioning/semver/
package_versions.rs

1use std::{collections::BTreeMap, fmt::Debug, str::FromStr};
2
3use tracing::debug;
4
5use super::{
6    prerelease_map::PrereleaseMap, Label, PreVersion, Prerelease, Rule, StableVersion, Version,
7};
8use crate::semver::rule::Stable;
9
10/// It's not enough to just track one version for each package, we need:
11/// - The latest stable version (if any)
12/// - The last version of each type of pre-release following the latest stable version
13///
14/// So we might have 1.2.3, 1.2.4-rc.1, 1.3.0-beta.0, and 2.0.0-alpha.4
15#[derive(Clone, Debug, Default, Eq, PartialEq)]
16pub struct PackageVersions {
17    stable: StableVersion,
18    prereleases: Prereleases,
19}
20
21type Prereleases = BTreeMap<StableVersion, PrereleaseMap>;
22
23impl PackageVersions {
24    /// Get the (relevant) current versions from a slice of Git tags.
25    ///
26    /// Tags are expected to either be `v{version}` or `{prefix}/v{version}` (if supplied),
27    ///
28    /// ## Parameters
29    /// - `prefix`: Only tag names starting with this string will be considered.
30    /// - `all_tags`: All tags in the repository.
31    pub fn from_tags<S: AsRef<str> + Debug>(prefix: Option<&str>, all_tags: &[S]) -> Self {
32        let pattern = prefix
33            .as_ref()
34            .map_or_else(|| String::from("v"), |prefix| format!("{prefix}/v"));
35        let mut tags = all_tags
36            .iter()
37            .filter(|tag| tag.as_ref().starts_with(&pattern))
38            .peekable();
39
40        if tags.peek().is_none() {
41            debug!("No tags found matching pattern {pattern}");
42        }
43
44        let mut current_versions = Self::default();
45        for tag in tags {
46            let version_string = tag.as_ref().replace(&pattern, "");
47            if let Ok(version) = Version::from_str(version_string.as_str()) {
48                match version {
49                    Version::Stable(stable) => {
50                        current_versions.stable = stable;
51                        break; // Only prereleases newer than the last stable version are relevant
52                    }
53                    Version::Pre(_) => {
54                        current_versions.update_version(version);
55                    }
56                }
57            }
58        }
59
60        current_versions
61    }
62
63    /// Consumes `self` to produce the most recent version (determined by order of tags).
64    #[must_use]
65    pub fn into_latest(mut self) -> Version {
66        self.prereleases.pop_last().map_or(
67            Version::Stable(self.stable),
68            |(stable_component, pres)| {
69                let pre_component = pres.into_last();
70                Version::Pre(PreVersion {
71                    stable_component,
72                    pre_component,
73                })
74            },
75        )
76    }
77
78    /// Replace or insert the version in the correct location if it's newer than the current
79    /// equivalent version.
80    /// If the version is a newer stable version, it will update `stable`
81    /// and erase all pre-releases.
82    /// If the version is a newer prerelease, it will overwrite the prerelease with
83    /// the same stable component and label.
84    pub fn update_version(&mut self, version: Version) {
85        match version {
86            Version::Stable(new) => {
87                if self.stable >= new {
88                    return;
89                }
90                self.stable = new;
91                self.prereleases.clear();
92            }
93            Version::Pre(PreVersion {
94                stable_component,
95                pre_component,
96            }) => {
97                let recorded_pre = self
98                    .prereleases
99                    .get(&stable_component)
100                    .and_then(|pres| pres.get(&pre_component.label));
101                if let Some(recorded_pre) = recorded_pre {
102                    if recorded_pre >= &pre_component {
103                        return;
104                    }
105                }
106                if let Some(labels) = self.prereleases.get_mut(&stable_component) {
107                    labels.insert(pre_component);
108                } else {
109                    self.prereleases
110                        .insert(stable_component, PrereleaseMap::new(pre_component));
111                }
112            }
113        }
114    }
115
116    /// Apply a Rule to a [`PackageVersion`], incrementing & resetting the correct components.
117    ///
118    /// # Versions 0.x
119    ///
120    /// Versions with major component 0 have special meaning in Semantic Versioning and therefore have
121    /// different behavior:
122    /// 1. [`Rule::Major`] will bump the minor component.
123    /// 2. [`Rule::Minor`] will bump the patch component.
124    ///
125    /// # Errors
126    ///
127    /// Can fail if trying to run [`Rule::Release`] when there is no pre-release.
128    pub fn bump(&mut self, rule: Rule) -> Result<(), PreReleaseNotFound> {
129        match rule {
130            Rule::Major => self.update_version(bump_stable(self.stable, Stable::Major).into()),
131            Rule::Minor => self.update_version(bump_stable(self.stable, Stable::Minor).into()),
132            Rule::Patch => self.update_version(bump_stable(self.stable, Stable::Patch).into()),
133            Rule::Release => {
134                let version = self
135                    .prereleases
136                    .pop_last()
137                    .map(|(version, _pre)| version)
138                    .ok_or(PreReleaseNotFound)?
139                    .into();
140                self.update_version(version);
141            }
142            Rule::Pre { label, stable_rule } => self.bump_pre(label, stable_rule),
143        }
144        Ok(())
145    }
146
147    #[must_use]
148    pub fn stable(&self) -> StableVersion {
149        self.stable
150    }
151
152    /// Bumps the pre-release component of a [`Version`] after applying the `stable_rule`.
153    ///
154    /// # Errors
155    ///
156    /// Can fail if there's an existing pre-release component that can't be incremented.
157    fn bump_pre(&mut self, label: Label, stable_rule: Stable) {
158        debug!("Pre-release label {label} selected. Determining next stable version...");
159        let stable_component = bump_stable(self.stable, stable_rule);
160        let pre_version = self
161            .prereleases
162            .get(&stable_component)
163            .and_then(|pres| {
164                pres.get(&label).map(|pre| {
165                    debug!("Found existing pre-release version {pre}");
166                    pre.version + 1
167                })
168            })
169            .unwrap_or_default();
170        let pre = Prerelease::new(label, pre_version);
171        if pre_version == 0 {
172            debug!("No existing pre-release version found; creating {pre}");
173        }
174
175        self.prereleases.clear();
176
177        self.update_version(Version::Pre(PreVersion {
178            stable_component,
179            pre_component: pre,
180        }));
181    }
182}
183
184fn bump_stable(version: StableVersion, rule: Stable) -> StableVersion {
185    let is_0 = version.major == 0;
186    match (rule, is_0) {
187        (Stable::Major, false) => {
188            let new = version.increment_major();
189            debug!("Using MAJOR rule to bump from {version} to {new}");
190            new
191        }
192        (Stable::Minor, false) => {
193            let new = version.increment_minor();
194            debug!("Using MINOR rule to bump from {version} to {new}");
195            new
196        }
197        (Stable::Major, true) => {
198            let new = version.increment_minor();
199            debug!("Rule is MAJOR, but major component is 0. Bumping minor component from {version} to {new}");
200            new
201        }
202        (Stable::Minor, true) => {
203            let new = version.increment_patch();
204            debug!("Rule is MINOR, but major component is 0. Bumping patch component from {version} to {new}");
205            new
206        }
207        (Stable::Patch, _) => {
208            let new = version.increment_patch();
209            debug!("Using PATCH rule to bump from {version} to {new}");
210            new
211        }
212    }
213}
214
215// #[derive(Debug, thiserror::Error)]
216// #[cfg_attr(feature = "miette", derive(Diagnostic))]
217// #[error("Could not increment pre-release version {0}")]
218// #[cfg_attr(
219//     feature = "miette",
220//     diagnostic(
221//         code(semver::invalid_pre_release_version),
222//         help(
223//             "The pre-release component of a version must be in the format of `-<label>.N` \
224//                     where <label> is a string and `N` is an integer"
225//         ),
226//         url("https://knope.tech/reference/concepts/semantic-versioning/#types-of-releases")
227//     )
228// )]
229// pub(crate) struct InvalidPreReleaseVersion(String);
230
231#[derive(Debug, thiserror::Error)]
232#[error("No prerelease version found, but a Release rule was requested")]
233pub struct PreReleaseNotFound;
234
235impl From<StableVersion> for PackageVersions {
236    fn from(version: StableVersion) -> Self {
237        Self {
238            stable: version,
239            prereleases: BTreeMap::new(),
240        }
241    }
242}
243
244impl From<Version> for PackageVersions {
245    fn from(version: Version) -> Self {
246        let mut new = Self::default();
247        new.update_version(version);
248        new
249    }
250}
251
252#[cfg(test)]
253#[allow(clippy::unwrap_used)]
254mod test_from_tags {
255    use std::str::FromStr;
256
257    use pretty_assertions::assert_eq;
258
259    use crate::semver::{PackageVersions, Prerelease, StableVersion, Version};
260    #[test]
261    fn collect_all_newer_pre_releases() {
262        let tags = [
263            "v2.0.0-alpha.0",
264            "v1.3.0-beta.0",
265            "v1.3.0-alpha.1",
266            "v1.3.0-alpha.0",
267            "v1.2.4-rc.0",
268            "v1.2.3",
269        ]
270        .map(String::from);
271
272        let versions = PackageVersions::from_tags(None, &tags);
273
274        assert_eq!(
275            versions.stable(),
276            StableVersion {
277                major: 1,
278                minor: 2,
279                patch: 3
280            }
281        );
282
283        assert_eq!(
284            versions.clone().into_latest(),
285            Version::from_str("2.0.0-alpha.0").unwrap()
286        );
287        assert_eq!(
288            *versions
289                .prereleases
290                .get(&StableVersion {
291                    major: 1,
292                    minor: 3,
293                    patch: 0
294                })
295                .unwrap()
296                .get(&"alpha".into())
297                .unwrap(),
298            Prerelease::new("alpha".into(), 1)
299        );
300
301        assert_eq!(
302            *versions
303                .prereleases
304                .get(&StableVersion {
305                    major: 1,
306                    minor: 3,
307                    patch: 0
308                })
309                .unwrap()
310                .get(&"beta".into())
311                .unwrap(),
312            Prerelease::new("beta".into(), 0)
313        );
314    }
315}
316
317#[cfg(test)]
318#[allow(clippy::unwrap_used)]
319mod test_bump {
320    use std::str::FromStr;
321
322    use super::*;
323
324    #[test]
325    fn major() {
326        let mut versions: PackageVersions = Version::new(1, 2, 3, None).into();
327        versions.bump(Rule::Major).unwrap();
328
329        assert_eq!(versions.into_latest(), Version::new(2, 0, 0, None));
330    }
331
332    #[test]
333    fn major_0() {
334        let mut versions = PackageVersions::from(Version::new(0, 1, 2, None));
335        versions.bump(Rule::Major).unwrap();
336
337        assert_eq!(versions.into_latest(), Version::new(0, 2, 0, None));
338    }
339
340    #[test]
341    fn major_unset() {
342        let mut versions = PackageVersions::default();
343        versions.bump(Rule::Major).unwrap();
344
345        assert_eq!(versions.into_latest(), Version::new(0, 1, 0, None));
346    }
347
348    #[test]
349    fn major_after_pre() {
350        for pre_version in ["1.2.4-rc.0", "1.3.0-rc.0", "2.0.0-rc.0"] {
351            let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
352            versions.update_version(Version::from_str(pre_version).unwrap());
353            versions.bump(Rule::Major).unwrap();
354
355            assert_eq!(versions.into_latest(), Version::new(2, 0, 0, None));
356        }
357    }
358
359    #[test]
360    fn minor() {
361        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
362        versions.bump(Rule::Minor).unwrap();
363
364        assert_eq!(versions.into_latest(), Version::new(1, 3, 0, None));
365    }
366
367    #[test]
368    fn minor_0() {
369        let mut versions = PackageVersions::from(Version::new(0, 1, 2, None));
370        versions.bump(Rule::Minor).unwrap();
371
372        assert_eq!(versions.into_latest(), Version::new(0, 1, 3, None));
373    }
374
375    #[test]
376    fn minor_unset() {
377        let mut versions = PackageVersions::default();
378        versions.bump(Rule::Minor).unwrap();
379
380        assert_eq!(versions.into_latest(), Version::new(0, 0, 1, None));
381    }
382
383    #[test]
384    fn minor_after_pre() {
385        for pre_version in ["1.2.4-rc.0", "1.3.0-rc.0"] {
386            let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
387            versions.update_version(Version::from_str(pre_version).unwrap());
388            versions.bump(Rule::Minor).unwrap();
389
390            assert_eq!(versions.into_latest(), Version::new(1, 3, 0, None));
391        }
392    }
393
394    #[test]
395    fn patch() {
396        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
397        versions.bump(Rule::Patch).unwrap();
398
399        assert_eq!(versions.into_latest(), Version::new(1, 2, 4, None));
400    }
401
402    #[test]
403    fn patch_0() {
404        let mut versions = PackageVersions::from(Version::new(0, 1, 0, None));
405        versions.bump(Rule::Patch).unwrap();
406
407        assert_eq!(versions.into_latest(), Version::new(0, 1, 1, None));
408    }
409
410    #[test]
411    fn patch_unset() {
412        let mut versions = PackageVersions::default();
413        versions.bump(Rule::Patch).unwrap();
414
415        assert_eq!(versions.into_latest(), Version::new(0, 0, 1, None));
416    }
417
418    #[test]
419    fn patch_after_pre() {
420        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
421        versions.update_version(Version::from_str("1.2.4-rc.0").unwrap());
422        versions.bump(Rule::Patch).unwrap();
423
424        assert_eq!(versions.into_latest(), Version::new(1, 2, 4, None));
425    }
426
427    #[test]
428    fn pre() {
429        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
430        versions
431            .bump(Rule::Pre {
432                label: Label::from("rc"),
433                stable_rule: Stable::Minor,
434            })
435            .unwrap();
436
437        assert_eq!(
438            versions.into_latest(),
439            Version::from_str("1.3.0-rc.0").unwrap()
440        );
441    }
442
443    #[test]
444    fn pre_after_same_pre() {
445        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
446        versions.update_version(Version::from_str("1.3.0-rc.0").unwrap());
447        versions.update_version(Version::from_str("1.2.4-rc.1").unwrap());
448        versions.update_version(Version::from_str("2.0.0-rc.2").unwrap());
449        versions
450            .bump(Rule::Pre {
451                label: Label::from("rc"),
452                stable_rule: Stable::Minor,
453            })
454            .unwrap();
455
456        assert_eq!(
457            versions.into_latest(),
458            Version::from_str("1.3.0-rc.1").unwrap()
459        );
460    }
461
462    #[test]
463    fn pre_after_different_pre_version() {
464        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
465        versions.update_version(Version::from_str("1.2.4-beta.1").unwrap());
466        versions.update_version(Version::from_str("1.2.4-rc.0").unwrap());
467        versions
468            .bump(Rule::Pre {
469                label: Label::from("beta"),
470                stable_rule: Stable::Patch,
471            })
472            .unwrap();
473
474        assert_eq!(
475            versions.into_latest(),
476            Version::from_str("1.2.4-beta.2").unwrap()
477        );
478    }
479
480    #[test]
481    fn pre_after_different_pre_label() {
482        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
483        versions.update_version(Version::from_str("1.3.0-beta.0").unwrap());
484        versions
485            .bump(Rule::Pre {
486                label: Label::from("rc"),
487                stable_rule: Stable::Minor,
488            })
489            .unwrap();
490
491        assert_eq!(
492            versions.into_latest(),
493            Version::from_str("1.3.0-rc.0").unwrap()
494        );
495    }
496
497    #[test]
498    fn release() {
499        let mut versions = PackageVersions::default();
500        versions.update_version(Version::from_str("1.2.3-rc.0").unwrap());
501        versions.update_version(Version::from_str("1.2.4-rc.1").unwrap());
502        versions.update_version(Version::from_str("2.0.0-rc.2").unwrap());
503
504        versions.bump(Rule::Release).unwrap();
505
506        assert_eq!(versions.into_latest(), Version::new(2, 0, 0, None));
507    }
508}