diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62b3dc0..b833e77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: matrix: include: - os: macos-latest - file-name: nvm-rust + file-name: nvm - os: ubuntu-latest - file-name: nvm-rust + file-name: nvm - os: windows-latest - file-name: nvm-rust.exe + file-name: nvm.exe runs-on: ${{ matrix.os }} @@ -53,7 +53,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v3 with: - name: build-${{ matrix.os }} + name: nvm-${{ matrix.os }} path: target/release/${{ matrix.file-name }} test: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c975dfb..e87eb71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ on: - workflow_dispatch: + workflow_dispatch: push: tags: - v* @@ -54,13 +54,13 @@ jobs: matrix: include: - os: macos-latest - file-name: nvm-rust + file-name: nvm display-name: nvm-macos - os: ubuntu-latest - file-name: nvm-rust + file-name: nvm display-name: nvm-linux - os: windows-latest - file-name: nvm-rust.exe + file-name: nvm.exe display-name: nvm-win.exe runs-on: ${{ matrix.os }} diff --git a/Cargo.toml b/Cargo.toml index 1d353de..1cf232f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,10 @@ exclude = [ "test-data/", ] +[[bin]] +name = "nvm" +path = "src/main.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/src/node_version.rs b/src/node_version.rs index b721ad2..1146928 100644 --- a/src/node_version.rs +++ b/src/node_version.rs @@ -1,5 +1,6 @@ use std::{ borrow::Borrow, + cmp::Ordering, collections::HashMap, fs::{read_link, remove_dir_all}, path::PathBuf, @@ -16,6 +17,26 @@ pub trait NodeVersion { fn version(&self) -> &Version; } +impl PartialEq for dyn NodeVersion { + fn eq(&self, other: &Self) -> bool { + self.version().eq(other.version()) + } +} + +impl Eq for dyn NodeVersion {} + +impl PartialOrd for dyn NodeVersion { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.version().cmp(other.version())) + } +} + +impl Ord for dyn NodeVersion { + fn cmp(&self, other: &Self) -> Ordering { + self.version().cmp(other.version()) + } +} + pub fn is_version_range(value: &str) -> Result { Range::parse(value).context(value.to_string()) } @@ -27,8 +48,8 @@ pub fn filter_version_req(versions: Vec, version_range: &Rang .collect() } -pub fn get_latest_of_each_major<'p, V: NodeVersion>(versions: &'p [V]) -> HashMap { - let mut map: HashMap = HashMap::new(); +pub fn get_latest_of_each_major(versions: &[V]) -> Vec<&V> { + let mut map: HashMap = HashMap::new(); for version in versions.iter() { let entry = map.get_mut(&version.version().major); @@ -39,7 +60,7 @@ pub fn get_latest_of_each_major<'p, V: NodeVersion>(versions: &'p [V]) -> HashMa map.insert(version.version().major, version); } - map + map.values().cloned().collect() } /// Handles `vX.X.X` prefixes @@ -54,7 +75,7 @@ fn parse_version_str(version_str: &str) -> Result { Version::parse(clean_version).context(version_str.to_owned()) } -#[derive(Clone, Deserialize, Debug, Eq, PartialEq)] +#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)] #[serde(rename_all(deserialize = "snake_case"))] pub struct OnlineNodeVersion { #[serde()] @@ -77,11 +98,7 @@ impl OnlineNodeVersion { pub fn get_download_url(&self) -> Result { let file_name = self.get_file(); - let url = format!( - "https://nodejs.org/dist/v{}/{}", - self.version, - file_name - ); + let url = format!("https://nodejs.org/dist/v{}/{}", self.version, file_name); Url::parse(&url).context(format!("Could not create a valid download url. [{}]", url)) } @@ -172,7 +189,7 @@ impl InstalledNodeVersion { remove_dir_all(self.get_dir_path(config))?; println!("Uninstalled {}!", self.version()); - Result::Ok(()) + Ok(()) } /// Checks that all the required files are present in the installation dir @@ -193,7 +210,7 @@ impl InstalledNodeVersion { ); } - Result::Ok(()) + Ok(()) } // Static functions @@ -218,7 +235,7 @@ impl InstalledNodeVersion { let entry = entry.unwrap(); let result = parse_version_str(entry.file_name().to_string_lossy().as_ref()); - if let Result::Ok(version) = result { + if let Ok(version) = result { version_dirs.push(version); } } @@ -264,10 +281,11 @@ impl NodeVersion for InstalledNodeVersion { #[cfg(test)] mod tests { mod online_version { - use crate::node_version::OnlineNodeVersion; use anyhow::Result; use node_semver::Version; + use crate::node_version::OnlineNodeVersion; + #[test] fn can_parse_version_data() -> Result<()> { let expected = OnlineNodeVersion { @@ -342,7 +360,7 @@ mod tests { assert_eq!(expected, result); - Result::Ok(()) + Ok(()) } } } diff --git a/src/subcommand/list.rs b/src/subcommand/list.rs index fc5d7d4..e01976a 100644 --- a/src/subcommand/list.rs +++ b/src/subcommand/list.rs @@ -1,7 +1,6 @@ -use std::{collections::HashMap, ops::Deref}; - use anyhow::Result; use clap::{AppSettings, Parser}; +use itertools::Itertools; use node_semver::Range; use crate::{ @@ -11,24 +10,39 @@ use crate::{ Config, }; -enum VersionStatus { - Outdated(OnlineNodeVersion), +enum VersionStatus<'p> { Latest, - Unknown, + NotInstalled, + Outdated(&'p OnlineNodeVersion), } -fn emoji_from(status: &VersionStatus) -> char { - match status { - VersionStatus::Outdated(_) => '⏫', - _ => '✅', +impl<'p> VersionStatus<'p> { + fn from(versions: &[&T], latest: &'p OnlineNodeVersion) -> VersionStatus<'p> { + if versions.is_empty() { + VersionStatus::NotInstalled + } else if versions + .iter() + .all(|version| version.version() < latest.version()) + { + VersionStatus::Outdated(latest) + } else { + VersionStatus::Latest + } } -} -fn latest_version_string_from(status: &VersionStatus) -> String { - match status { - VersionStatus::Outdated(version) => format!("-> {}", version.to_string()), - VersionStatus::Latest => "".to_string(), - _ => "-> unknown".to_string(), + fn to_emoji(&self) -> char { + match self { + VersionStatus::Latest => '✅', + VersionStatus::NotInstalled => '〰', + VersionStatus::Outdated(_) => '⏫', + } + } + + fn to_version_string(&self) -> String { + match self { + VersionStatus::Outdated(version) => format!("-> {}", version.to_string()), + _ => "".to_string(), + } } } @@ -40,11 +54,8 @@ setting = AppSettings::ColoredHelp )] pub struct ListCommand { /// Only display installed versions - #[clap(short, long)] - pub installed: bool, - /// Only display available versions - #[clap(short, long, takes_value(false))] - pub online: bool, + #[clap(short, long, alias = "installed")] + pub local: bool, /// Filter by semantic versions. /// /// `12`, `^10.9`, `>=8.10`, `>=8, <9` @@ -61,33 +72,71 @@ impl Action for ListCommand { installed_versions = node_version::filter_version_req(installed_versions, filter); } - let mut latest_per_major: HashMap = HashMap::new(); + if options.local { + println!( + "{}", + installed_versions + .iter() + .map(|version| version.to_string()) + .join("\n") + ); + + return Ok(()); + } + + // Get available versions, extract only the latest for each major version + let mut latest_per_major = Vec::<&OnlineNodeVersion>::new(); let online_versions = OnlineNodeVersion::fetch_all()?; if !online_versions.is_empty() { latest_per_major = node_version::get_latest_of_each_major(&online_versions); + latest_per_major.sort(); + latest_per_major.reverse(); } - let lines: Vec = installed_versions + let majors_and_installed_versions: Vec<(&OnlineNodeVersion, Vec<&InstalledNodeVersion>)> = + latest_per_major + .into_iter() + .map(|latest| { + ( + latest, + installed_versions + .iter() + .filter(|installed| installed.version().major == latest.version().major) + .collect(), + ) + }) + .collect(); + + // Show the latest X major versions by default + // and show any older, installed versions as well + let mut versions_to_show = Vec::<(&OnlineNodeVersion, &Vec<&InstalledNodeVersion>)>::new(); + for (i, (latest, installed)) in majors_and_installed_versions.iter().enumerate() { + if i < 5 || !installed.is_empty() { + versions_to_show.push((latest, installed)); + } + } + + let output = versions_to_show .iter() - .map(|version| { - let version_status = match latest_per_major.get(&version.version().major) { - Some(latest) if latest.version().gt(version.version()) => { - VersionStatus::Outdated(latest.deref().clone()) - }, - Some(_) => VersionStatus::Latest, - None => VersionStatus::Unknown, + .map(|(online_version, installed_versions)| { + let version_status = VersionStatus::from(installed_versions, online_version); + + let version_to_show = if installed_versions.is_empty() { + online_version.to_string() + } else { + installed_versions[0].to_string() }; format!( "{} {} {}", - emoji_from(&version_status), - version.to_string(), - latest_version_string_from(&version_status) + &version_status.to_emoji(), + version_to_show, + &version_status.to_version_string(), ) }) - .collect(); + .join("\n"); - println!("{}", lines.join("\n")); - Result::Ok(()) + println!("{output}"); + Ok(()) } } diff --git a/tests/utils.rs b/tests/utils.rs index 37e0c27..cd0f488 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -29,7 +29,7 @@ fn integration_dir() -> TempDir { pub fn setup_integration_test() -> Result<(TempDir, Command)> { let temp_dir = integration_dir(); - let mut cmd = Command::cargo_bin("nvm-rust").expect("Could not create Command"); + let mut cmd = Command::cargo_bin("nvm").expect("Could not create Command"); cmd.args(&["--dir", &temp_dir.to_string_lossy()]); Result::Ok((temp_dir, cmd))