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