feat: added --bump option for outdated/upgrade (#2667)

This commit is contained in:
jdx 2024-09-27 16:17:59 -05:00 committed by GitHub
parent 8f5b9b3da8
commit 0dd91666f5
58 changed files with 473 additions and 306 deletions

View File

@ -17,6 +17,7 @@ concurrency:
env:
MISE_EXPERIMENTAL: 1
NPM_CONFIG_FUND: false
jobs:
release-plz:

View File

@ -20,11 +20,6 @@ jobs:
runs-on: ${{matrix.runs-on}}
timeout-minutes: 45
env:
#RUSTC_WRAPPER: sccache
SCCACHE_ENDPOINT: minio.jdx.dev
SCCACHE_BUCKET: sccache
SCCACHE_REGION: auto
SCCACHE_S3_NO_CREDENTIALS: ${{ secrets.MINIO_AWS_ACCESS_KEY_ID && '0' || '1' }}
MINIO_AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_AWS_ACCESS_KEY_ID }}
MINIO_AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_AWS_SECRET_ACCESS_KEY }}
strategy:
@ -74,11 +69,6 @@ jobs:
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12 }}
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS }}
- uses: actions/checkout@v4
- if: ${{ env.SCCACHE_S3_NO_CREDENTIALS == '0' }}
run: |
echo "AWS_ACCESS_KEY_ID=$MINIO_AWS_ACCESS_KEY_ID" >> "$GITHUB_ENV"
echo "AWS_SECRET_ACCESS_KEY=$MINIO_AWS_SECRET_ACCESS_KEY" >> "$GITHUB_ENV"
- uses: mozilla-actions/sccache-action@v0.0.4
- name: cache crates
id: cache-crates
uses: actions/cache@v4
@ -109,19 +99,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: steps.cache-crates.outputs.cache-hit != 'true'
run: cargo cache --autoclean
- run: ${SCCACHE_PATH} --show-stats
build-tarball-windows:
name: build-tarball-windows-${{matrix.arch}}
runs-on: windows-latest
timeout-minutes: 45
env:
#RUSTC_WRAPPER: sccache
SCCACHE_ENDPOINT: minio.jdx.dev
SCCACHE_BUCKET: sccache
SCCACHE_REGION: auto
SCCACHE_S3_NO_CREDENTIALS: ${{ secrets.MINIO_AWS_ACCESS_KEY_ID && '0' || '1' }}
AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_AWS_SECRET_ACCESS_KEY }}
strategy:
fail-fast: false
matrix:
@ -133,7 +114,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: rustup target add ${{matrix.target}}
- uses: mozilla-actions/sccache-action@v0.0.4
- name: cache crates
id: cache-crates
uses: actions/cache@v4

View File

@ -1,26 +1,5 @@
target.x86_64-unknown-linux-gnu.pre-build = "./scripts/sccache.sh"
target.x86_64-unknown-linux-musl.pre-build = "./scripts/sccache.sh"
#target.aarch64-unknown-linux-gnu.pre-build = "./scripts/sccache.sh"
#target.aarch64-unknown-linux-musl.pre-build = "./scripts/sccache.sh"
#target.armv7-unknown-linux-gnueabihf.pre-build = "./scripts/sccache.sh"
#target.armv7-unknown-linux-musleabihf.pre-build = "./scripts/sccache.sh"
#target.arm-unknown-linux-gnueabi.pre-build = "./scripts/sccache.sh"
#target.arm-unknown-linux-musleabi.pre-build = "./scripts/sccache.sh"
#target.x86_64-apple-darwin.pre-build = "./scripts/sccache.sh"
#target.aarch64-apple-darwin.pre-build = "./scripts/sccache.sh"
#target.aarch64-pc-windows-msvc.pre-build = "./scripts/sccache.sh"
#target.x86_64-pc-windows-msvc.pre-build = "./scripts/sccache.sh"
[build.env]
passthrough = [
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"RUSTC_WRAPPER",
"SCCACHE_BUCKET",
"SCCACHE_DIR",
"SCCACHE_ENDPOINT",
"SCCACHE_ERROR_LOG",
"SCCACHE_LOG",
"SCCACHE_REGION",
"SCCACHE_S3_NO_CREDENTIALS",
]

View File

@ -792,9 +792,20 @@ Arguments:
If not specified, all tools in global and local configs will be shown
Options:
-l, --bump
Compares against the latest versions available, not what matches the current config
For example, if you have `node = "20"` in your config by default `mise outdated` will only
show other 20.x versions, not 21.x or 22.x versions.
Using this flag, if there are 21.x or newer versions it will display those instead of 20.x.
-J, --json
Output in JSON format
--no-header
Don't show table header
Examples:
$ mise outdated
@ -1763,14 +1774,23 @@ Options:
-n, --dry-run
Just print what would be done, don't actually do it
-i, --interactive
Display multiselect menu to choose which tools to upgrade
-j, --jobs <JOBS>
Number of jobs to run in parallel
[default: 4]
[env: MISE_JOBS=]
-i, --interactive
Display multiselect menu to choose which tools to upgrade
-l, --bump
Upgrades to the latest version available, bumping the version in mise.toml
For example, if you have `node = "20.0.0"` in your mise.toml but 22.1.0 is the latest available,
this will install 22.1.0 and set `node = "22.1.0"` in your config.
It keeps the same precision as what was there before, so if you instead had `node = "20"`, it
would change your config to `node = "22"`.
--raw
Directly pipe stdin/stdout/stderr from plugin to user Sets --jobs=1

View File

