Skip to main content

pray_core/
cli_release.rs

1use crate::registry::version_is_greater_than;
2use crate::PrayResult;
3use std::path::{Path, PathBuf};
4
5pub const DEFAULT_REPOSITORY: &str = "https://github.com/kiskolabs/pray";
6pub const DEFAULT_UPGRADE_COMMAND: &str = "pray upgrade";
7
8const VERSION_CHECK_CACHE_FILE: &str = "cli-version-check.json";
9const VERSION_CHECK_TTL_SECONDS: u64 = 86_400;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct UpgradeNotice {
13    pub current_version: String,
14    pub latest_version: String,
15    pub upgrade_command: String,
16    pub changelog_url: String,
17}
18
19pub fn normalize_release_version(tag: &str) -> String {
20    tag.trim().trim_start_matches(['v', 'V']).trim().to_string()
21}
22
23pub fn changelog_url(repository: &str, _latest_version: &str) -> String {
24    let repository = repository.trim_end_matches('/');
25    format!("{repository}/blob/main/CHANGELOG.md")
26}
27
28pub fn display_version(version: &str) -> String {
29    let normalized = normalize_release_version(version);
30    if normalized.is_empty() {
31        return "main".to_string();
32    }
33    format!("v{normalized}")
34}
35
36pub fn parse_github_latest_release_tag_name(body: &str) -> Option<String> {
37    let value: serde_json::Value = serde_json::from_str(body).ok()?;
38    value
39        .get("tag_name")
40        .and_then(|tag| tag.as_str())
41        .map(normalize_release_version)
42        .filter(|version| !version.is_empty())
43}
44
45pub fn parse_workspace_package_version(cargo_toml: &str) -> Option<String> {
46    let mut in_workspace_package = false;
47    for line in cargo_toml.lines() {
48        let trimmed = line.trim();
49        if trimmed.starts_with('[') && trimmed.ends_with(']') {
50            in_workspace_package = trimmed == "[workspace.package]";
51            continue;
52        }
53        if !in_workspace_package {
54            continue;
55        }
56        let Some((key, value)) = trimmed.split_once('=') else {
57            continue;
58        };
59        if key.trim() != "version" {
60            continue;
61        }
62        let version = value
63            .trim()
64            .trim_matches('"')
65            .trim_matches('\'')
66            .trim()
67            .to_string();
68        if version.is_empty() {
69            return None;
70        }
71        return Some(version);
72    }
73    None
74}
75
76pub fn upgrade_available(current_version: &str, latest_version: &str) -> PrayResult<bool> {
77    version_is_greater_than(latest_version, current_version)
78}
79
80pub fn build_upgrade_notice(
81    current_version: &str,
82    latest_version: &str,
83    repository: &str,
84    upgrade_command: &str,
85) -> UpgradeNotice {
86    UpgradeNotice {
87        current_version: current_version.to_string(),
88        latest_version: latest_version.to_string(),
89        upgrade_command: upgrade_command.to_string(),
90        changelog_url: changelog_url(repository, latest_version),
91    }
92}
93
94pub fn format_upgrade_notice(notice: &UpgradeNotice) -> String {
95    format!(
96        "A new version of pray is available\n  {} → {}\n  Run: {}\n  Changelog: {}",
97        display_version(&notice.current_version),
98        display_version(&notice.latest_version),
99        notice.upgrade_command,
100        notice.changelog_url
101    )
102}
103
104pub fn should_check_upgrade(command: &str, offline: bool) -> bool {
105    if offline {
106        return false;
107    }
108    !matches!(
109        command,
110        "upgrade" | "version" | "help" | "-h" | "--help" | "-V" | "--version"
111    )
112}
113
114pub fn version_check_cache_path(cache_root: &Path) -> PathBuf {
115    cache_root.join(VERSION_CHECK_CACHE_FILE)
116}
117
118pub fn version_check_ttl_seconds() -> u64 {
119    VERSION_CHECK_TTL_SECONDS
120}
121
122#[cfg(test)]
123mod tests {
124    use super::{
125        build_upgrade_notice, changelog_url, format_upgrade_notice, normalize_release_version,
126        parse_github_latest_release_tag_name, parse_workspace_package_version,
127        should_check_upgrade, upgrade_available, DEFAULT_REPOSITORY, DEFAULT_UPGRADE_COMMAND,
128    };
129
130    #[test]
131    fn normalizes_release_tags() {
132        assert_eq!(normalize_release_version("v1.2.3"), "1.2.3");
133        assert_eq!(normalize_release_version("1.2.3"), "1.2.3");
134    }
135
136    #[test]
137    fn parses_github_latest_release_tag() {
138        let body = r#"{"tag_name":"v1.2.0","name":"1.2.0"}"#;
139        assert_eq!(
140            parse_github_latest_release_tag_name(body),
141            Some("1.2.0".to_string())
142        );
143    }
144
145    #[test]
146    fn parses_workspace_package_version_from_cargo_toml() {
147        let cargo_toml = r#"
148[workspace.package]
149version = "1.2.0"
150edition = "2021"
151"#;
152        assert_eq!(
153            parse_workspace_package_version(cargo_toml),
154            Some("1.2.0".to_string())
155        );
156    }
157
158    #[test]
159    fn upgrade_notice_points_to_changelog() {
160        let notice = build_upgrade_notice(
161            "1.1.0",
162            "1.2.0",
163            DEFAULT_REPOSITORY,
164            DEFAULT_UPGRADE_COMMAND,
165        );
166        assert_eq!(
167            notice.changelog_url,
168            "https://github.com/kiskolabs/pray/blob/main/CHANGELOG.md"
169        );
170        let formatted = format_upgrade_notice(&notice);
171        assert!(formatted.contains("A new version of pray is available"));
172        assert!(formatted.contains("v1.1.0 → v1.2.0"));
173        assert!(formatted.contains("Run: pray upgrade"));
174        assert!(formatted
175            .contains("Changelog: https://github.com/kiskolabs/pray/blob/main/CHANGELOG.md"));
176    }
177
178    #[test]
179    fn detects_when_upgrade_is_available() {
180        assert!(upgrade_available("1.1.0", "1.2.0").expect("compare versions"));
181        assert!(!upgrade_available("1.2.0", "1.2.0").expect("compare versions"));
182    }
183
184    #[test]
185    fn skips_version_check_for_upgrade_and_version_commands() {
186        assert!(!should_check_upgrade("upgrade", false));
187        assert!(!should_check_upgrade("version", false));
188        assert!(should_check_upgrade("install", false));
189        assert!(!should_check_upgrade("install", true));
190    }
191
192    #[test]
193    fn changelog_url_points_to_main_branch() {
194        assert_eq!(
195            changelog_url(DEFAULT_REPOSITORY, "1.0.0"),
196            "https://github.com/kiskolabs/pray/blob/main/CHANGELOG.md"
197        );
198    }
199}