Skip to main content

knope_versioning/semver/
package_versions.rs

1use std::{collections::BTreeMap, fmt::Debug, str::FromStr};
2
3use tracing::debug;
4
5use super::{
6    Label, PreVersion, Prerelease, Rule, StableVersion, Version, prerelease_map::PrereleaseMap,
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: Option<StableVersion>,
18    pub(crate) 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 starting with {pattern}");
42        }
43
44        let mut current_versions = Self::default();
45        for tag in tags {
46            let version_string = tag.as_ref().replacen(&pattern, "", 1);
47            if let Ok(version) = Version::from_str(version_string.as_str()) {
48                match version {
49                    Version::Stable(stable) => {
50                        current_versions.stable = Some(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 latest(&self) -> Option<Version> {
66        self.prereleases
67            .last_key_value()
68            .map(|(stable_component, pres)| {
69                let pre_component = pres.last().clone();
70                Version::Pre(PreVersion {
71                    stable_component: *stable_component,
72                    pre_component,
73                })
74            })
75            .or_else(|| self.stable.map(Version::Stable))
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(crate) fn update_version(&mut self, version: Version) {
85        match version {
86            Version::Stable(new) => {
87                if self.stable.is_some_and(|it| it >= new) {
88                    return;
89                }
90                self.stable = Some(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. [`Stable(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(crate) fn calculate_new_version(&self, rule: Rule) -> Result<Version, PreReleaseNotFound> {
129        match (rule, self.stable) {
130            (Rule::Stable(rule), Some(stable)) => {
131                let version: Version = bump_stable(stable, rule).into();
132                Ok(version)
133            }
134            (Rule::Stable(_), None) => {
135                // Bumping the stable version, but there is no previous stable version.
136                // So we use the last pre-release version as-is (assuming it was set manually)
137                // _or_ 0.0.0 as the first version of the project.
138                let version: Version = self
139                    .prereleases
140                    .last_key_value()
141                    .map(|(version, _pre)| *version)
142                    .unwrap_or_default()
143                    .into();
144                Ok(version)
145            }
146            (Rule::Pre { label, stable_rule }, _) => Ok(self.calculate_pre(label, stable_rule)),
147            (Rule::Release, _) => {
148                let version: Version = self
149                    .prereleases
150                    .last_key_value()
151                    .map(|(version, _pre)| (*version).into())
152                    .ok_or(PreReleaseNotFound)?;
153                Ok(version)
154            }
155        }
156    }
157
158    #[must_use]
159    pub fn stable(&self) -> Option<StableVersion> {
160        self.stable
161    }
162
163    /// Gets the new pre-release component of a [`Version`] after applying the `stable_rule`.
164    fn calculate_pre(&self, label: Label, stable_rule: Stable) -> Version {
165        debug!("Pre-release label {label} selected. Determining next stable version...");
166        let stable_component = if let Some(stable) = self.stable {
167            bump_stable(stable, stable_rule)
168        } else {
169            self.prereleases
170                .last_key_value()
171                .map(|(stable, _)| *stable)
172                .unwrap_or_default()
173        };
174        let pre_version = self
175            .prereleases
176            .get(&stable_component)
177            .and_then(|pres| {
178                pres.get(&label).map(|pre| {
179                    debug!("Found existing pre-release version {pre}");
180                    pre.version + 1
181                })
182            })
183            .unwrap_or_default();
184        let pre = Prerelease::new(label, pre_version);
185        if pre_version == 0 {
186            debug!("No existing pre-release version found; creating {pre}");
187        }
188
189        Version::Pre(PreVersion {
190            stable_component,
191            pre_component: pre,
192        })
193    }
194}
195
196fn bump_stable(version: StableVersion, rule: Stable) -> StableVersion {
197    let is_0 = version.major == 0;
198    match (rule, is_0) {
199        (Stable::Major, false) => {
200            let new = version.increment_major();
201            debug!("Using MAJOR rule to bump from {version} to {new}");
202            new
203        }
204        (Stable::Minor, false) => {
205            let new = version.increment_minor();
206            debug!("Using MINOR rule to bump from {version} to {new}");
207            new
208        }
209        (Stable::Major, true) => {
210            let new = version.increment_minor();
211            debug!(
212                "Rule is MAJOR, but major component is 0. Bumping minor component from {version} to {new}"
213            );
214            new
215        }
216        (Stable::Minor, true) => {
217            let new = version.increment_patch();
218            debug!(
219                "Rule is MINOR, but major component is 0. Bumping patch component from {version} to {new}"
220            );
221            new
222        }
223        (Stable::Patch, _) => {
224            let new = version.increment_patch();
225            debug!("Using PATCH rule to bump from {version} to {new}");
226            new
227        }
228    }
229}
230
231// #[derive(Debug, thiserror::Error)]
232// #[cfg_attr(feature = "miette", derive(Diagnostic))]
233// #[error("Could not increment pre-release version {0}")]
234// #[cfg_attr(
235//     feature = "miette",
236//     diagnostic(
237//         code(semver::invalid_pre_release_version),
238//         help(
239//             "The pre-release component of a version must be in the format of `-<label>.N` \
240//                     where <label> is a string and `N` is an integer"
241//         ),
242//         url("https://knope.tech/reference/concepts/semantic-versioning/#types-of-releases")
243//     )
244// )]
245// pub(crate) struct InvalidPreReleaseVersion(String);
246
247#[derive(Debug, thiserror::Error)]
248#[error("No prerelease version found, but a Release rule was requested")]
249pub struct PreReleaseNotFound;
250
251impl From<StableVersion> for PackageVersions {
252    fn from(version: StableVersion) -> Self {
253        Self {
254            stable: Some(version),
255            prereleases: BTreeMap::new(),
256        }
257    }
258}
259
260impl From<Version> for PackageVersions {
261    fn from(version: Version) -> Self {
262        let mut new = Self::default();
263        new.update_version(version);
264        new
265    }
266}
267
268#[cfg(test)]
269mod test_from_tags {
270    use std::str::FromStr;
271
272    use pretty_assertions::assert_eq;
273
274    use crate::semver::{PackageVersions, Prerelease, StableVersion, Version};
275    #[test]
276    fn collect_all_newer_pre_releases() {
277        let tags = [
278            "v2.0.0-alpha.0",
279            "v1.3.0-beta.0",
280            "v1.3.0-alpha.1",
281            "v1.3.0-alpha.0",
282            "v1.2.4-rc.0",
283            "v1.2.3",
284        ]
285        .map(String::from);
286
287        let versions = PackageVersions::from_tags(None, &tags);
288
289        assert_eq!(
290            versions.stable().unwrap(),
291            StableVersion {
292                major: 1,
293                minor: 2,
294                patch: 3
295            }
296        );
297
298        assert_eq!(versions.latest(), Version::from_str("2.0.0-alpha.0").ok());
299        assert_eq!(
300            *versions
301                .prereleases
302                .get(&StableVersion {
303                    major: 1,
304                    minor: 3,
305                    patch: 0
306                })
307                .unwrap()
308                .get(&"alpha".into())
309                .unwrap(),
310            Prerelease::new("alpha".into(), 1)
311        );
312
313        assert_eq!(
314            *versions
315                .prereleases
316                .get(&StableVersion {
317                    major: 1,
318                    minor: 3,
319                    patch: 0
320                })
321                .unwrap()
322                .get(&"beta".into())
323                .unwrap(),
324            Prerelease::new("beta".into(), 0)
325        );
326    }
327}
328
329#[cfg(test)]
330mod test_calculate_new_version {
331    use std::str::FromStr;
332
333    use super::*;
334    use crate::semver::{Rule::*, StableRule::*};
335
336    #[test]
337    fn major() {
338        let versions: PackageVersions = Version::new(1, 2, 3, None).into();
339        let new = versions.calculate_new_version(Stable(Major)).unwrap();
340
341        assert_eq!(new, Version::new(2, 0, 0, None));
342    }
343
344    #[test]
345    fn major_0() {
346        let versions: PackageVersions = Version::new(0, 1, 2, None).into();
347        let new = versions.calculate_new_version(Stable(Major)).unwrap();
348
349        assert_eq!(new, Version::new(0, 2, 0, None));
350    }
351
352    #[test]
353    fn major_pre_only() {
354        let versions = PackageVersions::from_tags(None, &["v1.0.0-rc.0"]);
355        let new = versions.calculate_new_version(Stable(Major)).unwrap();
356
357        assert_eq!(new, Version::new(1, 0, 0, None));
358    }
359
360    #[test]
361    fn major_unset() {
362        let versions = PackageVersions::default();
363        let new = versions.calculate_new_version(Stable(Major)).unwrap();
364
365        assert_eq!(new, Version::new(0, 0, 0, None));
366    }
367
368    #[test]
369    fn major_after_pre() {
370        for pre_version in ["1.2.4-rc.0", "1.3.0-rc.0", "2.0.0-rc.0"] {
371            let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
372            versions.update_version(Version::from_str(pre_version).unwrap());
373            let new = versions.calculate_new_version(Stable(Major)).unwrap();
374
375            assert_eq!(new, Version::new(2, 0, 0, None));
376        }
377    }
378
379    #[test]
380    fn minor() {
381        let versions: PackageVersions = Version::new(1, 2, 3, None).into();
382        let new = versions.calculate_new_version(Stable(Minor)).unwrap();
383
384        assert_eq!(new, Version::new(1, 3, 0, None));
385    }
386
387    #[test]
388    fn minor_0() {
389        let versions = PackageVersions::from(Version::new(0, 1, 2, None));
390        let new = versions.calculate_new_version(Stable(Minor)).unwrap();
391
392        assert_eq!(new, Version::new(0, 1, 3, None));
393    }
394
395    #[test]
396    fn minor_pre_only() {
397        let versions = PackageVersions::from_tags(None, &["v1.0.0-rc.0"]);
398        let new = versions.calculate_new_version(Stable(Minor)).unwrap();
399
400        assert_eq!(new, Version::new(1, 0, 0, None));
401    }
402
403    #[test]
404    fn minor_unset() {
405        let versions = PackageVersions::default();
406        let new = versions.calculate_new_version(Stable(Minor)).unwrap();
407
408        assert_eq!(new, Version::new(0, 0, 0, None));
409    }
410
411    #[test]
412    fn minor_after_pre() {
413        for pre_version in ["1.2.4-rc.0", "1.3.0-rc.0"] {
414            let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
415            versions.update_version(Version::from_str(pre_version).unwrap());
416            let new = versions.calculate_new_version(Stable(Minor)).unwrap();
417
418            assert_eq!(new, Version::new(1, 3, 0, None));
419        }
420    }
421
422    #[test]
423    fn patch() {
424        let versions = PackageVersions::from(Version::new(1, 2, 3, None));
425        let new = versions.calculate_new_version(Stable(Patch)).unwrap();
426
427        assert_eq!(new, Version::new(1, 2, 4, None));
428    }
429
430    #[test]
431    fn patch_0() {
432        let versions = PackageVersions::from(Version::new(0, 1, 0, None));
433        let new = versions.calculate_new_version(Stable(Patch)).unwrap();
434
435        assert_eq!(new, Version::new(0, 1, 1, None));
436    }
437
438    #[test]
439    fn patch_pre_only() {
440        let versions = PackageVersions::from_tags(None, &["v1.0.0-rc.0"]);
441        let new = versions.calculate_new_version(Stable(Patch)).unwrap();
442
443        assert_eq!(new, Version::new(1, 0, 0, None));
444    }
445
446    #[test]
447    fn patch_unset() {
448        let versions = PackageVersions::default();
449        let new = versions.calculate_new_version(Stable(Patch)).unwrap();
450
451        assert_eq!(new, Version::new(0, 0, 0, None));
452    }
453
454    #[test]
455    fn patch_after_pre() {
456        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
457        versions.update_version(Version::from_str("1.2.4-rc.0").unwrap());
458        let new = versions.calculate_new_version(Stable(Patch)).unwrap();
459
460        assert_eq!(new, Version::new(1, 2, 4, None));
461    }
462
463    #[test]
464    fn pre() {
465        let versions = PackageVersions::from(Version::new(1, 2, 3, None));
466        let new = versions
467            .calculate_new_version(Pre {
468                label: Label::from("rc"),
469                stable_rule: Minor,
470            })
471            .unwrap();
472
473        assert_eq!(new, Version::from_str("1.3.0-rc.0").unwrap());
474    }
475
476    #[test]
477    fn pre_after_same_pre() {
478        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
479        versions.update_version(Version::from_str("1.3.0-rc.0").unwrap());
480        versions.update_version(Version::from_str("1.2.4-rc.1").unwrap());
481        versions.update_version(Version::from_str("2.0.0-rc.2").unwrap());
482        let new = versions
483            .calculate_new_version(Pre {
484                label: Label::from("rc"),
485                stable_rule: Minor,
486            })
487            .unwrap();
488
489        assert_eq!(new, Version::from_str("1.3.0-rc.1").unwrap());
490    }
491
492    #[test]
493    fn pre_without_stable() {
494        let mut versions = PackageVersions::default();
495        versions.update_version(Version::from_str("1.3.0-rc.0").unwrap());
496        versions.update_version(Version::from_str("1.2.4-rc.1").unwrap());
497        versions.update_version(Version::from_str("2.0.0-rc.2").unwrap());
498        let new = versions
499            .calculate_new_version(Pre {
500                label: Label::from("rc"),
501                stable_rule: Minor,
502            })
503            .unwrap();
504
505        assert_eq!(new, Version::from_str("2.0.0-rc.3").unwrap());
506    }
507
508    #[test]
509    fn pre_after_different_pre_version() {
510        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
511        versions.update_version(Version::from_str("1.2.4-beta.1").unwrap());
512        versions.update_version(Version::from_str("1.2.4-rc.0").unwrap());
513        let new = versions
514            .calculate_new_version(Pre {
515                label: Label::from("beta"),
516                stable_rule: Patch,
517            })
518            .unwrap();
519
520        assert_eq!(new, Version::from_str("1.2.4-beta.2").unwrap());
521    }
522
523    #[test]
524    fn pre_after_different_pre_label() {
525        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
526        versions.update_version(Version::from_str("1.3.0-beta.0").unwrap());
527        let new = versions
528            .calculate_new_version(Pre {
529                label: Label::from("rc"),
530                stable_rule: Minor,
531            })
532            .unwrap();
533
534        assert_eq!(new, Version::from_str("1.3.0-rc.0").unwrap());
535    }
536
537    #[test]
538    fn release() {
539        let mut versions = PackageVersions::default();
540        versions.update_version(Version::from_str("1.2.3-rc.0").unwrap());
541        versions.update_version(Version::from_str("1.2.4-rc.1").unwrap());
542        versions.update_version(Version::from_str("2.0.0-rc.2").unwrap());
543
544        let new = versions.calculate_new_version(Release).unwrap();
545
546        assert_eq!(new, Version::new(2, 0, 0, None));
547    }
548}