use std::cmp::Ordering;
use std::time::Duration;
use anyhow::{Context, Result};
use chrono::Utc;
use crate::config::Registry;
use crate::constants;
use crate::output;
pub fn run(offline: bool) -> Result<()> {
output::print_header("dev-prune version & upgrade");
output::print_info(&format!("Installed version: v{}", constants::VERSION));
if offline {
output::print_info("Skipping the release check because `--offline` was passed.");
} else if let Ok(mut registry) = Registry::load() {
if registry.settings.update_check {
match refresh_latest(&mut registry) {
Ok(latest) => report_comparison(&latest),
Err(e) => output::print_warning(&format!(
"Could not reach the release API ({e}). The upgrade commands below still apply."
)),
}
let _ = registry.save();
} else {
output::print_info(
"The release check is off (`devp config set update_check true` re-enables it).",
);
}
}
println!();
println!(" Latest releases: {}", constants::RELEASES_URL);
println!();
print_upgrade_commands();
Ok(())
}
pub fn check_now(registry: &mut Registry) -> bool {
if !registry.settings.update_check {
return false;
}
match refresh_latest(registry) {
Ok(latest) => {
report_comparison(&latest);
if compare_versions(constants::VERSION, &latest) == Some(Ordering::Less) {
print_upgrade_commands();
}
}
Err(e) => output::print_info(&format!("Could not check for a newer release ({e}).")),
}
true
}
fn print_upgrade_commands() {
println!(" Upgrade with whichever channel you installed from:");
println!(" cargo binstall dev-prune --force");
println!(" cargo install dev-prune --force");
println!(" npm install -g dev-prune@latest");
println!(" curl -fsSL https://devprune.vkrishna04.me/install.sh | sh");
println!(" iwr -useb https://devprune.vkrishna04.me/install.ps1 | iex");
}
pub fn notify_if_outdated(registry: &mut Registry) -> bool {
if !registry.settings.update_check {
return false;
}
let interval = registry.settings.update_check_interval_days;
let due = registry
.last_update_check
.is_none_or(|last| Utc::now().signed_duration_since(last).num_days() >= interval);
if due {
let _ = refresh_latest(registry);
}
if let Some(latest) = registry.latest_known_version.as_deref() {
if compare_versions(constants::VERSION, latest) == Some(Ordering::Less) {
output::print_info(&format!(
"dev-prune v{latest} is out (you have v{}). `devp update` has the commands; \
`devp config set update_check false` silences this.",
constants::VERSION
));
}
}
due
}
fn refresh_latest(registry: &mut Registry) -> Result<String> {
let result = latest_release(registry.settings.update_check_timeout_secs);
registry.last_update_check = Some(Utc::now());
let latest = result?;
registry.latest_known_version = Some(latest.clone());
Ok(latest)
}
fn report_comparison(latest: &str) {
let installed = constants::VERSION;
match compare_versions(installed, latest) {
Some(Ordering::Less) => {
output::print_warning(&format!(
"Latest release: v{latest} — an upgrade is available."
));
}
Some(Ordering::Equal) => {
output::print_success(&format!(
"Latest release: v{latest} — you are up to date."
));
}
Some(Ordering::Greater) => {
output::print_info(&format!(
"Latest release: v{latest} — your build is newer than the last published one."
));
}
None => {
output::print_info(&format!(
"Latest release: v{latest} (could not compare it to v{installed})."
));
}
}
}
fn latest_release(timeout_secs: u64) -> Result<String> {
let body = ureq::get(constants::LATEST_RELEASE_API_URL)
.header("User-Agent", &format!("dev-prune/{}", constants::VERSION))
.header("Accept", "application/vnd.github+json")
.config()
.timeout_global(Some(Duration::from_secs(timeout_secs.max(1))))
.build()
.call()
.context("request failed")?
.body_mut()
.read_to_string()
.context("could not read the response")?;
let json: serde_json::Value =
serde_json::from_str(&body).context("the response was not JSON")?;
let tag = json
.get("tag_name")
.and_then(|v| v.as_str())
.context("the response carried no tag_name")?;
Ok(tag.trim_start_matches('v').to_string())
}
fn compare_versions(a: &str, b: &str) -> Option<Ordering> {
let parse = |v: &str| -> Option<[u64; 3]> {
let core = v.split(['-', '+']).next()?;
let mut parts = core.split('.');
let out = [
parts.next()?.parse().ok()?,
parts.next()?.parse().ok()?,
parts.next()?.parse().ok()?,
];
if parts.next().is_some() {
return None;
}
Some(out)
};
Some(parse(a)?.cmp(&parse(b)?))
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration as ChronoDuration;
#[test]
fn orders_by_component_not_lexically() {
assert_eq!(compare_versions("1.9.0", "1.10.0"), Some(Ordering::Less));
assert_eq!(compare_versions("1.0.0", "1.0.0"), Some(Ordering::Equal));
assert_eq!(
compare_versions("2.0.0", "1.99.99"),
Some(Ordering::Greater)
);
}
#[test]
fn pre_release_suffixes_compare_by_their_core() {
assert_eq!(
compare_versions("1.0.0", "1.0.0-rc.1"),
Some(Ordering::Equal)
);
assert_eq!(
compare_versions("1.0.0+build7", "1.0.1"),
Some(Ordering::Less)
);
}
#[test]
fn unparseable_versions_report_no_answer_rather_than_a_wrong_one() {
assert_eq!(compare_versions("1.0", "1.0.0"), None);
assert_eq!(compare_versions("1.0.0.1", "1.0.0"), None);
assert_eq!(compare_versions("nightly", "1.0.0"), None);
}
#[test]
fn the_check_is_on_unless_the_user_turns_it_off() {
assert!(Registry::default().settings.update_check);
}
#[test]
fn a_disabled_check_touches_neither_the_network_nor_the_registry() {
let mut registry = Registry::default();
registry.settings.update_check = false;
assert!(!notify_if_outdated(&mut registry));
assert!(registry.last_update_check.is_none());
}
#[test]
fn a_recent_check_is_not_repeated() {
let mut registry = Registry::default();
let stamp = Utc::now() - ChronoDuration::days(constants::UPDATE_CHECK_INTERVAL_DAYS - 1);
registry.last_update_check = Some(stamp);
assert!(!notify_if_outdated(&mut registry));
assert_eq!(registry.last_update_check, Some(stamp));
}
}