Skip to main content

ephemeris_release/
unit.rs

1//! The resolved unit of a release graph.
2
3mod status;
4
5use std::collections::BTreeMap;
6
7use ephemeris_files::RepositoryPath;
8
9pub(crate) use self::status::{UnitStatus, UnparsableTag};
10use crate::state::{Pin, ReleaseTarget, SourceDirectories};
11use crate::version::Version;
12
13/// One package in a release graph.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub(crate) struct Unit {
16    /// Release graph ID, the key in the configured units.
17    pub name: String,
18    /// Ecosystem package identifier, kept separate from the release graph ID.
19    pub package_name: Option<String>,
20    /// Version on the authoritative branch.
21    pub version: Version,
22    /// Where the version lives and how the unit is published.
23    pub target: ReleaseTarget,
24    /// Repository directories whose changes trigger a release.
25    pub source: SourceDirectories,
26    /// Repository path of the unit changelog, absent for a unit without one.
27    pub changelog: Option<RepositoryPath>,
28    /// Prefix used for the unit's Git tag and release.
29    pub tag_prefix: String,
30    /// Release graph IDs this unit is published after.
31    pub dependencies: Vec<String>,
32    /// Files holding the versions of dependencies, keyed by dependency.
33    pub pins: BTreeMap<String, Pin>,
34    /// Current value of the configured revision file, when one is set.
35    pub revision: Option<u64>,
36}
37
38impl Unit {
39    /// The title of this unit's forge release.
40    #[must_use]
41    pub(crate) fn title(&self) -> String {
42        self.revision.map_or_else(
43            || format!("{} {}", self.name, self.version),
44            |revision| format!("{} {}-{revision}", self.name, self.version),
45        )
46    }
47
48    /// The repository path of the file that holds this unit's version.
49    #[must_use]
50    pub(crate) const fn version_path(&self) -> &RepositoryPath {
51        match &self.target {
52            ReleaseTarget::Cargo {
53                manifest,
54                publish: _,
55            }
56            | ReleaseTarget::Npm {
57                manifest,
58                publish: _,
59            } => manifest,
60            ReleaseTarget::VersionFile { version_file } => version_file,
61            ReleaseTarget::Document { path, key: _ } => path,
62        }
63    }
64
65    /// The Git tag for this unit's current version: the configured prefix
66    /// plus version, with the revision appended when one is configured.
67    #[must_use]
68    pub(crate) fn tag(&self) -> String {
69        self.revision.map_or_else(
70            || format!("{}{}", self.tag_prefix, self.version),
71            |revision| format!("{}{}-{revision}", self.tag_prefix, self.version),
72        )
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use crate::state::ReleaseUnitState;
79    use crate::unit::Unit;
80    use crate::version::TagVersion;
81
82    fn unit() -> Unit {
83        let state: ReleaseUnitState = facet_json::from_str(
84            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}"#,
85        )
86        .unwrap();
87        Unit::configured("app", None, "1.0.0".parse().unwrap(), &state)
88    }
89
90    #[test]
91    fn tags_read_back_as_their_version_and_revision_unless_ambiguous() {
92        let versions = [
93            "0.1.0",
94            "1.2.3",
95            "1.0.0-rc.1",
96            "1.0.0-rc-1",
97            "1.0.0-3",
98            "1.0.0-3.1",
99            "1.0.0+build.7",
100            "1.0.0+build-7",
101            "1.0.0-alpha.2+build",
102        ];
103        for version in versions {
104            for revision in [None, Some(0), Some(1), Some(42)] {
105                for prefix in ["", "v", "app-v"] {
106                    let mut unit = unit();
107                    unit.version = version.parse().unwrap();
108                    unit.revision = revision;
109                    prefix.clone_into(&mut unit.tag_prefix);
110                    let tag = unit.tag();
111                    let round_trips = TagVersion::parse(&tag, prefix)
112                        .is_some_and(|parsed| parsed.names(&unit.version, revision));
113                    let plain = !version.contains(['-', '+']);
114                    let ambiguous = if revision.is_some() {
115                        !plain
116                    } else {
117                        version == "1.0.0-3"
118                    };
119                    assert_eq!(round_trips, !ambiguous, "{tag}");
120                }
121            }
122        }
123    }
124}