@ -12,9 +12,20 @@ Arguments:
If not specified, all tools in global and local configs will be shown
Options:
-l, --bump
Compares against the latest versions available, not what matches the current config
For example, if you have `node = "20"` in your config by default `mise outdated` will only
show other 20.x versions, not 21.x or 22.x versions.
Using this flag, if there are 21.x or newer versions it will display those instead of 20.x.
-J, --json
Output in JSON format
--no-header
Don't show table header
Examples:
$ mise outdated

View File

@ -17,14 +17,23 @@ Options:
-n, --dry-run
Just print what would be done, don't actually do it
-i, --interactive
Display multiselect menu to choose which tools to upgrade
-j, --jobs <JOBS>
Number of jobs to run in parallel
[default: 4]
[env: MISE_JOBS=]
-i, --interactive
Display multiselect menu to choose which tools to upgrade
-l, --bump
Upgrades to the latest version available, bumping the version in mise.toml
For example, if you have `node = "20.0.0"` in your mise.toml but 22.1.0 is the latest available,
this will install 22.1.0 and set `node = "22.1.0"` in your config.
It keeps the same precision as what was there before, so if you instead had `node = "20"`, it
would change your config to `node = "22"`.
--raw
Directly pipe stdin/stdout/stderr from plugin to user Sets --jobs=1

View File

@ -10,3 +10,7 @@ mise upgrade dummy
assert_contains "mise ls --installed dummy" "1.1.0"
assert_not_contains "mise ls --installed dummy" "1.0.0"
mise upgrade dummy --bump
assert_contains "mise ls --installed dummy" "2.0.0"
assert_not_contains "mise ls --installed dummy" "1.1.0"

View File

@ -29,8 +29,7 @@ rm -rf "$MISE_DATA_DIR/installs/tiny"
mise use tiny@3
mv "$MISE_DATA_DIR/installs/tiny/"{3.1.0,3.0.0}
assert "mise current tiny" "3.0.0"
assert "mise outdated tiny" "Tool Requested Current Latest
tiny 3 3.0.0 3.1.0"
assert "mise outdated tiny" "tiny 3 3.0.0 3.1.0 ~/workdir/.mise.toml"
mise upgrade tiny
assert "mise current tiny" "3.1.0"
assert "mise outdated tiny" ""

View File

