Browse Source

Add uninstall command (#292)

remotes/origin/add-with-shims
Gal Schlezinger 4 years ago committed by GitHub
parent
commit
be3843710d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/alias.rs
  2. 2
      src/choose_version_for_user_input.rs
  3. 57
      src/cli.rs
  4. 2
      src/commands/install.rs
  5. 17
      src/commands/ls_local.rs
  6. 1
      src/commands/mod.rs
  7. 100
      src/commands/uninstall.rs
  8. 6
      src/commands/use.rs
  9. 4
      src/installed_versions.rs
  10. 33
      src/user_version.rs
  11. 22
      src/version.rs
  12. 1
      tests/feature_tests/mod.rs
  13. 14
      tests/feature_tests/uninstall.rs
  14. 16
      tests/snapshots/e2e__feature_tests__uninstall__Bash.snap
  15. 13
      tests/snapshots/e2e__feature_tests__uninstall__Fish.snap
  16. 14
      tests/snapshots/e2e__feature_tests__uninstall__PowerShell.snap
  17. 14
      tests/snapshots/e2e__feature_tests__uninstall__Zsh.snap

4
src/alias.rs

@ -70,4 +70,8 @@ impl StoredAlias {
.to_str() .to_str()
.unwrap() .unwrap()
} }
pub fn path(&self) -> &std::path::Path {
&self.alias_path
}
} }

2
src/choose_version_for_user_input.rs

