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: 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!(
200                "Rule is MAJOR, but major component is 0. Bumping minor component from {version} to {new}"
201            );
202            new
203        }
204        (Stable::Minor, true) => {
205            let new = version.increment_patch();
206            debug!(
207                "Rule is MINOR, but major component is 0. Bumping patch component from {version} to {new}"
208            );
209            new
210        }
211        (Stable::Patch, _) => {
212            let new = version.increment_patch();
213            debug!("Using PATCH rule to bump from {version} to {new}");
214            new
215        }
216    }
217}
218
219// #[derive(Debug, thiserror::Error)]
220// #[cfg_attr(feature = "miette", derive(Diagnostic))]
221// #[error("Could not increment pre-release version {0}")]
222// #[cfg_attr(
223//     feature = "miette",
224//     diagnostic(
225//         code(semver::invalid_pre_release_version),
226//         help(
227//             "The pre-release component of a version must be in the format of `-<label>.N` \
228//                     where <label> is a string and `N` is an integer"
229//         ),
230//         url("https://knope.tech/reference/concepts/semantic-versioning/#types-of-releases")
231//     )
232// )]
233// pub(crate) struct InvalidPreReleaseVersion(String);
234
235#[derive(Debug, thiserror::Error)]
236#[error("No prerelease version found, but a Release rule was requested")]
237pub struct PreReleaseNotFound;
238
239impl From<StableVersion> for PackageVersions {
240    fn from(version: StableVersion) -> Self {
241        Self {
242            stable: version,
243            prereleases: BTreeMap::new(),
244        }
245    }
246}
247
248impl From<Version> for PackageVersions {
249    fn from(version: Version) -> Self {
250        let mut new = Self::default();
251        new.update_version(version);
252        new
253    }
254}
255
256#[cfg(test)]
257#[allow(clippy::unwrap_used)]
258mod test_from_tags {
259    use std::str::FromStr;
260
261    use pretty_assertions::assert_eq;
262
263    use crate::semver::{PackageVersions, Prerelease, StableVersion, Version};
264    #[test]
265    fn collect_all_newer_pre_releases() {
266        let tags = [
267            "v2.0.0-alpha.0",
268            "v1.3.0-beta.0",
269            "v1.3.0-alpha.1",
270            "v1.3.0-alpha.0",
271            "v1.2.4-rc.0",
272            "v1.2.3",
273        ]
274        .map(String::from);
275
276        let versions = PackageVersions::from_tags(None, &tags);
277
278        assert_eq!(
279            versions.stable(),
280            StableVersion {
281                major: 1,
282                minor: 2,
283                patch: 3
284            }
285        );
286
287        assert_eq!(
288            versions.clone().into_latest(),
289            Version::from_str("2.0.0-alpha.0").unwrap()
290        );
291        assert_eq!(
292            *versions
293                .prereleases
294                .get(&StableVersion {
295                    major: 1,
296                    minor: 3,
297                    patch: 0
298                })
299                .unwrap()
300                .get(&"alpha".into())
301                .unwrap(),
302            Prerelease::new("alpha".into(), 1)
303        );
304
305        assert_eq!(
306            *versions
307                .prereleases
308                .get(&StableVersion {
309                    major: 1,
310                    minor: 3,
311                    patch: 0
312                })
313                .unwrap()
314                .get(&"beta".into())
315                .unwrap(),
316            Prerelease::new("beta".into(), 0)
317        );
318    }
319}
320
321#[cfg(test)]
322#[allow(clippy::unwrap_used)]
323mod test_bump {
324    use std::str::FromStr;
325
326    use super::*;
327
328    #[test]
329    fn major() {
330        let mut versions: PackageVersions = Version::new(1, 2, 3, None).into();
331        versions.bump(Rule::Major).unwrap();
332
333        assert_eq!(versions.into_latest(), Version::new(2, 0, 0, None));
334    }
335
336    #[test]
337    fn major_0() {
338        let mut versions = PackageVersions::from(Version::new(0, 1, 2, None));
339        versions.bump(Rule::Major).unwrap();
340
341        assert_eq!(versions.into_latest(), Version::new(0, 2, 0, None));
342    }
343
344    #[test]
345    fn major_unset() {
346        let mut versions = PackageVersions::default();
347        versions.bump(Rule::Major).unwrap();
348
349        assert_eq!(versions.into_latest(), Version::new(0, 1, 0, None));
350    }
351
352    #[test]
353    fn major_after_pre() {
354        for pre_version in ["1.2.4-rc.0", "1.3.0-rc.0", "2.0.0-rc.0"] {
355            let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
356            versions.update_version(Version::from_str(pre_version).unwrap());
357            versions.bump(Rule::Major).unwrap();
358
359            assert_eq!(versions.into_latest(), Version::new(2, 0, 0, None));
360        }
361    }
362
363    #[test]
364    fn minor() {
365        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
366        versions.bump(Rule::Minor).unwrap();
367
368        assert_eq!(versions.into_latest(), Version::new(1, 3, 0, None));
369    }
370
371    #[test]
372    fn minor_0() {
373        let mut versions = PackageVersions::from(Version::new(0, 1, 2, None));
374        versions.bump(Rule::Minor).unwrap();
375
376        assert_eq!(versions.into_latest(), Version::new(0, 1, 3, None));
377    }
378
379    #[test]
380    fn minor_unset() {
381        let mut versions = PackageVersions::default();
382        versions.bump(Rule::Minor).unwrap();
383
384        assert_eq!(versions.into_latest(), Version::new(0, 0, 1, None));
385    }
386
387    #[test]
388    fn minor_after_pre() {
389        for pre_version in ["1.2.4-rc.0", "1.3.0-rc.0"] {
390            let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
391            versions.update_version(Version::from_str(pre_version).unwrap());
392            versions.bump(Rule::Minor).unwrap();
393
394            assert_eq!(versions.into_latest(), Version::new(1, 3, 0, None));
395        }
396    }
397
398    #[test]
399    fn patch() {
400        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
401        versions.bump(Rule::Patch).unwrap();
402
403        assert_eq!(versions.into_latest(), Version::new(1, 2, 4, None));
404    }
405
406    #[test]
407    fn patch_0() {
408        let mut versions = PackageVersions::from(Version::new(0, 1, 0, None));
409        versions.bump(Rule::Patch).unwrap();
410
411        assert_eq!(versions.into_latest(), Version::new(0, 1, 1, None));
412    }
413
414    #[test]
415    fn patch_unset() {
416        let mut versions = PackageVersions::default();
417        versions.bump(Rule::Patch).unwrap();
418
419        assert_eq!(versions.into_latest(), Version::new(0, 0, 1, None));
420    }
421
422    #[test]
423    fn patch_after_pre() {
424        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
425        versions.update_version(Version::from_str("1.2.4-rc.0").unwrap());
426        versions.bump(Rule::Patch).unwrap();
427
428        assert_eq!(versions.into_latest(), Version::new(1, 2, 4, None));
429    }
430
431    #[test]
432    fn pre() {
433        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
434        versions
435            .bump(Rule::Pre {
436                label: Label::from("rc"),
437                stable_rule: Stable::Minor,
438            })
439            .unwrap();
440
441        assert_eq!(
442            versions.into_latest(),
443            Version::from_str("1.3.0-rc.0").unwrap()
444        );
445    }
446
447    #[test]
448    fn pre_after_same_pre() {
449        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
450        versions.update_version(Version::from_str("1.3.0-rc.0").unwrap());
451        versions.update_version(Version::from_str("1.2.4-rc.1").unwrap());
452        versions.update_version(Version::from_str("2.0.0-rc.2").unwrap());
453        versions
454            .bump(Rule::Pre {
455                label: Label::from("rc"),
456                stable_rule: Stable::Minor,
457            })
458            .unwrap();
459
460        assert_eq!(
461            versions.into_latest(),
462            Version::from_str("1.3.0-rc.1").unwrap()
463        );
464    }
465
466    #[test]
467    fn pre_after_different_pre_version() {
468        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
469        versions.update_version(Version::from_str("1.2.4-beta.1").unwrap());
470        versions.update_version(Version::from_str("1.2.4-rc.0").unwrap());
471        versions
472            .bump(Rule::Pre {
473                label: Label::from("beta"),
474                stable_rule: Stable::Patch,
475            })
476            .unwrap();
477
478        assert_eq!(
479            versions.into_latest(),
480            Version::from_str("1.2.4-beta.2").unwrap()
481        );
482    }
483
484    #[test]
485    fn pre_after_different_pre_label() {
486        let mut versions = PackageVersions::from(Version::new(1, 2, 3, None));
487        versions.update_version(Version::from_str("1.3.0-beta.0").unwrap());
488        versions
489            .bump(Rule::Pre {
490                label: Label::from("rc"),
491                stable_rule: Stable::Minor,
492            })
493            .unwrap();
494
495        assert_eq!(
496            versions.into_latest(),
497            Version::from_str("1.3.0-rc.0").unwrap()
498        );
499    }
500
501    #[test]
502    fn release() {
503        let mut versions = PackageVersions::default();
504        versions.update_version(Version::from_str("1.2.3-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
508        versions.bump(Rule::Release).unwrap();
509
510        assert_eq!(versions.into_latest(), Version::new(2, 0, 0, None));
511    }
512}