Skip to main content

ephemeris_release/unit/
status.rs

1//! Where a unit stands between its file version and its tags.
2
3use crate::unit::Unit;
4use crate::version::TagVersion;
5
6/// Tags exist under a unit's prefix and none of them names a version.
7#[derive(Debug, thiserror::Error)]
8#[error("unit {unit:?} has tag {tag:?} under its prefix, which is not a version")]
9pub(crate) struct UnparsableTag {
10    /// The unit name.
11    pub(crate) unit: String,
12    /// The first tag under the prefix.
13    pub(crate) tag: String,
14}
15
16/// Where a unit stands between its file version and its tags.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub(crate) enum UnitStatus<'t> {
19    /// No tag and the file version is 0.0.0. The proposal derives the first
20    /// version from the commits.
21    Unversioned,
22    /// The file version is newer than every tag. Publication releases it.
23    Pending,
24    /// A tag covers the file version.
25    Released {
26        /// The newest tag.
27        latest: &'t str,
28        /// Whether the file version has a tag of its own.
29        tagged: bool,
30    },
31}
32
33impl Unit {
34    /// The status of this unit given the `tags` under its prefix.
35    ///
36    /// # Errors
37    /// Returns an error when tags exist and none of them names a version.
38    pub(crate) fn status<'t>(&self, tags: &'t [String]) -> Result<UnitStatus<'t>, UnparsableTag> {
39        let Some((latest, tag)) = TagVersion::latest(tags, &self.tag_prefix) else {
40            if let Some(tag) = tags.first() {
41                return Err(UnparsableTag {
42                    unit: self.name.clone(),
43                    tag: tag.clone(),
44                });
45            }
46            return Ok(if self.version.is_unversioned() {
47                UnitStatus::Unversioned
48            } else {
49                UnitStatus::Pending
50            });
51        };
52        if tags.contains(&self.tag()) {
53            return Ok(UnitStatus::Released {
54                latest: tag,
55                tagged: true,
56            });
57        }
58        if TagVersion::new(self.version.clone(), self.revision) > latest {
59            Ok(UnitStatus::Pending)
60        } else {
61            Ok(UnitStatus::Released {
62                latest: tag,
63                tagged: false,
64            })
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::{UnitStatus, UnparsableTag};
72    use crate::state::ReleaseUnitState;
73    use crate::unit::Unit;
74
75    fn unit(version: &str) -> Unit {
76        let state: ReleaseUnitState = facet_json::from_str(
77            r#"{"target":{"ecosystem":"version-file","version_file":"VERSION"},"source":["."],"changelog":"CHANGELOG.md","dependencies":[],"pins":{},"tag_prefix":"v","kind":"library","application_bump":"none","revision_file":null}"#,
78        )
79        .unwrap();
80        Unit::configured("app", None, version.parse().unwrap(), &state)
81    }
82
83    #[test]
84    fn a_unit_at_0_0_0_without_tags_is_unversioned() {
85        assert_eq!(unit("0.0.0").status(&[]).unwrap(), UnitStatus::Unversioned);
86    }
87
88    #[test]
89    fn a_version_newer_than_every_tag_is_pending() {
90        assert_eq!(unit("1.0.0").status(&[]).unwrap(), UnitStatus::Pending);
91        assert_eq!(
92            unit("1.1.0").status(&["v1.0.0".into()]).unwrap(),
93            UnitStatus::Pending
94        );
95    }
96
97    #[test]
98    fn a_tag_covers_its_own_version_and_older_ones() {
99        let tags = ["v1.0.0".into(), "v0.9.0".into()];
100        assert_eq!(
101            unit("1.0.0").status(&tags).unwrap(),
102            UnitStatus::Released {
103                latest: "v1.0.0",
104                tagged: true,
105            }
106        );
107        assert_eq!(
108            unit("0.0.0").status(&tags).unwrap(),
109            UnitStatus::Released {
110                latest: "v1.0.0",
111                tagged: false,
112            }
113        );
114    }
115
116    #[test]
117    fn tags_that_name_no_version_are_an_error() {
118        assert!(matches!(
119            unit("1.0.0").status(&["vlatest".into()]),
120            Err(UnparsableTag { unit, tag }) if unit == "app" && tag == "vlatest"
121        ));
122    }
123}