use anyhow::Result;
use colored::Colorize;
use serde::Serialize;
use crate::{
config::{Config, ConfigMut},
http::HttpClient,
remote::index,
toolchain,
};
#[derive(Serialize)]
struct OutdatedEntry {
installed: String,
latest: Option<String>,
status: OutdatedStatus,
patches_behind: Option<u32>,
}
#[derive(Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum OutdatedStatus {
UpToDate,
Behind,
Unknown,
}
pub fn run(config: &Config, client: &HttpClient, json: bool) -> Result<()> {
let installed = toolchain::list_installed(config)?;
if installed.is_empty() {
if json {
println!("[]");
} else {
println!("No Go versions installed. Run 'gvsn install latest'.");
}
return Ok(());
}
config.ensure_dirs()?;
if json {
eprintln!("{} Fetching available Go versions...", "->".cyan());
} else {
println!("{} Fetching available Go versions...", "->".cyan());
}
let releases = index::fetch_releases(client, &config.root)?;
let stable: Vec<_> = releases
.iter()
.filter(|r| r.stable)
.filter_map(|r| r.go_version())
.collect();
if json {
let entries: Vec<OutdatedEntry> = installed
.iter()
.map(|v| build_outdated_entry(v, &stable))
.collect();
println!("{}", serde_json::to_string(&entries)?);
return Ok(());
}
println!();
println!(
" {:<14} {:<14} {}",
"Installed".bold(),
"Latest patch".bold(),
"Status".bold()
);
println!(" {}", "─".repeat(50));
let mut any_outdated = false;
for v in &installed {
let entry = build_outdated_entry(v, &stable);
let latest_col = entry.latest.as_deref().unwrap_or("-");
match entry.status {
OutdatedStatus::Unknown => {
println!(
" {:<14} {:<14} {}",
entry.installed,
latest_col,
"? no data".dimmed()
);
}
OutdatedStatus::UpToDate => {
println!(
" {:<14} {:<14} {}",
entry.installed,
latest_col,
format!("{} up to date", "✓").green()
);
}
OutdatedStatus::Behind => {
let diff = entry.patches_behind.unwrap_or(0);
println!(
" {:<14} {:<14} {}",
entry.installed,
latest_col,
format!(
"âš {} patch{} behind",
diff,
if diff == 1 { "" } else { "es" }
)
.yellow()
);
any_outdated = true;
}
}
}
println!();
if any_outdated {
println!(
" Run {} to update a version, or {} to remove it.",
"gvsn install <version>".cyan(),
"gvsn uninstall <version>".cyan()
);
} else {
println!(" {} All installed versions are up to date.", "✓".green());
}
Ok(())
}
fn build_outdated_entry(
v: &crate::version::GoVersion,
stable: &[crate::version::GoVersion],
) -> OutdatedEntry {
let latest = stable
.iter()
.filter(|rv| rv.major == v.major && rv.minor == v.minor)
.max();
match latest {
None => OutdatedEntry {
installed: v.tag(),
latest: None,
status: OutdatedStatus::Unknown,
patches_behind: None,
},
Some(latest) if v >= latest => OutdatedEntry {
installed: v.tag(),
latest: Some(latest.tag()),
status: OutdatedStatus::UpToDate,
patches_behind: None,
},
Some(latest) => OutdatedEntry {
installed: v.tag(),
latest: Some(latest.tag()),
status: OutdatedStatus::Behind,
patches_behind: Some(latest.patch.saturating_sub(v.patch)),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::version::GoVersion;
use tempfile::tempdir;
fn v(s: &str) -> GoVersion {
GoVersion::parse(s).unwrap()
}
#[test]
fn run_short_circuits_before_any_network_call_when_nothing_installed() {
let dir = tempdir().unwrap();
let config = Config {
root: dir.path().to_path_buf(),
};
let client = HttpClient::new(false, 0).unwrap();
run(&config, &client, false).unwrap();
}
#[test]
fn run_json_short_circuits_before_any_network_call_when_nothing_installed() {
let dir = tempdir().unwrap();
let config = Config {
root: dir.path().to_path_buf(),
};
let client = HttpClient::new(false, 0).unwrap();
run(&config, &client, true).unwrap();
}
#[test]
fn run_creates_gvsn_root_before_fetching_when_a_version_is_installed() {
let dir = tempdir().unwrap();
let config = Config {
root: dir.path().join("not-created-yet"),
};
std::fs::create_dir_all(config.version_dir("go1.22.4")).unwrap();
let client = HttpClient::new(false, 0).unwrap();
let _ = run(&config, &client, false);
assert!(config.tmp_dir().exists());
}
#[test]
fn build_outdated_entry_reports_up_to_date() {
let entry = build_outdated_entry(&v("go1.23.4"), &[v("go1.23.4")]);
assert_eq!(entry.status, OutdatedStatus::UpToDate);
assert_eq!(entry.patches_behind, None);
}
#[test]
fn build_outdated_entry_reports_patches_behind() {
let entry = build_outdated_entry(&v("go1.22.4"), &[v("go1.22.12")]);
assert_eq!(entry.status, OutdatedStatus::Behind);
assert_eq!(entry.patches_behind, Some(8));
}
#[test]
fn build_outdated_entry_reports_unknown_when_no_matching_minor() {
let entry = build_outdated_entry(&v("go1.10.0"), &[v("go1.23.4")]);
assert_eq!(entry.status, OutdatedStatus::Unknown);
assert_eq!(entry.latest, None);
}
#[test]
fn version_ordering_used_for_latest() {
let mut versions = [v("go1.21.3"), v("go1.21.13"), v("go1.21.1")];
versions.sort();
assert_eq!(versions.last().unwrap(), &v("go1.21.13"));
}
#[test]
fn patch_diff_calculation() {
let installed = v("go1.21.0");
let latest = v("go1.21.13");
let diff = latest.patch.saturating_sub(installed.patch);
assert_eq!(diff, 13);
}
#[test]
fn up_to_date_detection() {
let installed = v("go1.23.4");
let latest = v("go1.23.4");
assert!(installed >= latest);
}
#[test]
fn outdated_detection() {
let installed = v("go1.22.4");
let latest = v("go1.22.12");
assert!(installed < latest);
}
#[test]
fn version_without_patch_compared_correctly() {
let installed = v("go1.22");
let latest = v("go1.22.12");
assert!(installed < latest);
let diff = latest.patch.saturating_sub(installed.patch);
assert_eq!(diff, 12);
}
}