use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::path::Path;
use crate::{config::Config, lock, prompt, shell};
pub fn run(config: &Config, force: bool) -> Result<()> {
let versions_dir = config.versions_dir();
let version_count = count_versions(&versions_dir);
let data_mb = dir_size_mb(&config.root);
let exe_path = std::env::current_exe().context("Cannot determine gvsn binary location")?;
let sh = shell::detect();
let profile_path = sh.as_deref().and_then(|s| s.profile_path());
println!();
println!(
"{} This will permanently remove:",
"gvsn implode".red().bold()
);
println!();
println!(
" {} {} ({} version{}, {:.1} MB)",
"->".cyan(),
config.root.display(),
version_count,
if version_count == 1 { "" } else { "s" },
data_mb,
);
println!(" {} {}", "->".cyan(), exe_path.display());
if let Some(ref p) = profile_path {
println!(" {} {} (gvsn lines removed)", "->".cyan(), p.display());
}
println!();
if !force {
let message = format!(" Type {} to confirm: ", "yes".bold());
if !prompt::confirm(&message)? {
bail!("Aborted.");
}
println!();
}
if config.root.exists() {
let lock_path = config.root.join(".lock");
lock::with_lock(&lock_path, || Ok(std::fs::remove_dir_all(&config.root)?))
.with_context(|| format!("Failed to remove {}", config.root.display()))?;
println!(" {} Removed {}", "✓".green(), config.root.display());
}
if let Some(ref profile) = profile_path {
match shell::strip_profile(profile) {
Ok(true) => println!(" {} Cleaned {}", "✓".green(), profile.display()),
Ok(false) => {}
Err(e) => eprintln!(
" {} Could not clean {}: {e}",
"!".yellow(),
profile.display()
),
}
}
if let Some(ref sh) = sh {
if let Some(login_profile) = sh.login_profile_path() {
match shell::strip_profile(&login_profile) {
Ok(true) => println!(" {} Cleaned {}", "✓".green(), login_profile.display()),
Ok(false) => {}
Err(e) => eprintln!(
" {} Could not clean {}: {e}",
"!".yellow(),
login_profile.display()
),
}
}
}
remove_binary(&exe_path)?;
println!(" {} Removed {}", "✓".green(), exe_path.display());
println!();
println!("{}", "gvsn has been completely removed.".green().bold());
println!(" Open a new terminal to apply the profile changes.");
println!();
Ok(())
}
fn count_versions(versions_dir: &Path) -> usize {
if !versions_dir.exists() {
return 0;
}
std::fs::read_dir(versions_dir)
.map(|rd| rd.filter_map(|e| e.ok()).count())
.unwrap_or(0)
}
fn dir_size_mb(root: &Path) -> f64 {
fn walk(dir: &Path) -> u64 {
let Ok(rd) = std::fs::read_dir(dir) else {
return 0;
};
rd.filter_map(|e| e.ok())
.fold(0u64, |acc, entry| match entry.metadata() {
Ok(m) if m.is_dir() => acc + walk(&entry.path()),
Ok(m) => acc + m.len(),
Err(_) => acc,
})
}
if !root.exists() {
return 0.0;
}
walk(root) as f64 / (1024.0 * 1024.0)
}
fn remove_binary(exe: &Path) -> Result<()> {
#[cfg(windows)]
{
let old = exe.with_file_name("gvsn.exe.old");
std::fs::rename(exe, &old).with_context(|| format!("Cannot rename {}", exe.display()))?;
let _ = std::fs::remove_file(&old); }
#[cfg(not(windows))]
{
std::fs::remove_file(exe).with_context(|| format!("Cannot remove {}", exe.display()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn count_versions_returns_zero_when_dir_missing() {
let dir = tempdir().unwrap();
let versions = dir.path().join("versions");
assert_eq!(count_versions(&versions), 0);
}
#[test]
fn count_versions_counts_entries() {
let dir = tempdir().unwrap();
let versions = dir.path().join("versions");
std::fs::create_dir_all(versions.join("go1.22.4")).unwrap();
std::fs::create_dir_all(versions.join("go1.21.0")).unwrap();
assert_eq!(count_versions(&versions), 2);
}
#[test]
fn dir_size_mb_returns_zero_when_root_missing() {
let dir = tempdir().unwrap();
let root = dir.path().join("nope");
assert_eq!(dir_size_mb(&root), 0.0);
}
#[test]
fn dir_size_mb_sums_nested_file_sizes() {
let dir = tempdir().unwrap();
let root = dir.path().join("root");
std::fs::create_dir_all(root.join("nested")).unwrap();
std::fs::write(root.join("a.bin"), vec![0u8; 1024 * 1024]).unwrap(); std::fs::write(root.join("nested").join("b.bin"), vec![0u8; 512 * 1024]).unwrap();
let size = dir_size_mb(&root);
assert!((size - 1.5).abs() < 0.01, "expected ~1.5 MB, got {size}");
}
#[test]
fn remove_binary_removes_the_file() {
let dir = tempdir().unwrap();
let exe = dir.path().join("gvsn-fake-binary");
std::fs::write(&exe, b"not a real binary").unwrap();
remove_binary(&exe).unwrap();
#[cfg(not(windows))]
assert!(!exe.exists());
#[cfg(windows)]
{
assert!(!exe.exists());
}
}
#[test]
fn remove_binary_errors_when_file_missing() {
let dir = tempdir().unwrap();
let exe = dir.path().join("does-not-exist");
assert!(remove_binary(&exe).is_err());
}
}