@ -627,7 +627,11 @@ cmd "outdated" help="Shows outdated tool versions" {
$ mise outdated --json
{"python": {"requested": "3.11", "current": "3.11.0", "latest": "3.11.1"}, ...}
"#
flag "-l --bump" help="Compares against the latest versions available, not what matches the current config" {
long_help "Compares against the latest versions available, not what matches the current config\n\nFor example, if you have `node = \"20\"` in your config by default `mise outdated` will only\nshow other 20.x versions, not 21.x or 22.x versions.\n\nUsing this flag, if there are 21.x or newer versions it will display those instead of 20.x."
}
flag "-J --json" help="Output in JSON format"
flag "--no-header" help="Don't show table header"
arg "[TOOL@VERSION]..." help="Tool(s) to show outdated versions for\ne.g.: node@20 python@3.10\nIf not specified, all tools in global and local configs will be shown" var=true
}
cmd "plugins" help="Manage plugins" {
@ -1222,10 +1226,13 @@ By default this command modifies ".mise.toml" in the current directory."#
cmd "upgrade" help="Upgrades outdated tool versions" {
alias "up"
flag "-n --dry-run" help="Just print what would be done, don't actually do it"
flag "-i --interactive" help="Display multiselect menu to choose which tools to upgrade"
flag "-j --jobs" help="Number of jobs to run in parallel\n[default: 4]" {
arg "<JOBS>"
}
flag "-i --interactive" help="Display multiselect menu to choose which tools to upgrade"
flag "-l --bump" help="Upgrades to the latest version available, bumping the version in mise.toml" {
long_help "Upgrades to the latest version available, bumping the version in mise.toml\n\nFor example, if you have `node = \"20.0.0\"` in your mise.toml but 22.1.0 is the latest available,\nthis will install 22.1.0 and set `node = \"22.1.0\"` in your config.\n\nIt keeps the same precision as what was there before, so if you instead had `node = \"20\"`, it\nwould change your config to `node = \"22\"`."
}
flag "--raw" help="Directly pipe stdin/stdout/stderr from plugin to user Sets --jobs=1"
arg "[TOOL@VERSION]..." help="Tool(s) to upgrade\ne.g.: node@20 python@3.10\nIf not specified, all current tools will be upgraded" var=true
}

View File

@ -92,7 +92,7 @@ fi
cd dist
if [[ "$os" == "macos" ]]; then
codesign -f -s "Developer ID Application: Jeffrey Dickey (4993Y37DX6)" mise/bin/mise
codesign -f -p dev.jdx. -s "Developer ID Application: Jeffrey Dickey (4993Y37DX6)" mise/bin/mise
fi
if [[ "$os" == "windows" ]]; then

View File

@ -1,48 +0,0 @@
#!/bin/bash
set -x
set -euo pipefail
# shellcheck disable=SC1091
. lib.sh
main() {
local triple
local tag
local td
local url="https://github.com/mozilla/sccache"
triple="${1}"
triple="${triple%%-musl}"
triple="${triple%%-musleabi}"
triple="${triple%%-musleabihf}"
triple="${triple%%-gnu}"
triple="${triple%%-gnueabi}"
triple="${triple%%-gnueabihf}"
triple="$triple-musl"
install_packages unzip tar
# Download our package, then install our binary.
td="$(mktemp -d)"
pushd "${td}"
tag=$(git ls-remote --tags --refs --exit-code \
"${url}" \
| cut -d/ -f3 \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort --version-sort \
| tail -n1)
curl -LSfs "${url}/releases/download/${tag}/sccache-${tag}-${triple}.tar.gz" \
-o sccache.tar.gz
tar -xvf sccache.tar.gz
rm sccache.tar.gz
cp "sccache-${tag}-${triple}/sccache" "/usr/bin/sccache"
chmod +x "/usr/bin/sccache"
# clean up our install
purge_packages
popd
rm -rf "${td}"
rm "${0}"
}
main "${@}"

View File

@ -17,6 +17,7 @@ use versions::Versioning;
use self::backend_meta::BackendMeta;
use crate::cli::args::{BackendArg, ToolVersionType};
use crate::cmd::CmdLineRunner;
use crate::config::settings::SETTINGS;
use crate::config::{Config, Settings, CONFIG};
use crate::file::{display_path, remove_all, remove_all_with_warning};
use crate::install_context::InstallContext;
@ -364,7 +365,6 @@ pub trait Backend: Debug + Send + Sync {
plugin.is_installed_err()?;
}
let config = Config::get();
let settings = Settings::try_get()?;
if self.is_version_installed(&ctx.tv, true) {
if ctx.force {
self.uninstall_version(&ctx.tv, ctx.pr.as_ref(), false)?;
@ -377,13 +377,13 @@ pub trait Backend: Debug + Send + Sync {
self.create_install_dirs(&ctx.tv)?;
if let Err(e) = self.install_version_impl(&ctx) {
self.cleanup_install_dirs_on_error(&settings, &ctx.tv);
self.cleanup_install_dirs_on_error(&SETTINGS, &ctx.tv);
return Err(e);
}
BackendMeta::write(&ctx.tv.backend)?;
self.cleanup_install_dirs(&settings, &ctx.tv);
self.cleanup_install_dirs(&SETTINGS, &ctx.tv);
// attempt to touch all the .tool-version files to trigger updates in hook-env
let mut touch_dirs = vec![dirs::DATA.to_path_buf()];
touch_dirs.extend(config.config_files.keys().cloned());

View File

@ -1,13 +1,11 @@
use std::collections::HashSet;
use std::sync::Arc;
use console::{pad_str, style, Alignment};
use eyre::Result;
use crate::backend::Backend;
use crate::cli::args::ToolArg;
use crate::config::Config;
use crate::toolset::{ToolVersion, ToolsetBuilder};
use crate::toolset::{OutdatedInfo, ToolsetBuilder};
use crate::ui::table;
use eyre::Result;
use indexmap::IndexMap;
/// Shows outdated tool versions
#[derive(Debug, clap::Args)]
@ -19,9 +17,22 @@ pub struct Outdated {
#[clap(value_name = "TOOL@VERSION", verbatim_doc_comment)]
pub tool: Vec<ToolArg>,
/// Compares against the latest versions available, not what matches the current config
///
/// For example, if you have `node = "20"` in your config by default `mise outdated` will only
/// show other 20.x versions, not 21.x or 22.x versions.
///
/// Using this flag, if there are 21.x or newer versions it will display those instead of 20.x.
#[clap(long, short = 'l', verbatim_doc_comment)]
pub bump: bool,
/// Output in JSON format
#[clap(short = 'J', long, verbatim_doc_comment)]
pub json: bool,
/// Don't show table header
#[clap(long)]
pub no_header: bool,
}
impl Outdated {
@ -35,7 +46,7 @@ impl Outdated {
.collect::<HashSet<_>>();
ts.versions
.retain(|_, tvl| tool_set.is_empty() || tool_set.contains(&tvl.backend));
let outdated = ts.list_outdated_versions();
let outdated = ts.list_outdated_versions(self.bump);
if outdated.is_empty() {
info!("All tools are up to date");
} else if self.json {
@ -47,87 +58,23 @@ impl Outdated {
Ok(())
}
fn display(&self, outdated: OutputVec) -> Result<()> {
// TODO: make a generic table printer in src/ui/table
let plugins = outdated.iter().map(|(t, _, _)| t.id()).collect::<Vec<_>>();
let requests = outdated
.iter()
.map(|(_, tv, _)| tv.request.version())
.collect::<Vec<_>>();
let currents = outdated
.iter()
.map(|(t, tv, _)| {
if t.is_version_installed(tv, true) {
tv.version.clone()
} else {
"MISSING".to_string()
}
})
.collect::<Vec<_>>();
let latests = outdated
.iter()
.map(|(_, _, c)| c.clone())
.collect::<Vec<_>>();
let plugin_width = plugins
.iter()
.map(|s| s.len())
.max()
.unwrap_or_default()
.max(6)
+ 1;
let requested_width = requests
.iter()
.map(|s| s.len())
.max()
.unwrap_or_default()
.max(9)
+ 1;
let current_width = currents
.iter()
.map(|s| s.len())
.max()
.unwrap_or_default()
.max(7)
+ 1;
let pad_plugin = |s| pad_str(s, plugin_width, Alignment::Left, None);
let pad_requested = |s| pad_str(s, requested_width, Alignment::Left, None);
let pad_current = |s| pad_str(s, current_width, Alignment::Left, None);
miseprintln!(
"{} {} {} {}",
style(pad_plugin("Tool")).dim(),
style(pad_requested("Requested")).dim(),
style(pad_current("Current")).dim(),
style("Latest").dim(),
);
for i in 0..outdated.len() {
miseprintln!(
"{} {} {} {}",
pad_plugin(plugins[i]),
pad_requested(&requests[i]),
pad_current(&currents[i]),
latests[i]
);
}
fn display(&self, outdated: Vec<OutdatedInfo>) -> Result<()> {
let mut table = tabled::Table::new(outdated);
table::default_style(&mut table, self.no_header);
miseprintln!("{table}");
Ok(())
}
fn display_json(&self, outdated: OutputVec) -> Result<()> {
let mut map = serde_json::Map::new();
for (t, tv, c) in outdated {
let mut inner = serde_json::Map::new();
inner.insert("requested".to_string(), tv.request.version().into());
inner.insert("current".to_string(), tv.version.clone().into());
inner.insert("latest".to_string(), c.into());
map.insert(t.id().to_string(), serde_json::Value::Object(inner));
fn display_json(&self, outdated: Vec<OutdatedInfo>) -> Result<()> {
let mut map = IndexMap::new();
for o in outdated {
map.insert(o.name.to_string(), o);
}
let json = serde_json::Value::Object(map);
miseprintln!("{}", serde_json::to_string_pretty(&json)?);
miseprintln!("{}", serde_json::to_string_pretty(&map)?);
Ok(())
}
}
type OutputVec = Vec<(Arc<dyn Backend>, ToolVersion, String)>;
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
@ -170,4 +117,11 @@ mod tests {
assert_cli_snapshot!("outdated", "tiny", "--json");
change_installed_version("tiny", "3.0.0", "3.1.0");
}
#[test]
fn test_outdated_json_bump() {
reset();
assert_cli!("use", "tiny@2");
assert_cli_snapshot!("outdated", "tiny", "--json", "--bump");
}
}

View File

@ -4,4 +4,3 @@ expression: err
---
mise is not activated in this shell session.
Please run `mise activate` first in your shell rc file.

View File

@ -4,8 +4,14 @@ expression: output
---
{
"tiny": {
"name": "tiny",
"requested": "3",
"current": "3.0.0",
"bump": null,
"latest": "3.1.0",
"requested": "3"
"source": {
"type": ".tool-versions",
"path": "~/cwd/.test-tool-versions"
}
}
}

View File

@ -0,0 +1,17 @@
---
source: src/cli/outdated.rs
expression: output
---
{
"tiny": {
"name": "tiny",
"requested": "2",
"current": "2.1.0",
"bump": "3",
"latest": "3.1.0",
"source": {
"type": ".tool-versions",
"path": "~/cwd/.test-tool-versions"
}
}
}

View File

@ -4,4 +4,3 @@ expression: "file::read_to_string(cf_path).unwrap()"
---
[env]
FOO = "bar"

View File

@ -4,4 +4,3 @@ expression: "file::read_to_string(cf_path).unwrap()"
---
[env]
FOO = "bar"

View File

@ -3,4 +3,3 @@ source: src/cli/set.rs
expression: "file::read_to_string(cf_path).unwrap()"
---
[env]

View File

@ -3,4 +3,3 @@ source: src/cli/set.rs
expression: "file::read_to_string(cf_path).unwrap()"
---
[env]

View File

@ -4,4 +4,3 @@ expression: err
---
mise is not activated in this shell session.
Please run `mise activate` first in your shell rc file.

View File

@ -3,4 +3,3 @@ source: src/cli/unset.rs
expression: "file::read_to_string(cf_path).unwrap()"
---
[env]

View File

@ -0,0 +1,5 @@
---
source: src/cli/upgrade.rs
expression: output
---

View File

@ -0,0 +1,8 @@
---
source: src/cli/upgrade.rs
expression: output
---
mise Would uninstall tiny@3.0.0
mise Would uninstall dummy@ref:master
mise Would install tiny@3.1.0
mise Would install dummy@2.0.0

View File

@ -1,16 +1,12 @@
use std::collections::HashSet;
use std::sync::Arc;
use demand::DemandOption;
use eyre::{Context, Result};
use crate::backend::Backend;
use crate::cli::args::ToolArg;
use crate::config::Config;
use crate::toolset::{InstallOptions, ToolVersion, ToolsetBuilder};
use crate::config::{config_file, Config};
use crate::file::display_path;
use crate::toolset::{InstallOptions, OutdatedInfo, ToolVersion, ToolsetBuilder};
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::progress_report::SingleReport;
use crate::{runtime_symlinks, shims, ui};
use demand::DemandOption;
use eyre::{Context, Result};
/// Upgrades outdated tool versions
#[derive(Debug, clap::Args)]
@ -26,14 +22,24 @@ pub struct Upgrade {
#[clap(long, short = 'n', verbatim_doc_comment)]
dry_run: bool,
/// Display multiselect menu to choose which tools to upgrade
#[clap(long, short, verbatim_doc_comment, conflicts_with = "tool")]
interactive: bool,
/// Number of jobs to run in parallel
/// [default: 4]
#[clap(long, short, env = "MISE_JOBS", verbatim_doc_comment)]
jobs: Option<usize>,
/// Display multiselect menu to choose which tools to upgrade
#[clap(long, short, verbatim_doc_comment, conflicts_with = "tool")]
interactive: bool,
/// Upgrades to the latest version available, bumping the version in mise.toml
///
/// For example, if you have `node = "20.0.0"` in your mise.toml but 22.1.0 is the latest available,
/// this will install 22.1.0 and set `node = "22.1.0"` in your config.
///
/// It keeps the same precision as what was there before, so if you instead had `node = "20"`, it
/// would change your config to `node = "22"`.
#[clap(long, short = 'l', verbatim_doc_comment)]
bump: bool,
/// Directly pipe stdin/stdout/stderr from plugin to user
/// Sets --jobs=1
@ -45,17 +51,15 @@ impl Upgrade {
pub fn run(self) -> Result<()> {
let config = Config::try_get()?;
let ts = ToolsetBuilder::new().with_args(&self.tool).build(&config)?;
let mut outdated = ts.list_outdated_versions();
let mut outdated = ts.list_outdated_versions(self.bump);
if self.interactive && !outdated.is_empty() {
let tvs = self.get_interactive_tool_set(&outdated)?;
outdated.retain(|(_, tv, _)| tvs.contains(tv));
} else {
let tool_set = self
.tool
.iter()
.map(|t| t.backend.clone())
.collect::<HashSet<_>>();
outdated.retain(|(p, _, _)| tool_set.is_empty() || tool_set.contains(p.fa()));
outdated = self.get_interactive_tool_set(&outdated)?;
} else if !self.tool.is_empty() {
outdated.retain(|o| {
self.tool
.iter()
.any(|t| t.backend == o.tool_version.backend)
});
}
if outdated.is_empty() {
info!("All tools are up to date");
@ -66,31 +70,57 @@ impl Upgrade {
Ok(())
}
fn upgrade(&self, config: &Config, outdated: OutputVec) -> Result<()> {
fn upgrade(&self, config: &Config, outdated: Vec<OutdatedInfo>) -> Result<()> {
let mpr = MultiProgressReport::get();
let mut ts = ToolsetBuilder::new().with_args(&self.tool).build(config)?;
let new_versions = outdated
let config_file_updates = outdated
.iter()
.map(|(_, tv, latest)| {
let mut tv = tv.clone();
tv.version.clone_from(latest);
tv
.filter_map(|o| {
if let (Some(path), Some(bump)) = (o.source.path(), &o.bump) {
match config_file::parse(path) {
Ok(cf) => Some((o, bump.clone(), cf)),
Err(e) => {
warn!("failed to parse {}: {e}", display_path(path));
None
}
}
} else {
None
}
})
.filter(|(o, _bump, cf)| {
if let Ok(trs) = cf.to_tool_request_set() {
if let Some(versions) = trs.tools.get(o.tool_request.backend()) {
if versions.len() != 1 {
warn!("upgrading multiple versions with --bump is not yet supported");
return false;
}
}
}
true
})
.collect::<Vec<_>>();
let to_remove = outdated
.into_iter()
.filter(|(tool, tv, _)| tool.is_version_installed(tv, true))
.map(|(tool, tv, _)| (tool, tv))
.iter()
.filter_map(|o| o.current.as_ref().map(|current| (o, current)))
.collect::<Vec<_>>();
if self.dry_run {
for (_, tv) in &to_remove {
info!("Would uninstall {tv}");
for (o, current) in &to_remove {
info!("Would uninstall {}@{}", o.name, current);
}
for tv in &new_versions {
info!("Would install {tv}");
for o in &outdated {
info!("Would install {}@{}", o.name, o.latest);
}
for (o, bump, cf) in &config_file_updates {
info!(
"Would bump {}@{} in {}",
o.name,
bump,
display_path(cf.get_path())
);
}
return Ok(());
}
@ -100,11 +130,17 @@ impl Upgrade {
raw: self.raw,
latest_versions: true,
};
let new_versions = new_versions.into_iter().map(|tv| tv.request).collect();
let new_versions = outdated.iter().map(|o| o.tool_request.clone()).collect();
ts.install_versions(config, new_versions, &mpr, &opts)?;
for (tool, tv) in to_remove {
let pr = mpr.add(&tv.style());
self.uninstall_old_version(tool.clone(), &tv, pr.as_ref())?;
for (o, bump, mut cf) in config_file_updates {
cf.replace_versions(o.tool_request.backend(), &[bump])?;
cf.save()?;
}
for (o, tv) in to_remove {
let pr = mpr.add(&format!("Uninstalling {}@{}", o.name, tv));
self.uninstall_old_version(&o.tool_version, pr.as_ref())?;
}
let ts = ToolsetBuilder::new().with_args(&self.tool).build(config)?;
@ -113,38 +149,27 @@ impl Upgrade {
Ok(())
}
fn uninstall_old_version(
&self,
tool: Arc<dyn Backend>,
tv: &ToolVersion,
pr: &dyn SingleReport,
) -> Result<()> {
tool.uninstall_version(tv, pr, self.dry_run)
fn uninstall_old_version(&self, tv: &ToolVersion, pr: &dyn SingleReport) -> Result<()> {
tv.get_backend()
.uninstall_version(tv, pr, self.dry_run)
.wrap_err_with(|| format!("failed to uninstall {tv}"))?;
pr.finish();
Ok(())
}
fn get_interactive_tool_set(&self, outdated: &OutputVec) -> Result<HashSet<ToolVersion>> {
fn get_interactive_tool_set(&self, outdated: &Vec<OutdatedInfo>) -> Result<Vec<OutdatedInfo>> {
let _ctrlc = ui::ctrlc::handle_ctrlc()?;
let mut ms = demand::MultiSelect::new("mise upgrade")
.description("Select tools to upgrade")
.filterable(true)
.min(1);
for (_, tv, latest) in outdated {
let label = if &tv.version == latest {
tv.to_string()
} else {
format!("{tv} -> {latest}")
};
ms = ms.option(DemandOption::new(tv).label(&label));
for out in outdated {
ms = ms.option(DemandOption::new(out.clone()));
}
Ok(ms.run()?.into_iter().cloned().collect())
Ok(ms.run()?.into_iter().collect())
}
}
type OutputVec = Vec<(Arc<dyn Backend>, ToolVersion, String)>;
#[cfg(test)]
pub mod tests {
use crate::dirs;
@ -158,4 +183,13 @@ pub mod tests {
assert_cli_snapshot!("upgrade");
assert!(dirs::INSTALLS.join("tiny").join("3.1.0").exists());
}
#[test]
fn test_upgrade_bump() {
reset();
change_installed_version("tiny", "3.1.0", "3.0.0");
assert_cli_snapshot!("upgrade", "--dry-run", "--bump");
assert_cli_snapshot!("upgrade");
assert!(dirs::INSTALLS.join("tiny").join("3.1.0").exists());
}
}

View File

@ -2,4 +2,4 @@
source: src/config/config_file/mise_toml.rs
expression: cf
---
~/cwd/.test.mise.toml:
~/cwd/.test.mise.toml:

View File

@ -1,6 +1,6 @@
---
source: src/config/config_file/mise_toml.rs
expression: cf.plugins()
expression: cf.plugins().unwrap()
---
{
"node": "https://github.com/jdx/rtx-node",

View File

@ -1,6 +1,6 @@
---
source: src/config/config_file/mise_toml.rs
expression: cf.env_entries()
expression: cf.env_entries().unwrap()
---
[
Val(

View File

@ -4,4 +4,3 @@ expression: cf.dump().unwrap()
---
[alias.node]
18 = "18.0.0"

View File

@ -2,4 +2,4 @@
source: src/config/config_file/mise_toml.rs
expression: cf
---
~/cwd/.test.mise.toml:
~/cwd/.test.mise.toml:

View File

@ -2,4 +2,4 @@
source: src/config/config_file/mise_toml.rs
expression: cf
---
/tmp/.mise.toml:
/tmp/.mise.toml:

View File

@ -4,4 +4,3 @@ expression: cf.dump().unwrap()
---
[tools]
node = ["16.0.1", "18.0.1"]

View File

@ -2,4 +2,4 @@
source: src/config/config_file/mise_toml.rs
expression: cf
---
~/cwd/.test.mise.toml:
~/cwd/.test.mise.toml:

View File

@ -395,7 +395,7 @@ impl Backend for JavaPlugin {
}
fn fuzzy_match_filter(&self, versions: Vec<String>, query: &str) -> eyre::Result<Vec<String>> {
let escaped_query = regex::escape(query);
let escaped_query = regex::escape(query.trim_end_matches('-'));
let query = if query == "latest" {
"[0-9].*"
} else {

View File

@ -53,4 +53,3 @@ if [ -z "${_mise_cmd_not_found:-}" ]; then
fi
}
fi

View File

@ -7,4 +7,3 @@ PROMPT_COMMAND="${PROMPT_COMMAND//_mise_hook/}"
unset _mise_hook
unset mise
unset MISE_SHELL

View File

@ -3,4 +3,3 @@ source: src/shell/bash.rs
expression: "replace_path(&bash.prepend_env(\"PATH\", \"/some/dir:/2/dir\"))"
---
export PATH="/some/dir:/2/dir:$PATH"

View File

@ -3,4 +3,3 @@ source: src/shell/bash.rs
expression: "Bash::default().set_env(\"FOO\", \"1\")"
---
export FOO=1

View File

@ -3,4 +3,3 @@ source: src/shell/bash.rs
expression: "Bash::default().unset_env(\"FOO\")"
---
unset FOO

View File

@ -7,4 +7,3 @@ functions --erase __mise_env_eval_2
functions --erase __mise_cd_hook
functions --erase mise
set -e MISE_SHELL

View File

@ -3,4 +3,3 @@ source: src/shell/fish.rs
expression: "replace_path(&sh.prepend_env(\"PATH\", \"/some/dir:/2/dir\"))"
---
set -gx PATH '/some/dir:/2/dir' $PATH

View File

@ -3,4 +3,3 @@ source: src/shell/fish.rs
expression: "Fish::default().set_env(\"FOO\", \"1\")"
---
set -gx FOO 1

View File

@ -3,4 +3,3 @@ source: src/shell/fish.rs
expression: "Fish::default().unset_env(\"FOO\")"
---
set -e FOO

View File

@ -3,4 +3,3 @@ source: src/shell/nushell.rs
expression: replace_path(&deactivate)
---
hide,MISE_SHELL,

View File

@ -53,5 +53,3 @@ def --env mise_hook [] {
| parse vars
| update-env
}

View File

@ -3,4 +3,3 @@ source: src/shell/nushell.rs
expression: "replace_path(&sh.prepend_env(\"PATH\", \"/some/dir:/2/dir\"))"
---
$env.PATH = ($env.PATH | prepend '/some/dir:/2/dir')

View File

@ -3,4 +3,3 @@ source: src/shell/nushell.rs
expression: "Nushell::default().set_env(\"FOO\", \"1\")"
---
set,FOO,1

View File

@ -3,4 +3,3 @@ source: src/shell/nushell.rs
expression: "Nushell::default().unset_env(\"FOO\")"
---
hide,FOO,

View File

@ -8,4 +8,3 @@ from xonsh.built_ins import XSH
envx = XSH.env
envx[ 'PATH'].add('/some/dir:/2/dir', front=True)
environ['PATH'] = envx.get_detyped('PATH')

View File

@ -8,4 +8,3 @@ from xonsh.built_ins import XSH
envx = XSH.env
envx[ 'FOO'] = '1'
environ['FOO'] = envx.get_detyped('FOO')

View File

@ -8,4 +8,3 @@ from xonsh.built_ins import XSH
envx = XSH.env
envx.pop( 'FOO',None)
environ.pop('FOO',None)

View File

@ -54,4 +54,3 @@ if [ -z "${_mise_cmd_not_found:-}" ]; then
fi
}
fi

View File

@ -7,4 +7,3 @@ chpwd_functions=( ${chpwd_functions:#_mise_hook} )
unset -f _mise_hook
unset -f mise
unset MISE_SHELL

View File

@ -3,4 +3,3 @@ source: src/shell/zsh.rs
expression: "replace_path(&sh.prepend_env(\"PATH\", \"/some/dir:/2/dir\"))"
---
export PATH="/some/dir:/2/dir:$PATH"

View File

@ -3,4 +3,3 @@ source: src/shell/zsh.rs
expression: "Zsh::default().set_env(\"FOO\", \"1\")"
---
export FOO=1

View File

@ -3,4 +3,3 @@ source: src/shell/zsh.rs
expression: "Zsh::default().unset_env(\"FOO\")"
---
unset FOO

View File

@ -6,24 +6,25 @@ use std::thread::sleep;
use std::time::Duration;
use std::{panic, thread};
pub use builder::ToolsetBuilder;
use console::truncate_str;
use eyre::{eyre, Result};
use indexmap::IndexMap;
use itertools::Itertools;
use rayon::prelude::*;
pub use builder::ToolsetBuilder;
use serde_derive::Serialize;
use tabled::Tabled;
pub use tool_request::ToolRequest;
pub use tool_request_set::{ToolRequestSet, ToolRequestSetBuilder};
pub use tool_source::ToolSource;
pub use tool_version::ToolVersion;
pub use tool_version_list::ToolVersionList;
use versions::Version;
use versions::{Version, Versioning};
use crate::backend::Backend;
use crate::cli::args::BackendArg;
use crate::config::settings::SettingsStatusMissingTools;
use crate::config::{Config, Settings};
use crate::config::settings::{SettingsStatusMissingTools, SETTINGS};
use crate::config::Config;
use crate::env::{PATH_KEY, TERM_WIDTH};
use crate::errors::Error;
use crate::install_context::InstallContext;
@ -50,10 +51,9 @@ pub struct InstallOptions {
impl InstallOptions {
pub fn new() -> Self {
let settings = Settings::get();
InstallOptions {
jobs: Some(settings.jobs),
raw: settings.raw,
jobs: Some(SETTINGS.jobs),
raw: SETTINGS.raw,
..Default::default()
}
}
@ -161,7 +161,6 @@ impl Toolset {
self.install_versions(config, leaf_deps.into_iter().cloned().collect(), mpr, opts)?;
}
debug!("install_versions: {}", versions.iter().join(" "));
let settings = Settings::try_get()?;
let queue: Vec<_> = versions
.into_iter()
.rev()
@ -183,10 +182,10 @@ impl Toolset {
}
}
let queue = Arc::new(Mutex::new(queue));
let raw = opts.raw || settings.raw;
let raw = opts.raw || SETTINGS.raw;
let jobs = match raw {
true => 1,
false => opts.jobs.unwrap_or(settings.jobs),
false => opts.jobs.unwrap_or(SETTINGS.jobs),
};
let installing: HashSet<String> = HashSet::new();
let installing = Arc::new(Mutex::new(installing));
@ -316,28 +315,77 @@ impl Toolset {
.filter(|(p, v)| p.is_version_installed(v, true))
.collect()
}
pub fn list_outdated_versions(&self) -> Vec<(Arc<dyn Backend>, ToolVersion, String)> {
pub fn list_outdated_versions(&self, bump: bool) -> Vec<OutdatedInfo> {
self.list_current_versions()
.into_iter()
.filter_map(|(t, tv)| {
if t.symlink_path(&tv).is_some() {
trace!("skipping symlinked version {tv}");
// do not consider symlinked versions to be outdated
return None;
}
let latest = match tv.latest_version(t.as_ref()) {
Ok(latest) => latest,
// prefix is something like "temurin-" or "corretto-"
let prefix = regex!(r"^[a-zA-Z]+-")
.find(&tv.request.version())
.map(|m| m.as_str().to_string());
let latest_result = if bump {
t.latest_version(prefix.clone())
} else {
tv.latest_version(t.as_ref()).map(Option::from)
};
let mut out =
OutdatedInfo::new(tv.clone(), self.find_source(&tv.request).unwrap().clone());
out.current = if t.is_version_installed(&tv, true) {
Some(tv.version.clone())
} else {
None
};
out.latest = match latest_result {
Ok(Some(latest)) => latest,
Ok(None) => {
warn!("Error getting latest version for {t}: no latest version found");
return None;
}
Err(e) => {
warn!("Error getting latest version for {t}: {e:#}");
return None;
}
};
if !t.is_version_installed(&tv, true)
|| is_outdated_version(tv.version.as_str(), latest.as_str())
if t.is_version_installed(&tv, true)
&& !is_outdated_version(tv.version.as_str(), out.latest.as_str())
{
Some((t, tv, latest))
} else {
None
trace!("skipping up-to-date version {tv}");
return None;
}
if bump {
let prefix = prefix.unwrap_or_default();
let old = tv.request.version();
let old = old.strip_prefix(&prefix).unwrap_or_default();
let new = out.latest.strip_prefix(&prefix).unwrap_or_default();
if let Some(bumped_version) = check_semver_bump(old, new) {
if bumped_version != tv.request.version() {
out.bump = Some(format!("{prefix}{bumped_version}"));
match out.tool_request.clone() {
ToolRequest::Version {
backend,
version: _version,
options,
} => {
out.tool_request = ToolRequest::Version {
backend,
options,
version: out.bump.clone().unwrap(),
};
}
_ => {
warn!("upgrading non-version tool requests");
out.bump = None;
}
}
}
}
}
Some(out)
})
.collect()
}
@ -468,11 +516,10 @@ impl Toolset {
// shows a warning if any versions are missing
// only displays for tools which have at least one version already installed
pub fn notify_if_versions_missing(&self) {
let settings = Settings::get();
let missing = self
.list_missing_versions()
.into_iter()
.filter(|tv| match settings.status.missing_tools() {
.filter(|tv| match SETTINGS.status.missing_tools() {
SettingsStatusMissingTools::Never => false,
SettingsStatusMissingTools::Always => true,
SettingsStatusMissingTools::IfOtherVersionsInstalled => tv
@ -496,9 +543,12 @@ impl Toolset {
}
fn is_disabled(&self, fa: &BackendArg) -> bool {
let settings = Settings::get();
let fa = fa.to_string();
settings.disable_tools.iter().any(|s| s == &fa)
SETTINGS.disable_tools.iter().any(|s| s == &fa)
}
pub fn find_source(&self, tr: &ToolRequest) -> Option<&ToolSource> {
self.versions.get(tr.backend()).map(|tvl| &tvl.source)
}
}
@ -574,13 +624,117 @@ fn is_outdated_version(current: &str, latest: &str) -> bool {
current != latest
}
fn check_semver_bump(old: &str, new: &str) -> Option<String> {
let old = Versioning::new(old);
let new = Versioning::new(new);
let chunkify = |v: &Versioning| {
let mut chunks = vec![];
while let Some(chunk) = v.nth(chunks.len()) {
chunks.push(chunk);
}
chunks
};
if let (Some(old), Some(new)) = (old, new) {
let old = chunkify(&old);
let new = chunkify(&new);
if old.len() > new.len() {
warn!(
"something weird happened with versioning, old: {old}, new: {new}, skipping",
old = old
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("."),
new = new
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("."),
);
return None;
}
let bump = new.into_iter().take(old.len()).collect::<Vec<_>>();
if bump == old {
None
} else {
Some(
bump.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("."),
)
}
} else {
None
}
}
#[derive(Debug, Serialize, Clone, Tabled)]
pub struct OutdatedInfo {
pub name: String,
#[serde(skip)]
#[tabled(skip)]
pub tool_request: ToolRequest,
#[serde(skip)]
#[tabled(skip)]
pub tool_version: ToolVersion,
pub requested: String,
#[tabled(display_with("Self::display_current", self))]
pub current: Option<String>,
#[tabled(skip)]
pub bump: Option<String>,
pub latest: String,
pub source: ToolSource,
}
impl OutdatedInfo {
fn new(tv: ToolVersion, source: ToolSource) -> Self {
Self {
name: tv.request.backend().name.clone(),
current: None,
requested: tv.request.version(),
tool_request: tv.request.clone(),
tool_version: tv,
bump: None,
latest: "".to_string(),
source,
}
}
fn display_current(&self) -> String {
if let Some(current) = &self.current {
current.clone()
} else {
"[MISSING]".to_string()
}
}
}
impl Display for OutdatedInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
if let Some(current) = &self.current {
write!(
f,
"{:<20} {:<10} -> {:<10} ({})",
self.name, current, self.latest, self.source
)
} else {
write!(
f,
"{:<20} {:<10} -> {:<10} ({})",
self.name, "MISSING", self.latest, self.source
)
}
}
}
#[cfg(test)]
mod tests {
use crate::backend::reset;
use pretty_assertions::assert_eq;
use test_log::test;
use super::is_outdated_version;
use super::{check_semver_bump, is_outdated_version};
#[test]
fn test_is_outdated_version() {
@ -607,4 +761,21 @@ mod tests {
false
);
}
#[test]
fn test_check_semver_bump() {
crate::test::reset();
std::assert_eq!(check_semver_bump("20", "20.0.0"), None);
std::assert_eq!(check_semver_bump("20.0", "20.0.0"), None);
std::assert_eq!(check_semver_bump("20.0.0", "20.0.0"), None);
std::assert_eq!(check_semver_bump("20", "21.0.0"), Some("21".to_string()));
std::assert_eq!(
check_semver_bump("20.0", "20.1.0"),
Some("20.1".to_string())
);
std::assert_eq!(
check_semver_bump("20.0.0", "20.0.1"),
Some("20.0.1".to_string())
);
}
}

View File

@ -1,13 +1,13 @@
use serde::ser::{Serialize, SerializeStruct, Serializer};
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use indexmap::{indexmap, IndexMap};
use serde_derive::Serialize;
use crate::file::display_path;
/// where a tool version came from (e.g.: .tool-versions)
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone)]
pub enum ToolSource {
ToolVersions(PathBuf),
MiseToml(PathBuf),
@ -29,6 +29,15 @@ impl Display for ToolSource {
}
impl ToolSource {
pub fn path(&self) -> Option<&Path> {
match self {
ToolSource::ToolVersions(path) => Some(path),
ToolSource::MiseToml(path) => Some(path),
ToolSource::LegacyVersionFile(path) => Some(path),
_ => None,
}
}
pub fn as_json(&self) -> IndexMap<String, String> {
match self {
ToolSource::ToolVersions(path) => indexmap! {
@ -36,7 +45,7 @@ impl ToolSource {
"path".to_string() => path.to_string_lossy().to_string(),
},
ToolSource::MiseToml(path) => indexmap! {
"type".to_string() => ".mise.toml".to_string(),
"type".to_string() => "mise.toml".to_string(),
"path".to_string() => path.to_string_lossy().to_string(),
},
ToolSource::LegacyVersionFile(path) => indexmap! {
@ -55,6 +64,39 @@ impl ToolSource {
}
}
impl Serialize for ToolSource {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("ToolSource", 3)?;
match self {
ToolSource::ToolVersions(path) => {
s.serialize_field("type", ".tool-versions")?;
s.serialize_field("path", path)?;
}
ToolSource::MiseToml(path) => {
s.serialize_field("type", "mise.toml")?;
s.serialize_field("path", path)?;
}
ToolSource::LegacyVersionFile(path) => {
s.serialize_field("type", "legacy-version-file")?;
s.serialize_field("path", path)?;
}
ToolSource::Argument => {
s.serialize_field("type", "argument")?;
}
ToolSource::Environment(key, value) => {
s.serialize_field("type", "environment")?;
s.serialize_field("key", key)?;
s.serialize_field("value", value)?;
}
}
s.end()
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
@ -96,7 +138,7 @@ mod tests {
assert_eq!(
ts.as_json(),
indexmap! {
"type".to_string() => ".mise.toml".to_string(),
"type".to_string() => "mise.toml".to_string(),
"path".to_string() => "/home/user/.mise.toml".to_string(),
}
);