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(¬ice.current_version),
98 display_version(¬ice.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(¬ice);
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}