fix(upgrade): respect tracked configs when pruning old versions#7926
fix(upgrade): respect tracked configs when pruning old versions#7926
Conversation
Summary of ChangesHello @jdx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances the Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR fixes mise upgrade to respect tool versions needed by tracked config files before uninstalling old versions, making its behavior consistent with mise prune. Previously, upgrading a tool in one directory could uninstall versions still required by other projects.
Changes:
- Added
get_versions_needed_by_tracked_configs()helper to collect versions from tracked configs - Modified version removal logic to skip uninstalling versions still needed by tracked configs
- Added debug logging when preserving versions needed by tracked configs
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/cli/upgrade.rs
Outdated
| async fn get_versions_needed_by_tracked_configs( | ||
| &self, | ||
| config: &Arc<Config>, | ||
| ) -> std::collections::HashSet<(String, String)> { |
There was a problem hiding this comment.
The return type uses the fully qualified std::collections::HashSet instead of importing HashSet at the module level. For consistency with the rest of the codebase and improved readability, consider adding use std::collections::HashSet; to the imports and using HashSet<(String, String)> here.
src/cli/upgrade.rs
Outdated
| let mut ts = Toolset::from(trs); | ||
| if ts.resolve(config).await.is_ok() { | ||
| for (_, tv) in ts.list_current_versions() { |
There was a problem hiding this comment.
The variable name 'ts' is ambiguous. Consider using a more descriptive name like 'toolset' to improve code clarity.
| let mut ts = Toolset::from(trs); | |
| if ts.resolve(config).await.is_ok() { | |
| for (_, tv) in ts.list_current_versions() { | |
| let mut toolset = Toolset::from(trs); | |
| if toolset.resolve(config).await.is_ok() { | |
| for (_, tv) in toolset.list_current_versions() { |
src/cli/upgrade.rs
Outdated
| for cf in tracked_configs.values() { | ||
| if let Ok(trs) = cf.to_tool_request_set() { |
There was a problem hiding this comment.
The variable name 'cf' is ambiguous. Consider using a more descriptive name like 'config_file' to improve code readability.
| for cf in tracked_configs.values() { | |
| if let Ok(trs) = cf.to_tool_request_set() { | |
| for config_file in tracked_configs.values() { | |
| if let Ok(trs) = config_file.to_tool_request_set() { |
There was a problem hiding this comment.
Code Review
This pull request addresses an issue where mise upgrade could uninstall tool versions that are still in use by other projects. The change introduces a check against tracked configuration files before uninstalling old versions, which aligns its behavior with mise prune. The implementation is sound and correctly solves the problem. My main feedback is to suggest a refactoring to centralize the new logic for determining needed versions, which is currently duplicated from the prune command. This will improve code maintainability.
src/cli/upgrade.rs
Outdated
| /// Get all tool versions that are needed by tracked config files. | ||
| /// This ensures we don't uninstall versions that other projects still need. | ||
| async fn get_versions_needed_by_tracked_configs( | ||
| &self, | ||
| config: &Arc<Config>, | ||
| ) -> std::collections::HashSet<(String, String)> { | ||
| let mut needed = std::collections::HashSet::new(); | ||
| match config.get_tracked_config_files().await { | ||
| Ok(tracked_configs) => { | ||
| for cf in tracked_configs.values() { | ||
| if let Ok(trs) = cf.to_tool_request_set() { | ||
| let mut ts = Toolset::from(trs); | ||
| if ts.resolve(config).await.is_ok() { | ||
| for (_, tv) in ts.list_current_versions() { | ||
| needed.insert((tv.ba().short.to_string(), tv.tv_pathname())); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Err(e) => { | ||
| debug!("Failed to get tracked config files: {}", e); | ||
| } | ||
| } | ||
| needed | ||
| } |
There was a problem hiding this comment.
This function is very useful and correctly implements the desired logic. However, its logic is almost identical to what's currently in src/cli/prune.rs for determining prunable tools. To avoid code duplication and improve maintainability, this logic should be extracted to a more central location.
I suggest moving this function to src/config/mod.rs as a free function that returns a Result. This would allow both upgrade and prune commands to use it, with each command handling the Result as appropriate for its context.
Here's how it could look in src/config/mod.rs:
// in src/config/mod.rs
pub async fn get_versions_needed_by_tracked_configs(
config: &Arc<Config>,
) -> eyre::Result<std::collections::HashSet<(String, String)>> {
let mut needed = std::collections::HashSet::new();
let tracked_configs = config.get_tracked_config_files().await?;
for cf in tracked_configs.values() {
let trs = cf.to_tool_request_set()?;
let mut ts = Toolset::from(trs);
ts.resolve(config).await?;
for (_, tv) in ts.list_current_versions() {
needed.insert((tv.ba().short.to_string(), tv.tv_pathname()));
}
}
Ok(needed)
}Then, in upgrade, you can call it and handle the error gracefully:
// in src/cli/upgrade.rs inside `upgrade` method
let versions_needed_by_tracked = crate::config::get_versions_needed_by_tracked_configs(config)
.await
.unwrap_or_else(|e| {
debug!("Failed to get needed versions from tracked configs: {e}");
Default::default()
});This change would centralize the logic and make the codebase cleaner. The prune command could then also be refactored to use this shared function.
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.12 x -- echo |
20.9 ± 0.2 | 20.4 | 23.1 | 1.00 |
mise x -- echo |
21.7 ± 0.3 | 21.0 | 23.4 | 1.04 ± 0.02 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.12 env |
20.4 ± 0.6 | 19.8 | 28.0 | 1.00 |
mise env |
21.3 ± 0.4 | 20.5 | 24.3 | 1.04 ± 0.04 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.12 hook-env |
21.2 ± 0.2 | 20.6 | 22.4 | 1.00 |
mise hook-env |
21.9 ± 0.6 | 21.2 | 30.7 | 1.03 ± 0.03 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.12 ls |
19.0 ± 0.2 | 18.5 | 20.3 | 1.00 |
mise ls |
19.8 ± 0.4 | 18.8 | 21.6 | 1.04 ± 0.03 |
xtasks/test/perf
| Command | mise-2026.1.12 | mise | Variance |
|---|---|---|---|
| install (cached) | 113ms | 114ms | +0% |
| ls (cached) | 73ms | 72ms | +1% |
| bin-paths (cached) | 75ms | 76ms | -1% |
| task-ls (cached) | 542ms | 541ms | +0% |
`mise upgrade` now checks tracked config files before uninstalling old versions, consistent with how `mise prune` behaves. If a version is still referenced by any tracked config, it will be kept instead of being uninstalled. This prevents the issue where upgrading a tool in one directory would uninstall versions that are still needed by projects in other directories. Fixes #7640 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix dry-run output to accurately reflect tracked config protection - Propagate errors from get_versions_needed_by_tracked_configs (no silent failures) - Extract shared helper function to toolset module to eliminate code duplication Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move the tracked config check to happen AFTER the upgrade and config reset. This ensures versions resolve correctly - when `.tool-versions` says `dummy 1`, it should resolve to the new version (1.1.0) after upgrade, not the old version (1.0.0). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
7edc1b2 to
87f1ac2
Compare
| pub async fn get_versions_needed_by_tracked_configs( | ||
| config: &Arc<Config>, | ||
| ) -> Result<std::collections::HashSet<(String, String)>> { | ||
| let mut needed = std::collections::HashSet::new(); |
There was a problem hiding this comment.
Inconsistent import style for HashSet
Low Severity
std::collections::HashSet is used with full path twice, while HashMap is imported at line 1. Other files in this module (helpers.rs, tool_request_set.rs, toolset_install.rs) import HashSet at the top. Add HashSet to the existing import on line 1 for consistency.
### 🚀 Features - **(edit)** add interactive config editor (`mise edit`) by @jdx in [#7930](#7930) - **(lockfile)** graduate lockfiles from experimental by @jdx in [#7929](#7929) - **(task)** add support for usage values in task confirm dialog by @roele in [#7924](#7924) - **(task)** improve source freshness checking with edge case handling by @jdx in [#7932](#7932) ### 🐛 Bug Fixes - **(activate)** preserve ordering of paths appended after mise activate by @jdx in [#7919](#7919) - **(install)** sort failed installations for deterministic error output by @jdx in [#7936](#7936) - **(lockfile)** preserve URL and prefer sha256 when merging platform info by @jdx in [#7923](#7923) - **(lockfile)** add atomic writes and cache invalidation by @jdx in [#7927](#7927) - **(templates)** use sha256 for hash filter instead of blake3 by @jdx in [#7925](#7925) - **(upgrade)** respect tracked configs when pruning old versions by @jdx in [#7926](#7926) ### 🚜 Refactor - **(progress)** migrate from indicatif to clx by @jdx in [#7928](#7928) ### 📚 Documentation - improve clarity on uvx and pipx dependencies by @ygormutti in [#7878](#7878) ### ⚡ Performance - **(install)** use Kahn's algorithm for dependency scheduling by @jdx in [#7933](#7933) - use Aho-Corasick for efficient redaction by @jdx in [#7931](#7931) ### 🧪 Testing - remove flaky test_http_version_list test by @jdx in [#7934](#7934) ### Chore - use github backend instead of ubi in mise.lock by @jdx in [#7922](#7922) ### New Contributors - @ygormutti made their first contribution in [#7878](#7878)
…7926) ## Summary - Make `mise upgrade` check tracked config files before uninstalling old versions - Consistent with how `mise prune` behaves ## Problem When running `mise upgrade` from one directory, it would uninstall old versions of tools without checking if those versions are still needed by projects in other directories (tracked configs). For example: 1. Global config has `tiny = "latest"` 2. Project A has `tiny = "3.0.0"` (tracked) 3. Running `mise upgrade tiny` from home would upgrade to 3.1.0 and **uninstall 3.0.0** 4. Project A can no longer use its pinned version ## Solution Added a helper function `get_versions_needed_by_tracked_configs()` that: 1. Gets all tracked config files 2. Resolves each toolset to get concrete versions 3. Returns a set of (tool_name, version_pathname) pairs Before uninstalling an old version, we now check if it's in this set and skip uninstalling if so. Fixes jdx#7640 ## Test plan - [x] Code compiles and passes linting - [ ] Manual testing with tracked configs 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes uninstallation behavior in `mise upgrade` and centralizes version-resolution logic across tracked configs; mistakes could leave unused versions installed or, worse, still remove a needed version if the tracked-config resolution misses cases. > > **Overview** > `mise upgrade` now checks all *tracked config files* before uninstalling an old tool version, and skips uninstall when that version is still needed by another tracked project. > > This behavior is shared with `mise prune` via a new helper, `get_versions_needed_by_tracked_configs()`, which resolves each tracked config’s toolset and returns the concrete versions to keep (identified by `(tool_short_name, tv_pathname)`). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 87f1ac2. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
### 🚀 Features - **(edit)** add interactive config editor (`mise edit`) by @jdx in [jdx#7930](jdx#7930) - **(lockfile)** graduate lockfiles from experimental by @jdx in [jdx#7929](jdx#7929) - **(task)** add support for usage values in task confirm dialog by @roele in [jdx#7924](jdx#7924) - **(task)** improve source freshness checking with edge case handling by @jdx in [jdx#7932](jdx#7932) ### 🐛 Bug Fixes - **(activate)** preserve ordering of paths appended after mise activate by @jdx in [jdx#7919](jdx#7919) - **(install)** sort failed installations for deterministic error output by @jdx in [jdx#7936](jdx#7936) - **(lockfile)** preserve URL and prefer sha256 when merging platform info by @jdx in [jdx#7923](jdx#7923) - **(lockfile)** add atomic writes and cache invalidation by @jdx in [jdx#7927](jdx#7927) - **(templates)** use sha256 for hash filter instead of blake3 by @jdx in [jdx#7925](jdx#7925) - **(upgrade)** respect tracked configs when pruning old versions by @jdx in [jdx#7926](jdx#7926) ### 🚜 Refactor - **(progress)** migrate from indicatif to clx by @jdx in [jdx#7928](jdx#7928) ### 📚 Documentation - improve clarity on uvx and pipx dependencies by @ygormutti in [jdx#7878](jdx#7878) ### ⚡ Performance - **(install)** use Kahn's algorithm for dependency scheduling by @jdx in [jdx#7933](jdx#7933) - use Aho-Corasick for efficient redaction by @jdx in [jdx#7931](jdx#7931) ### 🧪 Testing - remove flaky test_http_version_list test by @jdx in [jdx#7934](jdx#7934) ### Chore - use github backend instead of ubi in mise.lock by @jdx in [jdx#7922](jdx#7922) ### New Contributors - @ygormutti made their first contribution in [jdx#7878](jdx#7878)


Summary
mise upgradecheck tracked config files before uninstalling old versionsmise prunebehavesProblem
When running
mise upgradefrom one directory, it would uninstall old versions of tools without checking if those versions are still needed by projects in other directories (tracked configs).For example:
tiny = "latest"tiny = "3.0.0"(tracked)mise upgrade tinyfrom home would upgrade to 3.1.0 and uninstall 3.0.0Solution
Added a helper function
get_versions_needed_by_tracked_configs()that:Before uninstalling an old version, we now check if it's in this set and skip uninstalling if so.
Fixes #7640
Test plan
🤖 Generated with Claude Code
Note
Medium Risk
Changes uninstallation behavior in
mise upgradeand centralizes version-resolution logic across tracked configs; mistakes could leave unused versions installed or, worse, still remove a needed version if the tracked-config resolution misses cases.Overview
mise upgradenow checks all tracked config files before uninstalling an old tool version, and skips uninstall when that version is still needed by another tracked project.This behavior is shared with
mise prunevia a new helper,get_versions_needed_by_tracked_configs(), which resolves each tracked config’s toolset and returns the concrete versions to keep (identified by(tool_short_name, tv_pathname)).Written by Cursor Bugbot for commit 87f1ac2. This will update automatically on new commits. Configure here.