diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index e7601bc6d..7e9b1555c 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -17,6 +17,7 @@ concurrency: env: MISE_EXPERIMENTAL: 1 + NPM_CONFIG_FUND: false jobs: release-plz: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ade91ebf..800c64673 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c86bb08b8..023776e43 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,14 +17,8 @@ env: MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }} MISE_EXPERIMENTAL: 1 RUST_BACKTRACE: 1 - #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 }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_FUND: false permissions: pull-requests: write @@ -87,11 +81,6 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} token: ${{ secrets.RTX_GITHUB_BOT_TOKEN || github.token }} - - 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 - uses: actions-rust-lang/setup-rust-toolchain@v1 with: { toolchain: nightly, components: "rustfmt, clippy", rustflags: "" } @@ -116,7 +105,6 @@ jobs: push: true author_name: mise[bot] author_email: 123107610+mise-en-dev@users.noreply.github.com - - run: ${SCCACHE_PATH} --show-stats coverage: name: coverage-${{matrix.tranche}} @@ -131,11 +119,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - 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 - uses: Swatinem/rust-cache@v2 with: shared-key: coverage @@ -179,7 +162,6 @@ jobs: name: coverage-${{matrix.tranche}}.lcov path: coverage-${{matrix.tranche}}.lcov if-no-files-found: error - - run: ${SCCACHE_PATH} --show-stats coverage-report: name: coverage-report runs-on: ubuntu-latest diff --git a/Cross.toml b/Cross.toml index 8377b1098..96e6a33ac 100644 --- a/Cross.toml +++ b/Cross.toml @@ -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", ] diff --git a/docs/cli/index.md b/docs/cli/index.md index 782497039..3d18d87d3 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -738,9 +738,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 @@ -1757,14 +1768,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 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 diff --git a/docs/cli/outdated.md b/docs/cli/outdated.md index 3d88252ee..f96bf106a 100644 --- a/docs/cli/outdated.md +++ b/docs/cli/outdated.md @@ -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 diff --git a/docs/cli/upgrade.md b/docs/cli/upgrade.md index c6fdd53b3..207b4112b 100644 --- a/docs/cli/upgrade.md +++ b/docs/cli/upgrade.md @@ -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 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 diff --git a/e2e/cli/test_upgrade b/e2e/cli/test_upgrade index 1bfe6ad46..b763cbd47 100644 --- a/e2e/cli/test_upgrade +++ b/e2e/cli/test_upgrade @@ -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" diff --git a/e2e/plugins/test_tiny b/e2e/plugins/test_tiny index 8363bf2eb..ddab7c563 100644 --- a/e2e/plugins/test_tiny +++ b/e2e/plugins/test_tiny @@ -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" "" diff --git a/mise.usage.kdl b/mise.usage.kdl index 5bf59f21d..dfe008682 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -597,7 +597,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" { @@ -1214,10 +1218,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 "" } - 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 } diff --git a/scripts/build-tarball.sh b/scripts/build-tarball.sh index 60b2c5797..26ffc804d 100755 --- a/scripts/build-tarball.sh +++ b/scripts/build-tarball.sh @@ -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 diff --git a/scripts/sccache.sh b/scripts/sccache.sh deleted file mode 100755 index 7c4b03aa0..000000000 --- a/scripts/sccache.sh +++ /dev/null @@ -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 "${@}" diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 2abea7283..830a34d4b 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -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()); diff --git a/src/cli/outdated.rs b/src/cli/outdated.rs index 07a4cc256..f442af166 100644 --- a/src/cli/outdated.rs +++ b/src/cli/outdated.rs @@ -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, + /// 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::>(); 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::>(); - let requests = outdated - .iter() - .map(|(_, tv, _)| tv.request.version()) - .collect::>(); - let currents = outdated - .iter() - .map(|(t, tv, _)| { - if t.is_version_installed(tv, true) { - tv.version.clone() - } else { - "MISSING".to_string() - } - }) - .collect::>(); - let latests = outdated - .iter() - .map(|(_, _, c)| c.clone()) - .collect::>(); - 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(¤ts[i]), - latests[i] - ); - } + fn display(&self, outdated: Vec) -> 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) -> 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, ToolVersion, String)>; - static AFTER_LONG_HELP: &str = color_print::cstr!( r#"Examples: @@ -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"); + } } diff --git a/src/cli/snapshots/mise__cli__deactivate__tests__deactivate.snap b/src/cli/snapshots/mise__cli__deactivate__tests__deactivate.snap index 2c376f225..2613f7230 100644 --- a/src/cli/snapshots/mise__cli__deactivate__tests__deactivate.snap +++ b/src/cli/snapshots/mise__cli__deactivate__tests__deactivate.snap @@ -4,4 +4,3 @@ expression: err --- mise is not activated in this shell session. Please run `mise activate` first in your shell rc file. - diff --git a/src/cli/snapshots/mise__cli__outdated__tests__outdated_json.snap b/src/cli/snapshots/mise__cli__outdated__tests__outdated_json.snap index bb8555e51..0d01a665d 100644 --- a/src/cli/snapshots/mise__cli__outdated__tests__outdated_json.snap +++ b/src/cli/snapshots/mise__cli__outdated__tests__outdated_json.snap @@ -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" + } } } diff --git a/src/cli/snapshots/mise__cli__outdated__tests__outdated_json_bump.snap b/src/cli/snapshots/mise__cli__outdated__tests__outdated_json_bump.snap new file mode 100644 index 000000000..13f4f7e45 --- /dev/null +++ b/src/cli/snapshots/mise__cli__outdated__tests__outdated_json_bump.snap @@ -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" + } + } +} diff --git a/src/cli/snapshots/mise__cli__set__tests__env_vars-3.snap b/src/cli/snapshots/mise__cli__set__tests__env_vars-3.snap index 28d4b0ee1..7f6f9e6d3 100644 --- a/src/cli/snapshots/mise__cli__set__tests__env_vars-3.snap +++ b/src/cli/snapshots/mise__cli__set__tests__env_vars-3.snap @@ -4,4 +4,3 @@ expression: "file::read_to_string(cf_path).unwrap()" --- [env] FOO = "bar" - diff --git a/src/cli/snapshots/mise__cli__set__tests__env_vars-5.snap b/src/cli/snapshots/mise__cli__set__tests__env_vars-5.snap index 28d4b0ee1..7f6f9e6d3 100644 --- a/src/cli/snapshots/mise__cli__set__tests__env_vars-5.snap +++ b/src/cli/snapshots/mise__cli__set__tests__env_vars-5.snap @@ -4,4 +4,3 @@ expression: "file::read_to_string(cf_path).unwrap()" --- [env] FOO = "bar" - diff --git a/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-3.snap b/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-3.snap index 769565740..f8a01278a 100644 --- a/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-3.snap +++ b/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-3.snap @@ -3,4 +3,3 @@ source: src/cli/set.rs expression: "file::read_to_string(cf_path).unwrap()" --- [env] - diff --git a/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-6.snap b/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-6.snap index 769565740..f8a01278a 100644 --- a/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-6.snap +++ b/src/cli/snapshots/mise__cli__set__tests__env_vars_remove-6.snap @@ -3,4 +3,3 @@ source: src/cli/set.rs expression: "file::read_to_string(cf_path).unwrap()" --- [env] - diff --git a/src/cli/snapshots/mise__cli__shell__tests__shell.snap b/src/cli/snapshots/mise__cli__shell__tests__shell.snap index d524cdd86..36b167eea 100644 --- a/src/cli/snapshots/mise__cli__shell__tests__shell.snap +++ b/src/cli/snapshots/mise__cli__shell__tests__shell.snap @@ -4,4 +4,3 @@ expression: err --- mise is not activated in this shell session. Please run `mise activate` first in your shell rc file. - diff --git a/src/cli/snapshots/mise__cli__unset__tests__unset_remove-4.snap b/src/cli/snapshots/mise__cli__unset__tests__unset_remove-4.snap index e6e57a16b..bf7fd0080 100644 --- a/src/cli/snapshots/mise__cli__unset__tests__unset_remove-4.snap +++ b/src/cli/snapshots/mise__cli__unset__tests__unset_remove-4.snap @@ -3,4 +3,3 @@ source: src/cli/unset.rs expression: "file::read_to_string(cf_path).unwrap()" --- [env] - diff --git a/src/cli/snapshots/mise__cli__upgrade__tests__upgrade_bump-2.snap b/src/cli/snapshots/mise__cli__upgrade__tests__upgrade_bump-2.snap new file mode 100644 index 000000000..23c1cf15c --- /dev/null +++ b/src/cli/snapshots/mise__cli__upgrade__tests__upgrade_bump-2.snap @@ -0,0 +1,5 @@ +--- +source: src/cli/upgrade.rs +expression: output +--- + diff --git a/src/cli/snapshots/mise__cli__upgrade__tests__upgrade_bump.snap b/src/cli/snapshots/mise__cli__upgrade__tests__upgrade_bump.snap new file mode 100644 index 000000000..2ef22c6fd --- /dev/null +++ b/src/cli/snapshots/mise__cli__upgrade__tests__upgrade_bump.snap @@ -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 diff --git a/src/cli/upgrade.rs b/src/cli/upgrade.rs index 5787a3ffe..36842aabb 100644 --- a/src/cli/upgrade.rs +++ b/src/cli/upgrade.rs @@ -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, - /// 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::>(); - 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) -> 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::>(); 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::>(); 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, - 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> { + fn get_interactive_tool_set(&self, outdated: &Vec) -> Result> { 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, 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()); + } } diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap index a8e33405a..efa04aea7 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap @@ -2,4 +2,4 @@ source: src/config/config_file/mise_toml.rs expression: cf --- -~/cwd/.test.mise.toml: +~/cwd/.test.mise.toml: diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-2.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-2.snap index 147d72fb7..57f454937 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-2.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-2.snap @@ -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", diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap index c39cbcaf0..702328c9d 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap @@ -1,6 +1,6 @@ --- source: src/config/config_file/mise_toml.rs -expression: cf.env_entries() +expression: cf.env_entries().unwrap() --- [ Val( diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-2.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-2.snap index 5b6297bff..d096a3a03 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-2.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-2.snap @@ -4,4 +4,3 @@ expression: cf.dump().unwrap() --- [alias.node] 18 = "18.0.0" - diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-3.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-3.snap index a8e33405a..efa04aea7 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-3.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_alias-3.snap @@ -2,4 +2,4 @@ source: src/config/config_file/mise_toml.rs expression: cf --- -~/cwd/.test.mise.toml: +~/cwd/.test.mise.toml: diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin-3.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin-3.snap index 7f358280f..8ff740726 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin-3.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__remove_plugin-3.snap @@ -2,4 +2,4 @@ source: src/config/config_file/mise_toml.rs expression: cf --- -/tmp/.mise.toml: +/tmp/.mise.toml: diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions-2.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions-2.snap index a7e567290..805772222 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions-2.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__replace_versions-2.snap @@ -4,4 +4,3 @@ expression: cf.dump().unwrap() --- [tools] node = ["16.0.1", "18.0.1"] - diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__set_alias-2.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__set_alias-2.snap index a8e33405a..efa04aea7 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__set_alias-2.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__set_alias-2.snap @@ -2,4 +2,4 @@ source: src/config/config_file/mise_toml.rs expression: cf --- -~/cwd/.test.mise.toml: +~/cwd/.test.mise.toml: diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 68435c091..d5c79a51d 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -395,7 +395,7 @@ impl Backend for JavaPlugin { } fn fuzzy_match_filter(&self, versions: Vec, query: &str) -> eyre::Result> { - let escaped_query = regex::escape(query); + let escaped_query = regex::escape(query.trim_end_matches('-')); let query = if query == "latest" { "[0-9].*" } else { diff --git a/src/shell/snapshots/mise__shell__bash__tests__activate.snap b/src/shell/snapshots/mise__shell__bash__tests__activate.snap index ff812aa65..174a5f954 100644 --- a/src/shell/snapshots/mise__shell__bash__tests__activate.snap +++ b/src/shell/snapshots/mise__shell__bash__tests__activate.snap @@ -53,4 +53,3 @@ if [ -z "${_mise_cmd_not_found:-}" ]; then fi } fi - diff --git a/src/shell/snapshots/mise__shell__bash__tests__deactivate.snap b/src/shell/snapshots/mise__shell__bash__tests__deactivate.snap index b0703ede2..30dbff45f 100644 --- a/src/shell/snapshots/mise__shell__bash__tests__deactivate.snap +++ b/src/shell/snapshots/mise__shell__bash__tests__deactivate.snap @@ -7,4 +7,3 @@ PROMPT_COMMAND="${PROMPT_COMMAND//_mise_hook/}" unset _mise_hook unset mise unset MISE_SHELL - diff --git a/src/shell/snapshots/mise__shell__bash__tests__prepend_env.snap b/src/shell/snapshots/mise__shell__bash__tests__prepend_env.snap index 69b5f92c9..5238bebae 100644 --- a/src/shell/snapshots/mise__shell__bash__tests__prepend_env.snap +++ b/src/shell/snapshots/mise__shell__bash__tests__prepend_env.snap @@ -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" - diff --git a/src/shell/snapshots/mise__shell__bash__tests__set_env.snap b/src/shell/snapshots/mise__shell__bash__tests__set_env.snap index 486b79ca1..46fe3a6f5 100644 --- a/src/shell/snapshots/mise__shell__bash__tests__set_env.snap +++ b/src/shell/snapshots/mise__shell__bash__tests__set_env.snap @@ -3,4 +3,3 @@ source: src/shell/bash.rs expression: "Bash::default().set_env(\"FOO\", \"1\")" --- export FOO=1 - diff --git a/src/shell/snapshots/mise__shell__bash__tests__unset_env.snap b/src/shell/snapshots/mise__shell__bash__tests__unset_env.snap index a884ffb40..b65e646dd 100644 --- a/src/shell/snapshots/mise__shell__bash__tests__unset_env.snap +++ b/src/shell/snapshots/mise__shell__bash__tests__unset_env.snap @@ -3,4 +3,3 @@ source: src/shell/bash.rs expression: "Bash::default().unset_env(\"FOO\")" --- unset FOO - diff --git a/src/shell/snapshots/mise__shell__fish__tests__deactivate.snap b/src/shell/snapshots/mise__shell__fish__tests__deactivate.snap index ec3fb7a79..bfd8f2c2b 100644 --- a/src/shell/snapshots/mise__shell__fish__tests__deactivate.snap +++ b/src/shell/snapshots/mise__shell__fish__tests__deactivate.snap @@ -7,4 +7,3 @@ functions --erase __mise_env_eval_2 functions --erase __mise_cd_hook functions --erase mise set -e MISE_SHELL - diff --git a/src/shell/snapshots/mise__shell__fish__tests__prepend_env.snap b/src/shell/snapshots/mise__shell__fish__tests__prepend_env.snap index fe31ade26..55894fa49 100644 --- a/src/shell/snapshots/mise__shell__fish__tests__prepend_env.snap +++ b/src/shell/snapshots/mise__shell__fish__tests__prepend_env.snap @@ -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 - diff --git a/src/shell/snapshots/mise__shell__fish__tests__set_env.snap b/src/shell/snapshots/mise__shell__fish__tests__set_env.snap index 22a698114..91fa705a0 100644 --- a/src/shell/snapshots/mise__shell__fish__tests__set_env.snap +++ b/src/shell/snapshots/mise__shell__fish__tests__set_env.snap @@ -3,4 +3,3 @@ source: src/shell/fish.rs expression: "Fish::default().set_env(\"FOO\", \"1\")" --- set -gx FOO 1 - diff --git a/src/shell/snapshots/mise__shell__fish__tests__unset_env.snap b/src/shell/snapshots/mise__shell__fish__tests__unset_env.snap index e6aa5413d..4ca667054 100644 --- a/src/shell/snapshots/mise__shell__fish__tests__unset_env.snap +++ b/src/shell/snapshots/mise__shell__fish__tests__unset_env.snap @@ -3,4 +3,3 @@ source: src/shell/fish.rs expression: "Fish::default().unset_env(\"FOO\")" --- set -e FOO - diff --git a/src/shell/snapshots/mise__shell__nushell__tests__deactivate.snap b/src/shell/snapshots/mise__shell__nushell__tests__deactivate.snap index e45bdf6e9..330b460bb 100644 --- a/src/shell/snapshots/mise__shell__nushell__tests__deactivate.snap +++ b/src/shell/snapshots/mise__shell__nushell__tests__deactivate.snap @@ -3,4 +3,3 @@ source: src/shell/nushell.rs expression: replace_path(&deactivate) --- hide,MISE_SHELL, - diff --git a/src/shell/snapshots/mise__shell__nushell__tests__hook_init.snap b/src/shell/snapshots/mise__shell__nushell__tests__hook_init.snap index b62b4db1c..81f0736e6 100644 --- a/src/shell/snapshots/mise__shell__nushell__tests__hook_init.snap +++ b/src/shell/snapshots/mise__shell__nushell__tests__hook_init.snap @@ -53,5 +53,3 @@ def --env mise_hook [] { | parse vars | update-env } - - diff --git a/src/shell/snapshots/mise__shell__nushell__tests__prepend_env.snap b/src/shell/snapshots/mise__shell__nushell__tests__prepend_env.snap index ab7e2e5d3..0f233ac0d 100644 --- a/src/shell/snapshots/mise__shell__nushell__tests__prepend_env.snap +++ b/src/shell/snapshots/mise__shell__nushell__tests__prepend_env.snap @@ -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') - diff --git a/src/shell/snapshots/mise__shell__nushell__tests__set_env.snap b/src/shell/snapshots/mise__shell__nushell__tests__set_env.snap index dc75a2e0b..48b231c83 100644 --- a/src/shell/snapshots/mise__shell__nushell__tests__set_env.snap +++ b/src/shell/snapshots/mise__shell__nushell__tests__set_env.snap @@ -3,4 +3,3 @@ source: src/shell/nushell.rs expression: "Nushell::default().set_env(\"FOO\", \"1\")" --- set,FOO,1 - diff --git a/src/shell/snapshots/mise__shell__nushell__tests__unset_env.snap b/src/shell/snapshots/mise__shell__nushell__tests__unset_env.snap index 5c44a6aae..354b08723 100644 --- a/src/shell/snapshots/mise__shell__nushell__tests__unset_env.snap +++ b/src/shell/snapshots/mise__shell__nushell__tests__unset_env.snap @@ -3,4 +3,3 @@ source: src/shell/nushell.rs expression: "Nushell::default().unset_env(\"FOO\")" --- hide,FOO, - diff --git a/src/shell/snapshots/mise__shell__xonsh__tests__prepend_env.snap b/src/shell/snapshots/mise__shell__xonsh__tests__prepend_env.snap index 754013d80..2f1241421 100644 --- a/src/shell/snapshots/mise__shell__xonsh__tests__prepend_env.snap +++ b/src/shell/snapshots/mise__shell__xonsh__tests__prepend_env.snap @@ -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') - diff --git a/src/shell/snapshots/mise__shell__xonsh__tests__set_env.snap b/src/shell/snapshots/mise__shell__xonsh__tests__set_env.snap index 99a4fa213..10433b6fd 100644 --- a/src/shell/snapshots/mise__shell__xonsh__tests__set_env.snap +++ b/src/shell/snapshots/mise__shell__xonsh__tests__set_env.snap @@ -8,4 +8,3 @@ from xonsh.built_ins import XSH envx = XSH.env envx[ 'FOO'] = '1' environ['FOO'] = envx.get_detyped('FOO') - diff --git a/src/shell/snapshots/mise__shell__xonsh__tests__unset_env.snap b/src/shell/snapshots/mise__shell__xonsh__tests__unset_env.snap index 072d1d815..38e4c7d87 100644 --- a/src/shell/snapshots/mise__shell__xonsh__tests__unset_env.snap +++ b/src/shell/snapshots/mise__shell__xonsh__tests__unset_env.snap @@ -8,4 +8,3 @@ from xonsh.built_ins import XSH envx = XSH.env envx.pop( 'FOO',None) environ.pop('FOO',None) - diff --git a/src/shell/snapshots/mise__shell__zsh__tests__activate.snap b/src/shell/snapshots/mise__shell__zsh__tests__activate.snap index 2fc8d4e55..c24aa41b2 100644 --- a/src/shell/snapshots/mise__shell__zsh__tests__activate.snap +++ b/src/shell/snapshots/mise__shell__zsh__tests__activate.snap @@ -54,4 +54,3 @@ if [ -z "${_mise_cmd_not_found:-}" ]; then fi } fi - diff --git a/src/shell/snapshots/mise__shell__zsh__tests__deactivate.snap b/src/shell/snapshots/mise__shell__zsh__tests__deactivate.snap index 82ea88a51..65c266ee7 100644 --- a/src/shell/snapshots/mise__shell__zsh__tests__deactivate.snap +++ b/src/shell/snapshots/mise__shell__zsh__tests__deactivate.snap @@ -7,4 +7,3 @@ chpwd_functions=( ${chpwd_functions:#_mise_hook} ) unset -f _mise_hook unset -f mise unset MISE_SHELL - diff --git a/src/shell/snapshots/mise__shell__zsh__tests__prepend_env.snap b/src/shell/snapshots/mise__shell__zsh__tests__prepend_env.snap index f6bd89771..3254dbc10 100644 --- a/src/shell/snapshots/mise__shell__zsh__tests__prepend_env.snap +++ b/src/shell/snapshots/mise__shell__zsh__tests__prepend_env.snap @@ -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" - diff --git a/src/shell/snapshots/mise__shell__zsh__tests__set_env.snap b/src/shell/snapshots/mise__shell__zsh__tests__set_env.snap index 74d9e09f7..6f5bfc0c4 100644 --- a/src/shell/snapshots/mise__shell__zsh__tests__set_env.snap +++ b/src/shell/snapshots/mise__shell__zsh__tests__set_env.snap @@ -3,4 +3,3 @@ source: src/shell/zsh.rs expression: "Zsh::default().set_env(\"FOO\", \"1\")" --- export FOO=1 - diff --git a/src/shell/snapshots/mise__shell__zsh__tests__unset_env.snap b/src/shell/snapshots/mise__shell__zsh__tests__unset_env.snap index 3f333d497..f4eb72be0 100644 --- a/src/shell/snapshots/mise__shell__zsh__tests__unset_env.snap +++ b/src/shell/snapshots/mise__shell__zsh__tests__unset_env.snap @@ -3,4 +3,3 @@ source: src/shell/zsh.rs expression: "Zsh::default().unset_env(\"FOO\")" --- unset FOO - diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index 41258b70f..5810c9959 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -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 = 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, ToolVersion, String)> { + pub fn list_outdated_versions(&self, bump: bool) -> Vec { 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 { + 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::>() + .join("."), + new = new + .iter() + .map(|c| c.to_string()) + .collect::>() + .join("."), + ); + return None; + } + let bump = new.into_iter().take(old.len()).collect::>(); + if bump == old { + None + } else { + Some( + bump.iter() + .map(|c| c.to_string()) + .collect::>() + .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, + #[tabled(skip)] + pub bump: Option, + 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()) + ); + } } diff --git a/src/toolset/tool_source.rs b/src/toolset/tool_source.rs index 5642d66f7..1debee8f6 100644 --- a/src/toolset/tool_source.rs +++ b/src/toolset/tool_source.rs @@ -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 { 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(&self, serializer: S) -> Result + 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(), } );