Skip to main content

cli/
version.rs

1use std::sync::OnceLock;
2
3static SEMVER: OnceLock<String> = OnceLock::new();
4static DISPLAY_VERSION: OnceLock<String> = OnceLock::new();
5
6pub fn semver() -> &'static str {
7    SEMVER
8        .get_or_init(|| {
9            format_semver(
10                env!("CARGO_PKG_VERSION"),
11                option_env!("SHINE_VERSION_METADATA"),
12            )
13        })
14        .as_str()
15}
16
17pub fn display() -> &'static str {
18    DISPLAY_VERSION
19        .get_or_init(|| {
20            format_display(
21                semver(),
22                option_env!("SHINE_GIT_SHA"),
23                option_env!("SHINE_GIT_DATE"),
24            )
25        })
26        .as_str()
27}
28
29pub fn package() -> &'static str {
30    env!("CARGO_PKG_VERSION")
31}
32
33pub fn preview_commit() -> Option<&'static str> {
34    is_preview()
35        .then_some(option_env!("SHINE_GIT_SHA"))
36        .flatten()
37}
38
39fn is_preview() -> bool {
40    option_env!("SHINE_VERSION_METADATA").is_some_and(|metadata| {
41        let metadata = metadata.trim();
42        metadata == "preview" || metadata.starts_with("preview.")
43    })
44}
45
46fn format_semver(package_version: &str, metadata: Option<&str>) -> String {
47    match metadata
48        .map(str::trim)
49        .filter(|metadata| !metadata.is_empty())
50    {
51        Some("preview") => format_preview(package_version),
52        Some(metadata) if metadata.starts_with("preview.") => format_preview(package_version),
53        Some(metadata) => format!("{package_version}+{metadata}"),
54        None => package_version.to_string(),
55    }
56}
57
58fn format_preview(package_version: &str) -> String {
59    if package_version.contains('-') {
60        format!("{package_version}.preview")
61    } else {
62        format!("{package_version}-preview")
63    }
64}
65
66fn format_display(semver: &str, sha: Option<&str>, date: Option<&str>) -> String {
67    match (sha.and_then(trimmed), date.and_then(trimmed)) {
68        (Some(sha), Some(date)) => format!("{semver} ({sha} {date})"),
69        _ => semver.to_string(),
70    }
71}
72
73fn trimmed(value: &str) -> Option<&str> {
74    let value = value.trim();
75    (!value.is_empty()).then_some(value)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::{format_display, format_semver};
81
82    #[test]
83    fn format_version_without_metadata_returns_package_version() {
84        assert_eq!(format_semver("1.0.0", None), "1.0.0");
85        assert_eq!(format_semver("1.0.0", Some("")), "1.0.0");
86    }
87
88    #[test]
89    fn preview_uses_prerelease_label_without_duplicate_commit() {
90        assert_eq!(format_semver("1.0.0", Some("preview")), "1.0.0-preview");
91        assert_eq!(
92            format_semver("1.0.0", Some("preview.abc1234")),
93            "1.0.0-preview"
94        );
95        assert_eq!(
96            format_semver("2.0.0-rc.1", Some("preview")),
97            "2.0.0-rc.1.preview"
98        );
99    }
100
101    #[test]
102    fn display_matches_cargo_style_when_provenance_is_available() {
103        assert_eq!(
104            format_display("1.0.0", Some("30a34c682"), Some("2026-05-25")),
105            "1.0.0 (30a34c682 2026-05-25)"
106        );
107        assert_eq!(
108            format_display("1.0.0-preview", Some("30a34c682"), Some("2026-05-25")),
109            "1.0.0-preview (30a34c682 2026-05-25)"
110        );
111    }
112
113    #[test]
114    fn display_falls_back_to_semver_when_provenance_is_incomplete() {
115        assert_eq!(format_display("1.0.0", None, None), "1.0.0");
116        assert_eq!(format_display("1.0.0", Some("30a34c682"), None), "1.0.0");
117    }
118}