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!(" npm install -g dev-prune@latest");
97 println!(" curl -fsSL https://devprune.vkrishna04.me/install.sh | sh");
98 println!(" iwr -useb https://devprune.vkrishna04.me/install.ps1 | iex");
99}
100
101pub fn notify_if_outdated(registry: &mut Registry) -> bool {
107 if !registry.settings.update_check {
108 return false;
109 }
110
111 let interval = registry.settings.update_check_interval_days;
112 let due = registry
113 .last_update_check
114 .is_none_or(|last| Utc::now().signed_duration_since(last).num_days() >= interval);
115
116 if due {
117 let _ = refresh_latest(registry);
121 }
122
123 if let Some(latest) = registry.latest_known_version.as_deref() {
124 if compare_versions(constants::VERSION, latest) == Some(Ordering::Less) {
125 output::print_info(&format!(
126 "dev-prune v{latest} is out (you have v{}). `devp update` has the commands; \
127 `devp config set update_check false` silences this.",
128 constants::VERSION
129 ));
130 }
131 }
132
133 due
134}
135
136fn refresh_latest(registry: &mut Registry) -> Result<String> {
141 let result = latest_release(registry.settings.update_check_timeout_secs);
142 registry.last_update_check = Some(Utc::now());
143 let latest = result?;
144 registry.latest_known_version = Some(latest.clone());
145 Ok(latest)
146}
147
148fn report_comparison(latest: &str) {
150 let installed = constants::VERSION;
151 match compare_versions(installed, latest) {
152 Some(Ordering::Less) => {
153 output::print_warning(&format!(
154 "Latest release: v{latest} — an upgrade is available."
155 ));
156 }
157 Some(Ordering::Equal) => {
158 output::print_success(&format!(
159 "Latest release: v{latest} — you are up to date."
160 ));
161 }
162 Some(Ordering::Greater) => {
163 output::print_info(&format!(
165 "Latest release: v{latest} — your build is newer than the last published one."
166 ));
167 }
168 None => {
169 output::print_info(&format!(
170 "Latest release: v{latest} (could not compare it to v{installed})."
171 ));
172 }
173 }
174}
175
176fn latest_release(timeout_secs: u64) -> Result<String> {
181 let body = ureq::get(constants::LATEST_RELEASE_API_URL)
182 .header("User-Agent", &format!("dev-prune/{}", constants::VERSION))
183 .header("Accept", "application/vnd.github+json")
184 .config()
185 .timeout_global(Some(Duration::from_secs(timeout_secs.max(1))))
186 .build()
187 .call()
188 .context("request failed")?
189 .body_mut()
190 .read_to_string()
191 .context("could not read the response")?;
192
193 let json: serde_json::Value =
194 serde_json::from_str(&body).context("the response was not JSON")?;
195 let tag = json
196 .get("tag_name")
197 .and_then(|v| v.as_str())
198 .context("the response carried no tag_name")?;
199
200 Ok(tag.trim_start_matches('v').to_string())
201}
202
203fn compare_versions(a: &str, b: &str) -> Option<Ordering> {
209 let parse = |v: &str| -> Option<[u64; 3]> {
210 let core = v.split(['-', '+']).next()?;
211 let mut parts = core.split('.');
212 let out = [
213 parts.next()?.parse().ok()?,
214 parts.next()?.parse().ok()?,
215 parts.next()?.parse().ok()?,
216 ];
217 if parts.next().is_some() {
219 return None;
220 }
221 Some(out)
222 };
223 Some(parse(a)?.cmp(&parse(b)?))
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use chrono::Duration as ChronoDuration;
230
231 #[test]
232 fn orders_by_component_not_lexically() {
233 assert_eq!(compare_versions("1.9.0", "1.10.0"), Some(Ordering::Less));
235 assert_eq!(compare_versions("1.0.0", "1.0.0"), Some(Ordering::Equal));
236 assert_eq!(
237 compare_versions("2.0.0", "1.99.99"),
238 Some(Ordering::Greater)
239 );
240 }
241
242 #[test]
243 fn pre_release_suffixes_compare_by_their_core() {
244 assert_eq!(
245 compare_versions("1.0.0", "1.0.0-rc.1"),
246 Some(Ordering::Equal)
247 );
248 assert_eq!(
249 compare_versions("1.0.0+build7", "1.0.1"),
250 Some(Ordering::Less)
251 );
252 }
253
254 #[test]
255 fn unparseable_versions_report_no_answer_rather_than_a_wrong_one() {
256 assert_eq!(compare_versions("1.0", "1.0.0"), None);
257 assert_eq!(compare_versions("1.0.0.1", "1.0.0"), None);
258 assert_eq!(compare_versions("nightly", "1.0.0"), None);
259 }
260
261 #[test]
262 fn the_check_is_on_unless_the_user_turns_it_off() {
263 assert!(Registry::default().settings.update_check);
264 }
265
266 #[test]
267 fn a_disabled_check_touches_neither_the_network_nor_the_registry() {
268 let mut registry = Registry::default();
269 registry.settings.update_check = false;
270 assert!(!notify_if_outdated(&mut registry));
271 assert!(registry.last_update_check.is_none());
272 }
273
274 #[test]
275 fn a_recent_check_is_not_repeated() {
276 let mut registry = Registry::default();
277 let stamp = Utc::now() - ChronoDuration::days(constants::UPDATE_CHECK_INTERVAL_DAYS - 1);
278 registry.last_update_check = Some(stamp);
279 assert!(!notify_if_outdated(&mut registry));
281 assert_eq!(registry.last_update_check, Some(stamp));
282 }
283}