diff --git a/src/aqua/aqua_registry.rs b/src/aqua/aqua_registry.rs index ac4b3ceb18..c0c68d1c4d 100644 --- a/src/aqua/aqua_registry.rs +++ b/src/aqua/aqua_registry.rs @@ -2,13 +2,13 @@ use crate::backend::aqua; use crate::backend::aqua::{arch, os}; use crate::duration::{DAILY, WEEKLY}; use crate::git::{CloneOptions, Git}; +use crate::semver::split_version_prefix; use crate::{aqua::aqua_template, config::Settings}; use crate::{dirs, file, hashmap, http}; use expr::{Context, Program, Value}; use eyre::{ContextCompat, Result, eyre}; use indexmap::IndexSet; use itertools::Itertools; -use regex::Regex; use serde_derive::Deserialize; use std::collections::HashMap; use std::path::PathBuf; @@ -462,8 +462,8 @@ impl AquaPackage { } fn expr_parser(&self, v: &str) -> expr::Environment<'_> { - let prefix = Regex::new(r"^[^0-9.]+").unwrap(); - let ver = versions::Versioning::new(prefix.replace(v, "")); + let (_, v) = split_version_prefix(v); + let ver = versions::Versioning::new(v); let mut env = expr::Environment::new(); env.add_function("semver", move |c| { if c.args.len() != 1 { diff --git a/src/main.rs b/src/main.rs index 75272944ce..304839af59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,7 @@ mod redactions; mod registry; pub(crate) mod result; mod runtime_symlinks; +mod semver; mod shell; mod shims; mod shorthands; diff --git a/src/runtime_symlinks.rs b/src/runtime_symlinks.rs index 6067c183b1..7c56433bf2 100644 --- a/src/runtime_symlinks.rs +++ b/src/runtime_symlinks.rs @@ -5,12 +5,12 @@ use crate::backend::Backend; use crate::config::{Alias, Config}; use crate::file::make_symlink_or_file; use crate::plugins::VERSION_REGEX; +use crate::semver::split_version_prefix; use crate::{backend, file}; use eyre::Result; use indexmap::IndexMap; use itertools::Itertools; use versions::Versioning; -use xx::regex; pub async fn rebuild(config: &Config) -> Result<()> { for backend in backend::list() { @@ -39,14 +39,9 @@ fn list_symlinks(config: &Config, backend: Arc) -> IndexMap (String, String) { + version + .char_indices() + .find_map(|(i, c)| { + if c.is_ascii_digit() { + if i == 0 { + return Some(i); + } + // If the previous char is a delimiter or 'v', we found a split point. + let prev_char = version.chars().nth(i - 1).unwrap(); + if ['-', '_', '/', '.', 'v', 'V'].contains(&prev_char) { + return Some(i); + } + } + None + }) + .map_or_else( + || ("".into(), version.into()), + |i| { + let (prefix, version) = version.split_at(i); + (prefix.into(), version.into()) + }, + ) +} + +/// split a version number into chunks +/// given v: "1.2-3a4" return ["1", ".2", "-3a4"] +pub fn chunkify_version(v: &str) -> Vec { + fn chunkify(m: &Mess, sep0: &str, chunks: &mut Vec) { + for (i, chunk) in m.chunks.iter().enumerate() { + let sep = if i == 0 { sep0 } else { "." }; + chunks.push(format!("{sep}{chunk}")); + } + if let Some((next_sep, next_mess)) = &m.next { + chunkify(next_mess, next_sep.to_string().as_ref(), chunks) + } + } + + let mut chunks = vec![]; + // don't parse "latest", otherwise bump from latest to any version would have one chunk only + if v != "latest" { + if let Some(v) = Versioning::new(v) { + let m = match v { + Versioning::Ideal(sem_ver) => sem_ver.to_mess(), + Versioning::General(version) => version.to_mess(), + Versioning::Complex(mess) => mess, + }; + chunkify(&m, "", &mut chunks); + } + } + chunks +} + +#[cfg(test)] +mod tests { + use super::{chunkify_version, split_version_prefix}; + + #[test] + fn test_split_version_prefix() { + assert_eq!(split_version_prefix("latest"), ("".into(), "latest".into())); + assert_eq!(split_version_prefix("v1.2.3"), ("v".into(), "1.2.3".into())); + assert_eq!( + split_version_prefix("mountpoint-s3-v1.2.3-5_beta.5"), + ("mountpoint-s3-v".into(), "1.2.3-5_beta.5".into()) + ); + assert_eq!( + split_version_prefix("cli/1.2.3"), + ("cli/".into(), "1.2.3".into()) + ); + assert_eq!( + split_version_prefix("temurin-17.0.7+7"), + ("temurin-".into(), "17.0.7+7".into()) + ); + assert_eq!(split_version_prefix("1.2"), ("".into(), "1.2".into())); + assert_eq!( + split_version_prefix("2:1.2.1"), + ("".into(), "2:1.2.1".into()) + ); + assert_eq!( + split_version_prefix("2025-05-17"), + ("".into(), "2025-05-17".into()) + ); + } + + #[test] + fn test_chunkify_version() { + assert_eq!(chunkify_version("1.2-3a4"), vec!["1", ".2", "-3a4"]); + assert_eq!(chunkify_version("latest"), Vec::::new()); + assert_eq!(chunkify_version("1.0.0"), vec!["1", ".0", ".0"]); + assert_eq!( + chunkify_version("2.3.4-beta"), + vec!["2", ".3", ".4", "-beta"] + ); + } +} diff --git a/src/toolset/outdated_info.rs b/src/toolset/outdated_info.rs index eeee934eb3..e5ca47820a 100644 --- a/src/toolset/outdated_info.rs +++ b/src/toolset/outdated_info.rs @@ -1,3 +1,4 @@ +use crate::semver::{chunkify_version, split_version_prefix}; use crate::toolset; use crate::toolset::{ToolRequest, ToolSource, ToolVersion}; use crate::{Result, config::Config}; @@ -7,7 +8,7 @@ use std::{ sync::Arc, }; use tabled::Tabled; -use versions::{Mess, Version, Versioning}; +use versions::Version; #[derive(Debug, Serialize, Clone, Tabled)] pub struct OutdatedInfo { @@ -55,11 +56,10 @@ impl OutdatedInfo { ) -> eyre::Result> { let t = tv.backend()?; // prefix is something like "temurin-" or "corretto-" - let prefix = xx::regex!(r"^[a-zA-Z-]+-") - .find(&tv.request.version()) - .map(|m| m.as_str().to_string()); + let (prefix, _) = split_version_prefix(&tv.request.version()); let latest_result = if bump { - t.latest_version(config, prefix.clone()).await + t.latest_version(config, Some(prefix.clone()).filter(|s| !s.is_empty())) + .await } else { tv.latest_version(config).await.map(Option::from) }; @@ -84,7 +84,6 @@ impl OutdatedInfo { return Ok(None); } if bump { - let prefix = prefix.unwrap_or_default(); let old = oi.tool_version.request.version(); let old = old.strip_prefix(&prefix).unwrap_or_default(); let new = oi.latest.strip_prefix(&prefix).unwrap_or_default(); @@ -198,34 +197,6 @@ fn check_semver_bump(old: &str, new: &str) -> Option { } } -/// split a version number into chunks -/// given v: "1.2-3a4" return ["1", ".2", "-3", "a4"] -fn chunkify_version(v: &str) -> Vec { - fn chunkify(m: &Mess, sep0: &str, chunks: &mut Vec) { - for (i, chunk) in m.chunks.iter().enumerate() { - let sep = if i == 0 { sep0 } else { "." }; - chunks.push(format!("{sep}{chunk}")); - } - if let Some((next_sep, next_mess)) = &m.next { - chunkify(next_mess, next_sep.to_string().as_ref(), chunks) - } - } - - let mut chunks = vec![]; - // don't parse "latest", otherwise bump from latest to any version would have one chunk only - if v != "latest" { - if let Some(v) = Versioning::new(v) { - let m = match v { - Versioning::Ideal(sem_ver) => sem_ver.to_mess(), - Versioning::General(version) => version.to_mess(), - Versioning::Complex(mess) => mess, - }; - chunkify(&m, "", &mut chunks); - } - } - chunks -} - pub fn is_outdated_version(current: &str, latest: &str) -> bool { if let (Some(c), Some(l)) = (Version::new(current), Version::new(latest)) { c.lt(&l)