use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::commands::upgrade_details::{UpgradeDetail, UpgradeDetails, UpgradeDetailsManager};
use crate::runner::Runner;
#[derive(Debug, Deserialize, Serialize)]
struct MiseToolVersion {
name: String,
version: String,
}
fn get_mise_versions_json(runner: &dyn Runner, tmpdir: &Path) -> Result<Vec<MiseToolVersion>> {
match get_mise_versions_from_ls(runner, tmpdir) {
Ok(versions) => {
if !versions.is_empty() {
return Ok(versions);
}
}
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("mise_errors.log")) {
let _ = writeln!(file, "mise ls --current method failed: {}", e);
}
}
}
match get_mise_versions_from_ls_simple(runner, tmpdir) {
Ok(versions) => Ok(versions),
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("mise_errors.log")) {
let _ = writeln!(file, "All methods failed: {}", e);
}
Ok(Vec::new())
}
}
}
fn get_mise_versions_from_ls(runner: &dyn Runner, tmpdir: &Path) -> Result<Vec<MiseToolVersion>> {
let (_, versions_output) = runner.run(
"mise ls --current",
&tmpdir.join("mise_versions.log"),
false,
)?;
if versions_output.trim().is_empty() {
return Err(anyhow::anyhow!("Empty output from mise ls --current"));
}
let versions = parse_mise_versions(&versions_output);
let mut tool_versions = Vec::new();
for (name, version) in versions {
if !name.is_empty() && !version.is_empty() && version.contains('.') {
tool_versions.push(MiseToolVersion { name, version });
}
}
let json_file = tmpdir.join("mise_versions.json");
if let Ok(mut file) = File::create(&json_file) {
let _ = writeln!(file, "{}", serde_json::to_string_pretty(&tool_versions)?);
}
Ok(tool_versions)
}
fn get_mise_versions_from_ls_simple(
runner: &dyn Runner,
tmpdir: &Path,
) -> Result<Vec<MiseToolVersion>> {
let (_, versions_output) =
runner.run("mise ls", &tmpdir.join("mise_versions_simple.log"), false)?;
if versions_output.trim().is_empty() {
return Err(anyhow::anyhow!("Empty output from mise ls"));
}
let versions = parse_mise_versions_simple(&versions_output);
let mut tool_versions = Vec::new();
for (name, version) in versions {
if !name.is_empty() && !version.is_empty() && version.contains('.') {
tool_versions.push(MiseToolVersion { name, version });
}
}
let json_file = tmpdir.join("mise_versions_simple.json");
if let Ok(mut file) = File::create(&json_file) {
let _ = writeln!(file, "{}", serde_json::to_string_pretty(&tool_versions)?);
}
Ok(tool_versions)
}
fn parse_mise_versions(output: &str) -> HashMap<String, String> {
let mut versions = HashMap::new();
if output.trim().starts_with('{') {
return versions;
}
for line in output.lines() {
let line = line.trim();
if line.is_empty()
|| line.starts_with('{')
|| line.starts_with('}')
|| line.starts_with('"')
{
continue;
}
if let Some((name, version)) = line.split_once('@') {
let name = name.trim().to_string();
let version = version
.split_whitespace()
.next()
.unwrap_or("")
.trim()
.to_string();
if !version.is_empty() {
versions.insert(name, version);
}
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let name = parts[0].to_string();
let version = parts[1].to_string();
if version.contains(|c: char| c.is_numeric()) {
versions.insert(name, version);
}
}
}
versions
}
fn parse_mise_versions_simple(output: &str) -> HashMap<String, String> {
let mut versions = HashMap::new();
for line in output.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('{') || line.starts_with('}') {
continue;
}
if let Some((name, version)) = line.split_once('@') {
let name = name.trim().to_string();
let version = version
.split_whitespace()
.next()
.unwrap_or("")
.trim()
.to_string();
if !version.is_empty() && version.contains('.') {
versions.insert(name, version);
}
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let name = parts[0].to_string();
let version = parts[1].to_string();
if version.contains(|c: char| c.is_numeric()) && version.contains('.') {
versions.insert(name, version);
}
}
}
versions
}
pub fn mise_up(
runner: &dyn Runner,
tmpdir: &Path,
verbose: bool,
) -> Result<(String, i32, PathBuf)> {
let logfile = tmpdir.join("mise_up.log");
let versions_before = get_mise_versions_json(runner, tmpdir)?;
let (rc, out) = runner.run("mise up", &logfile, verbose)?;
let outl = out.to_lowercase();
let install_markers = ["install", "installed", "upgraded", "updated", "->", "→"];
let has_updates = install_markers.iter().any(|k| outl.contains(k));
let mut upgrade_details = Vec::new();
if has_updates {
let versions_after = get_mise_versions_json(runner, tmpdir)?;
for before_tool in &versions_before {
if let Some(after_tool) = versions_after
.iter()
.find(|tool| tool.name == before_tool.name)
{
if before_tool.version != after_tool.version {
upgrade_details.push(UpgradeDetail::version_upgrade(
before_tool.name.clone(),
before_tool.version.clone(),
after_tool.version.clone(),
));
}
}
}
for after_tool in &versions_after {
if !versions_before
.iter()
.any(|tool| tool.name == after_tool.name)
{
upgrade_details.push(UpgradeDetail::new_installation(
after_tool.name.clone(),
after_tool.version.clone(),
));
}
}
}
let mut details = UpgradeDetails::new("Mise".to_string());
details.add_details(upgrade_details);
if details.has_upgrades() {
let _ = UpgradeDetailsManager::save_upgrade_details(&details, tmpdir, "mise");
}
let state = if has_updates {
if rc == 0 {
"changed"
} else {
"failed"
}
} else {
"unchanged"
};
Ok((state.to_string(), rc, logfile))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mise_versions_tool_at_version() {
let output = "node@20.11.0\npython@3.11.5";
let versions = parse_mise_versions(output);
assert_eq!(versions.get("node"), Some(&"20.11.0".to_string()));
assert_eq!(versions.get("python"), Some(&"3.11.5".to_string()));
}
#[test]
fn test_parse_mise_versions_space_separated() {
let output = "node 20.11.0 ~/.tool-versions\npython 3.11.5 ~/.tool-versions";
let versions = parse_mise_versions(output);
assert_eq!(versions.get("node"), Some(&"20.11.0".to_string()));
assert_eq!(versions.get("python"), Some(&"3.11.5".to_string()));
}
#[test]
fn test_parse_mise_versions_empty() {
let output = "";
let versions = parse_mise_versions(output);
assert!(versions.is_empty());
}
#[test]
fn test_parse_mise_versions_json_skipped() {
let output = r#"{"node": "20.11.0"}"#;
let versions = parse_mise_versions(output);
assert!(versions.is_empty());
}
#[test]
fn test_parse_mise_versions_mixed_format() {
let output = "node@20.11.0\npython 3.11.5 ~/.tool-versions";
let versions = parse_mise_versions(output);
assert_eq!(versions.len(), 2);
assert_eq!(versions.get("node"), Some(&"20.11.0".to_string()));
assert_eq!(versions.get("python"), Some(&"3.11.5".to_string()));
}
#[test]
fn test_parse_mise_versions_invalid_lines() {
let output = "\n \n{}\n\"\"\nnodejs 20.11.0";
let versions = parse_mise_versions(output);
assert!(versions.len() <= 1);
if !versions.is_empty() {
assert_eq!(versions.get("nodejs"), Some(&"20.11.0".to_string()));
}
}
}