dev_prune/commands/
update.rs1use std::cmp::Ordering;
21use std::time::Duration;
22
23use anyhow::{Context, Result};
24use chrono::Utc;
25
26use crate::config::Registry;
27use crate::constants;
28use crate::output;
29
30pub fn run(offline: bool) -> Result<()> {
31 output::print_header("dev-prune version & upgrade");
32
33 output::print_info(&format!("Installed version: v{}", constants::VERSION));
34
35 if offline {
36 output::print_info("Skipping the release check because `--offline` was passed.");
37 } else if let Ok(mut registry) = Registry::load() {
38 if registry.settings.update_check {
39 match refresh_latest(&mut registry) {
42 Ok(latest) => report_comparison(&latest),
43 Err(e) => output::print_warning(&format!(
46 "Could not reach the release API ({e}). The upgrade commands below still apply."
47 )),
48 }
49 let _ = registry.save();
50 } else {
51 output::print_info(
52 "The release check is off (`devp config set update_check true` re-enables it).",
53 );
54 }
55 }
56
57 println!();
58 println!(" Latest releases: {}", constants::RELEASES_URL);
59 println!();
60 print_upgrade_commands();
61
62 Ok(())
63}
64
65pub fn check_now(registry: &mut Registry) -> bool {
74 if !registry.settings.update_check {
75 return false;
76 }
77
78 match refresh_latest(registry) {
79 Ok(latest) => {
80 report_comparison(&latest);
81 if compare_versions(constants::VERSION, &latest) == Some(Ordering::Less) {
82 print_upgrade_commands();
83 }
84 }
85 Err(e) => output::print_info(&format!("Could not check for a newer release ({e}).")),
87 }
88 true
89}
90
91fn print_upgrade_commands() {
93 println!(" Upgrade with whichever channel you installed from:");
94 println!(" cargo binstall dev-prune --force");
95 println!(" cargo install dev-prune --force");
96 println!(" curl -fsSL https://devprune.vkrishna04.me/install.sh | sh");
97 println!(" iwr -useb https://devprune.vkrishna04.me/install.ps1 | iex");
98}
99
100pub fn notify_if_outdated(registry: &mut Registry) -> bool {
106 if !registry.settings.update_check {
107 return false;
108 }
109
110 let interval = registry.settings.update_check_interval_days;
111 let due = registry
112 .last_update_check
113 .is_none_or(|last| Utc::now().signed_duration_since(last).num_days() >= interval);
114
115 if due {
116 let _ = refresh_latest(registry);
120 }
121
122 if let Some(latest) = registry.latest_known_version.as_deref() {
123 if compare_versions(constants::VERSION, latest) == Some(Ordering::Less) {
124 output::print_info(&format!(
125 "dev-prune v{latest} is out (you have v{}). `devp update` has the commands; \
126 `devp config set update_check false` silences this.",
127 constants::VERSION
128 ));
129 }
130 }
131
132 due
133}
134
135fn refresh_latest(registry: &mut Registry) -> Result<String> {
140 let result = latest_release(registry.settings.update_check_timeout_secs);
141 registry.last_update_check = Some(Utc::now());
142 let latest = result?;
143 registry.latest_known_version = Some(latest.clone());
144 Ok(latest)
145}
146
147fn report_comparison(latest: &str) {
149 let installed = constants::VERSION;
150 match compare_versions(installed, latest) {
151 Some(Ordering::Less) => {
152 output::print_warning(&format!(
153 "Latest release: v{latest} — an upgrade is available."
154 ));
155 }
156 Some(Ordering::Equal) => {
157 output::print_success(&format!(
158 "Latest release: v{latest} — you are up to date."
159 ));
160 }
161 Some(Ordering::Greater) => {
162 output::print_info(&format!(
164 "Latest release: v{latest} — your build is newer than the last published one."
165 ));
166 }
167 None => {
168 output::print_info(&format!(
169 "Latest release: v{latest} (could not compare it to v{installed})."
170 ));
171 }
172 }
173}
174
175fn latest_release(timeout_secs: u64) -> Result<String> {
180 let body = ureq::get(constants::LATEST_RELEASE_API_URL)
181 .header("User-Agent", &format!("dev-prune/{}", constants::VERSION))
182 .header("Accept", "application/vnd.github+json")
183 .config()
184 .timeout_global(Some(Duration::from_secs(timeout_secs.max(1))))
185 .build()
186 .call()
187 .context("request failed")?
188 .body_mut()
189 .read_to_string()
190 .context("could not read the response")?;
191
192 let json: serde_json::Value =
193 serde_json::from_str(&body).context("the response was not JSON")?;
194 let tag = json
195 .get("tag_name")
196 .and_then(|v| v.as_str())
197 .context("the response carried no tag_name")?;
198
199 Ok(tag.trim_start_matches('v').to_string())
200}
201
202fn compare_versions(a: &str, b: &str) -> Option<Ordering> {
208 let parse = |v: &str| -> Option<[u64; 3]> {
209 let core = v.split(['-', '+']).next()?;
210 let mut parts = core.split('.');
211 let out = [
212 parts.next()?.parse().ok()?,
213 parts.next()?.parse().ok()?,
214 parts.next()?.parse().ok()?,
215 ];
216 if parts.next().is_some() {
218 return None;
219 }
220 Some(out)
221 };
222 Some(parse(a)?.cmp(&parse(b)?))
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use chrono::Duration as ChronoDuration;
229
230 #[test]
231 fn orders_by_component_not_lexically() {
232 assert_eq!(compare_versions("1.9.0", "1.10.0"), Some(Ordering::Less));
234 assert_eq!(compare_versions("1.0.0", "1.0.0"), Some(Ordering::Equal));
235 assert_eq!(
236 compare_versions("2.0.0", "1.99.99"),
237 Some(Ordering::Greater)
238 );
239 }
240
241 #[test]
242 fn pre_release_suffixes_compare_by_their_core() {
243 assert_eq!(
244 compare_versions("1.0.0", "1.0.0-rc.1"),
245 Some(Ordering::Equal)
246 );
247 assert_eq!(
248 compare_versions("1.0.0+build7", "1.0.1"),
249 Some(Ordering::Less)
250 );
251 }
252
253 #[test]
254 fn unparseable_versions_report_no_answer_rather_than_a_wrong_one() {
255 assert_eq!(compare_versions("1.0", "1.0.0"), None);
256 assert_eq!(compare_versions("1.0.0.1", "1.0.0"), None);
257 assert_eq!(compare_versions("nightly", "1.0.0"), None);
258 }
259
260 #[test]
261 fn the_check_is_on_unless_the_user_turns_it_off() {
262 assert!(Registry::default().settings.update_check);
263 }
264
265 #[test]
266 fn a_disabled_check_touches_neither_the_network_nor_the_registry() {
267 let mut registry = Registry::default();
268 registry.settings.update_check = false;
269 assert!(!notify_if_outdated(&mut registry));
270 assert!(registry.last_update_check.is_none());
271 }
272
273 #[test]
274 fn a_recent_check_is_not_repeated() {
275 let mut registry = Registry::default();
276 let stamp = Utc::now() - ChronoDuration::days(constants::UPDATE_CHECK_INTERVAL_DAYS - 1);
277 registry.last_update_check = Some(stamp);
278 assert!(!notify_if_outdated(&mut registry));
280 assert_eq!(registry.last_update_check, Some(stamp));
281 }
282}