@ -50,7 +50,7 @@ pub fn choose_version_for_user_input<'a>(
version: Version::Alias(alias_name), version: Version::Alias(alias_name),
}) })
} else { } else {
let current_version = requested_version.to_version(&all_versions); let current_version = requested_version.to_version(&all_versions, &config);
current_version.map(|version| { current_version.map(|version| {
info!("Using Node {}", version.to_string().cyan()); info!("Using Node {}", version.to_string().cyan());
let path = config let path = config

57
src/cli.rs

@ -5,29 +5,59 @@ use structopt::StructOpt;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
pub enum SubCommand { pub enum SubCommand {
#[structopt(name = "ls-remote", about = "List all remote Node.js versions")] /// List all remote Node.js versions
#[structopt(name = "ls-remote")]
LsRemote(commands::ls_remote::LsRemote), LsRemote(commands::ls_remote::LsRemote),
#[structopt(name = "ls", about = "List all local Node.js versions")]
/// List all locally installed Node.js versions
#[structopt(name = "ls")]
LsLocal(commands::ls_local::LsLocal), LsLocal(commands::ls_local::LsLocal),
#[structopt(name = "install", about = "Install a new Node.js version")]
/// Install a new Node.js version
#[structopt(name = "install")]
Install(commands::install::Install), Install(commands::install::Install),
#[structopt(name = "use", about = "Change Node.js version")]
/// Change Node.js version
#[structopt(name = "use")]
Use(commands::r#use::Use), Use(commands::r#use::Use),
#[structopt(
name = "env", /// Print and set up required environment variables for fnm
about = "Print and setup required environment variables for fnm" #[structopt(name = "env")]
)]
Env(commands::env::Env), Env(commands::env::Env),
#[structopt(name = "completions", about = "Create completions file")]
/// Print shell completions to stdout
#[structopt(name = "completions")]
Completions(commands::completions::Completions), Completions(commands::completions::Completions),
#[structopt(name = "alias", about = "alias a version to a common name")]
/// Alias a version to a common name
#[structopt(name = "alias")]
Alias(commands::alias::Alias), Alias(commands::alias::Alias),
#[structopt(name = "default", about = "set a version as the default version")]
/// Set a version as the default version
///
/// This is a shorthand for `fnm alias VERSION default`
#[structopt(name = "default")]
Default(commands::default::Default), Default(commands::default::Default),
#[structopt(name = "current", about = "The current version")]
/// Print the current Node.js version
#[structopt(name = "current")]
Current(commands::current::Current), Current(commands::current::Current),
#[structopt(name = "exec", about = "Run a command with in fnm context")]
/// Run a command within fnm context
///
/// Example:
/// --------
/// fnm exec --using=v12.0.0 -- node --version
/// => v12.0.0
#[structopt(name = "exec")]
Exec(commands::exec::Exec), Exec(commands::exec::Exec),
/// Uninstall a Node.js version
///
/// > Warning: when providing an alias, it will remove the Node version the alias
/// is pointing to, along with the other aliases that point to the same version.
#[structopt(name = "uninstall")]
Uninstall(commands::uninstall::Uninstall),
} }
impl SubCommand { impl SubCommand {
@ -43,6 +73,7 @@ impl SubCommand {
Self::Default(cmd) => cmd.call(config), Self::Default(cmd) => cmd.call(config),
Self::Current(cmd) => cmd.call(config), Self::Current(cmd) => cmd.call(config),
Self::Exec(cmd) => cmd.call(config), Self::Exec(cmd) => cmd.call(config),
Self::Uninstall(cmd) => cmd.call(config),
} }
} }
} }

2
src/commands/install.rs

@ -82,7 +82,7 @@ impl super::command::Command for Install {
.collect(); .collect();
current_version current_version
.to_version(&available_versions) .to_version(&available_versions, &config)
.context(CantFindNodeVersion { .context(CantFindNodeVersion {
requested_version: current_version, requested_version: current_version,
})? })?

17
src/commands/ls_local.rs

@ -16,17 +16,8 @@ impl super::command::Command for LsLocal {
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let base_dir = config.installations_dir(); let base_dir = config.installations_dir();
let mut versions: Vec<_> = std::fs::read_dir(&base_dir) let mut versions =
.context(CantListLocallyInstalledVersion)? crate::installed_versions::list(base_dir).context(CantListLocallyInstalledVersion)?;
.filter_map(|x| {
if let Ok(version_dir) = x {
let file_name = version_dir.file_name();
file_name.to_str().and_then(|x| Version::parse(x).ok())
} else {
None
}
})
.collect();
versions.insert(0, Version::Bypassed); versions.insert(0, Version::Bypassed);
versions.sort(); versions.sort();
let aliases_hash = generate_aliases_hash(&config).context(CantReadAliases)?; let aliases_hash = generate_aliases_hash(&config).context(CantReadAliases)?;
@ -73,7 +64,9 @@ fn generate_aliases_hash(config: &FnmConfig) -> std::io::Result<HashMap<String,
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
pub enum Error { pub enum Error {
#[snafu(display("Can't list locally installed versions: {}", source))] #[snafu(display("Can't list locally installed versions: {}", source))]
CantListLocallyInstalledVersion { source: std::io::Error }, CantListLocallyInstalledVersion {
source: crate::installed_versions::Error,
},
#[snafu(display("Can't read aliases: {}", source))] #[snafu(display("Can't read aliases: {}", source))]
CantReadAliases { source: std::io::Error }, CantReadAliases { source: std::io::Error },
} }

1
src/commands/mod.rs

@ -8,4 +8,5 @@ pub mod exec;
pub mod install; pub mod install;
pub mod ls_local; pub mod ls_local;
pub mod ls_remote; pub mod ls_remote;
pub mod uninstall;
pub mod r#use; pub mod r#use;

100
src/commands/uninstall.rs

@ -0,0 +1,100 @@
use super::command::Command;
use crate::config::FnmConfig;
use crate::fs::remove_symlink_dir;
use crate::installed_versions;
use crate::outln;
use crate::user_version::UserVersion;
use crate::version::Version;
use crate::version_files::get_user_version_from_file;
use colored::Colorize;
use log::debug;
use snafu::{ensure, OptionExt, ResultExt, Snafu};
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
pub struct Uninstall {
version: Option<UserVersion>,
}
impl Command for Uninstall {
type Error = Error;
fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> {
let all_versions =
installed_versions::list(config.installations_dir()).context(VersionListingError)?;
let requested_version = self
.version
.or_else(|| {
let current_dir = std::env::current_dir().unwrap();
get_user_version_from_file(current_dir)
})
.context(CantInferVersion)?;
ensure!(
!matches!(requested_version, UserVersion::Full(Version::Bypassed)),
CantUninstallSystemVersion
);
let available_versions: Vec<&Version> = all_versions
.iter()
.filter(|v| requested_version.matches(v, &config))
.collect();
ensure!(
available_versions.len() < 2,
PleaseBeMoreSpecificToDelete {
matched_versions: available_versions
.iter()
.map(|x| x.v_str())
.collect::<Vec<_>>()
}
);
let version = requested_version
.to_version(&all_versions, &config)
.context(CantFindVersion)?;
let matching_aliases = version.find_aliases(&config).context(IoError)?;
let root_path = version
.root_path(&config)
.with_context(|| RootPathNotFound {
version: version.clone(),
})?;
debug!("Removing Node version from {:?}", root_path);
std::fs::remove_dir_all(root_path).context(CantDeleteNodeVersion)?;
outln!(config#Info, "Node version {} was removed successfuly", version.v_str().cyan());
for alias in matching_aliases {
debug!("Removing alias from {:?}", alias.path());
remove_symlink_dir(alias.path()).context(CantDeleteSymlink)?;
outln!(config#Info, "Alias {} was removed successfuly", alias.name().cyan());
}
Ok(())
}
}
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Can't get locally installed versions: {}", source))]
VersionListingError { source: installed_versions::Error },
#[snafu(display(
"Can't find version in dotfiles. Please provide a version manually to the command."
))]
CantInferVersion,
#[snafu(display("Can't uninstall system version"))]
CantUninstallSystemVersion,
#[snafu(display("Too many versions had matched, please be more specific.\nFound {} matching versions, expected 1:\n{}", matched_versions.len(), matched_versions.iter().map(|v| format!("* {}", v)).collect::<Vec<_>>().join("\n")))]
PleaseBeMoreSpecificToDelete { matched_versions: Vec<String> },
#[snafu(display("Can't find a matching version"))]
CantFindVersion,
#[snafu(display("Root path not found for version {}", version))]
RootPathNotFound { version: Version },
#[snafu(display("io error: {}", source))]
IoError { source: std::io::Error },
#[snafu(display("Can't delete Node.js version: {}", source))]
CantDeleteNodeVersion { source: std::io::Error },
#[snafu(display("Can't delete symlink: {}", source))]
CantDeleteSymlink { source: std::io::Error },
}

6
src/commands/use.rs

@ -50,7 +50,7 @@ impl Command for Use {
outln!(config#Info, "Using Node for alias {}", alias_name.cyan()); outln!(config#Info, "Using Node for alias {}", alias_name.cyan());
alias_path alias_path
} else { } else {
let current_version = requested_version.to_version(&all_versions); let current_version = requested_version.to_version(&all_versions, &config);
match current_version { match current_version {
Some(version) => { Some(version) => {
outln!(config#Info, "Using Node {}", version.to_string().cyan()); outln!(config#Info, "Using Node {}", version.to_string().cyan());
@ -123,9 +123,7 @@ pub enum Error {
#[snafu(display("{}", source))] #[snafu(display("{}", source))]
InstallError { source: <Install as Command>::Error }, InstallError { source: <Install as Command>::Error },
#[snafu(display("Can't get locally installed versions: {}", source))] #[snafu(display("Can't get locally installed versions: {}", source))]
VersionListingError { VersionListingError { source: installed_versions::Error },
source: crate::installed_versions::Error,
},
#[snafu(display("Requested version {} is not currently installed", version))] #[snafu(display("Requested version {} is not currently installed", version))]
CantFindVersion { version: UserVersion }, CantFindVersion { version: UserVersion },
#[snafu(display( #[snafu(display(

4
src/installed_versions.rs

@ -6,6 +6,10 @@ pub fn list<P: AsRef<Path>>(installations_dir: P) -> Result<Vec<Version>, Error>
let mut vec = vec![]; let mut vec = vec![];
for result_entry in installations_dir.as_ref().read_dir().context(IoError)? { for result_entry in installations_dir.as_ref().read_dir().context(IoError)? {
let entry = result_entry.context(IoError)?; let entry = result_entry.context(IoError)?;
if entry.file_name() == ".downloads" {
continue;
}
let path = entry.path(); let path = entry.path();
let filename = path let filename = path
.file_name() .file_name()

33
src/user_version.rs

@ -8,11 +8,18 @@ pub enum UserVersion {
} }
impl UserVersion { impl UserVersion {
pub fn to_version<'a, T>(&self, available_versions: T) -> Option<&'a Version> pub fn to_version<'a, T>(
&self,
available_versions: T,
config: &crate::config::FnmConfig,
) -> Option<&'a Version>
where where
T: IntoIterator<Item = &'a Version>, T: IntoIterator<Item = &'a Version>,
{ {
available_versions.into_iter().filter(|x| &self == x).max() available_versions
.into_iter()
.filter(|x| self.matches(x, config))
.max()
} }
pub fn alias_name(&self) -> Option<String> { pub fn alias_name(&self) -> Option<String> {
@ -21,13 +28,19 @@ impl UserVersion {
_ => None, _ => None,
} }
} }
}
impl PartialEq<Version> for UserVersion { pub fn matches(&self, version: &Version, config: &crate::config::FnmConfig) -> bool {
fn eq(&self, other: &Version) -> bool { match (self, version) {
match (self, other) {
(Self::Full(a), b) if a == b => true, (Self::Full(a), b) if a == b => true,
(Self::Full(_), _) => false, (Self::Full(user_version), maybe_alias) => {
match (user_version.alias_name(), maybe_alias.find_aliases(&config)) {
(None, _) | (_, Err(_)) => false,
(Some(user_alias), Ok(aliases)) => aliases
.iter()
.find(|alias| alias.name() == user_alias)
.is_some(),
}
}
(_, Version::Bypassed) => false, (_, Version::Bypassed) => false,
(_, Version::Lts(_)) => false, (_, Version::Lts(_)) => false,
(_, Version::Alias(_)) => false, (_, Version::Alias(_)) => false,
@ -84,7 +97,7 @@ mod tests {
expected.clone(), expected.clone(),
Version::parse("7.0.1").unwrap(), Version::parse("7.0.1").unwrap(),
]; ];
let result = UserVersion::OnlyMajor(6).to_version(&versions); let result = UserVersion::OnlyMajor(6).to_version(&versions, &Default::default());
assert_eq!(result, Some(&expected)); assert_eq!(result, Some(&expected));
} }
@ -98,7 +111,7 @@ mod tests {
expected.clone(), expected.clone(),
Version::parse("7.0.1").unwrap(), Version::parse("7.0.1").unwrap(),
]; ];
let result = UserVersion::MajorMinor(6, 0).to_version(&versions); let result = UserVersion::MajorMinor(6, 0).to_version(&versions, &Default::default());
assert_eq!(result, Some(&expected)); assert_eq!(result, Some(&expected));
} }
@ -112,7 +125,7 @@ mod tests {
Version::parse("6.0.1").unwrap(), Version::parse("6.0.1").unwrap(),
Version::parse("7.0.1").unwrap(), Version::parse("7.0.1").unwrap(),
]; ];
let result = UserVersion::Full(expected.clone()).to_version(&versions); let result = UserVersion::Full(expected.clone()).to_version(&versions, &Default::default());
assert_eq!(result, Some(&expected)); assert_eq!(result, Some(&expected));
} }

22
src/version.rs

@ -38,6 +38,17 @@ impl Version {
} }
} }
pub fn find_aliases(
&self,
config: &crate::config::FnmConfig,
) -> std::io::Result<Vec<crate::alias::StoredAlias>> {
let aliases = crate::alias::list_aliases(&config)?
.drain(..)
.filter(|alias| alias.s_ver() == self.v_str())
.collect();
Ok(aliases)
}
pub fn v_str(&self) -> String { pub fn v_str(&self) -> String {
format!("{}", self) format!("{}", self)
} }
@ -59,6 +70,17 @@ impl Version {
), ),
} }
} }
pub fn root_path(&self, config: &crate::config::FnmConfig) -> Option<std::path::PathBuf> {
match self.installation_path(&config) {
None => None,
Some(path) => {
let mut canon_path = path.canonicalize().ok()?;
canon_path.pop();
Some(canon_path)
}
}
}
} }
impl<'de> serde::Deserialize<'de> for Version { impl<'de> serde::Deserialize<'de> for Version {

1
tests/feature_tests/mod.rs

@ -1,5 +1,6 @@
mod aliases; mod aliases;
mod current; mod current;
mod uninstall;
use crate::shellcode::*; use crate::shellcode::*;

14
tests/feature_tests/uninstall.rs

@ -0,0 +1,14 @@
#[allow(unused_imports)]
use crate::shellcode::*;
test_shell!(Bash, Zsh, Fish, PowerShell; {
EvalFnmEnv::default()
.then(Call::new("fnm", vec!["install", "12.0.0"]))
.then(Call::new("fnm", vec!["alias", "12.0.0", "hello"]))
.then(OutputContains::new(
OutputContains::new(Call::new("fnm", vec!["ls"]), "v12.0.0"),
"hello",
))
.then(Call::new("fnm", vec!["uninstall", "hello"]))
.then(ExpectCommandOutput::new(Call::new("fnm", vec!["ls"]), "* system", "fnm ls"))
});

16
tests/snapshots/e2e__feature_tests__uninstall__Bash.snap

@ -0,0 +1,16 @@
---
source: tests/e2e.rs
expression: "&source.trim()"
---
set -e
shopt -s expand_aliases
eval "$(fnm env)"
fnm install 12.0.0
fnm alias 12.0.0 hello
fnm ls | grep v12.0.0 | grep hello
fnm uninstall hello
if [ "$(fnm ls)" != "* system" ]; then
echo 'Expected fnm ls to be "* system", Got: '"$(fnm ls)"
exit 1
fi

13
tests/snapshots/e2e__feature_tests__uninstall__Fish.snap

@ -0,0 +1,13 @@
---
source: tests/e2e.rs
expression: "&source.trim()"
---
fnm env | source
fnm install 12.0.0
fnm alias 12.0.0 hello
fnm ls | grep v12.0.0 | grep hello
fnm uninstall hello
if test (fnm ls) != "* system"
echo 'Expected fnm ls to be "* system", Got: '(fnm ls)
exit 1
end

14
tests/snapshots/e2e__feature_tests__uninstall__PowerShell.snap

@ -0,0 +1,14 @@
---
source: tests/e2e.rs
expression: "&source.trim()"
---
$ErrorActionPreference = "Stop"
fnm env | Out-String | Invoke-Expression
fnm install 12.0.0
fnm alias 12.0.0 hello
$($__out__ = $($($__out__ = $(fnm ls | Select-String 'v12.0.0'); echo $__out__; if ($__out__ -eq $null){ exit 1 } else { $__out__ }) | Select-String 'hello'); echo $__out__; if ($__out__ -eq $null){ exit 1 } else { $__out__ })
fnm uninstall hello
If ("$(fnm ls)" -ne "* system") {
Write-Output ('Expected fnm ls to be "* system", Got: ' + $(fnm ls))
exit 1
}

14
tests/snapshots/e2e__feature_tests__uninstall__Zsh.snap

@ -0,0 +1,14 @@
---
source: tests/e2e.rs
expression: "&source.trim()"
---
set -e
eval "$(fnm env)"
fnm install 12.0.0
fnm alias 12.0.0 hello
fnm ls | grep v12.0.0 | grep hello
fnm uninstall hello
if [ "$(fnm ls)" != "* system" ]; then
echo 'Expected fnm ls to be "* system", Got: '"$(fnm ls)"
exit 1
fi
Loading…
Cancel
Save