fix(lockfile): fix multiple lockfile issues with version management#5907
fix(lockfile): fix multiple lockfile issues with version management#5907
Conversation
…tiple constraints When using multiple version constraints (e.g., ['latest', 'prefix:1.0', 'prefix:1.1']), the lockfile would lose track of versions during partial reinstalls. This fix ensures that when updating lockfiles, existing versions are merged with new versions instead of being replaced, maintaining all versions specified in the config. Fixes issue where lockfile format would change from [[tools.X]] array to [tools.X] single table and lose version tracking after reinstalling a removed version. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull Request Overview
This PR fixes a critical issue where lockfiles would lose track of tool versions when using multiple version constraints during partial reinstalls. The core problem was that when updating lockfiles, the function would replace all versions with just the ones being installed instead of merging them, causing version loss and lockfile format inconsistency.
- Implements version merging logic in
update_lockfilesfunction to preserve all versions during partial reinstalls - Maintains consistent lockfile format (array of tables
[[tools.X]]) when multiple versions exist - Adds comprehensive e2e test to verify the fix works correctly
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/lockfile.rs | Adds version merging logic to preserve all tool versions during lockfile updates |
| e2e/cli/test_multiple_version_constraints_lockfile | Comprehensive test case reproducing the issue and validating the fix |
| // Check if we have multiple versions in the config (array format) | ||
| if existing_tools.len() > 1 || lockfile_tools.len() > 1 { | ||
| // Create a map to merge versions by version string | ||
| let mut version_map: HashMap<String, LockfileTool> = HashMap::new(); |
There was a problem hiding this comment.
Consider pre-allocating the HashMap capacity based on the sum of existing_tools.len() and lockfile_tools.len() to avoid potential rehashing: HashMap::with_capacity(existing_tools.len() + lockfile_tools.len())
| let mut version_map: HashMap<String, LockfileTool> = HashMap::new(); | |
| let mut version_map: HashMap<String, LockfileTool> = | |
| HashMap::with_capacity(existing_tools.len() + lockfile_tools.len()); |
| let merged_tools: Vec<LockfileTool> = version_map | ||
| .into_values() | ||
| .sorted_by(|a, b| a.version.cmp(&b.version)) | ||
| .collect(); |
There was a problem hiding this comment.
The sorted_by operation here could be expensive for large numbers of versions. Consider using sort_unstable_by instead of sorted_by for better performance, as the order of equal elements doesn't matter for version strings.
| let merged_tools: Vec<LockfileTool> = version_map | |
| .into_values() | |
| .sorted_by(|a, b| a.version.cmp(&b.version)) | |
| .collect(); | |
| let mut merged_tools: Vec<LockfileTool> = version_map | |
| .into_values() | |
| .collect(); | |
| merged_tools.sort_unstable_by(|a, b| a.version.cmp(&b.version)); |
| assert_contains "cat mise.lock" 'version = "3.1.0"' | ||
|
|
||
| # Count how many versions are in the lockfile | ||
| lockfile_versions=$(grep -c "version =" mise.lock || echo 0) |
There was a problem hiding this comment.
The grep command could match unrelated 'version =' strings in comments or other contexts. Consider using a more specific pattern like grep -c '^version =' to match only lines that start with 'version =' to avoid false positives.
| lockfile_versions=$(grep -c "version =" mise.lock || echo 0) | |
| lockfile_versions=$(grep -c '^version =' mise.lock || echo 0) |
… BTreeMap - Extract version merging logic into `merge_tool_versions` and `should_merge_versions` helper functions - Replace HashMap with BTreeMap to maintain natural sorting without explicit sort calls - Improve code readability and maintainability - No functional changes, existing behavior preserved 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.8.6 x -- echo |
18.3 ± 0.3 | 17.8 | 22.4 | 1.00 |
mise x -- echo |
18.4 ± 0.2 | 17.9 | 19.4 | 1.00 ± 0.02 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.8.6 env |
17.8 ± 0.3 | 17.2 | 19.0 | 1.00 |
mise env |
17.8 ± 0.5 | 17.3 | 23.8 | 1.00 ± 0.03 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.8.6 hook-env |
17.4 ± 0.2 | 17.0 | 19.7 | 1.00 |
mise hook-env |
17.4 ± 0.2 | 16.9 | 18.4 | 1.00 ± 0.02 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.8.6 ls |
16.3 ± 0.2 | 15.9 | 17.6 | 1.00 |
mise ls |
16.4 ± 0.9 | 15.9 | 33.0 | 1.01 ± 0.06 |
xtasks/test/perf
| Command | mise-2025.8.6 | mise | Variance |
|---|---|---|---|
| install (cached) | 190ms | ✅ 127ms | +49% |
| ls (cached) | 81ms | 80ms | +1% |
| bin-paths (cached) | 65ms | 65ms | +0% |
| task-ls (cached) | 479ms | 480ms | +0% |
✅ Performance improvement: install cached is 49%
Fixes issue where lockfile would lose version tracking during partial reinstalls of tools with multiple version constraints. The problem was that new_versions would overwrite the complete toolset in tools_by_source, causing only the newly installed versions to be written to the lockfile. Changed the logic to merge new_versions with existing toolset versions instead of replacing them, ensuring the lockfile always reflects all versions specified in the config that are actually installed. Fixes #5906 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Updated the test to properly fail when the lockfile bug is detected, rather than just reporting it. This ensures the test validates the fix correctly - failing when the bug exists and passing when fixed.
While fixing the multiple version constraints issue, also improved how platform information is preserved when merging lockfile entries. The new logic: - Preserves existing platform info when new tools lack it - Merges platform data intelligently between old and new entries - Maintains all versions from config while avoiding accumulation Note: The failing CI tests are due to a pre-existing platform info timing issue where verify_checksum runs after update_lockfiles. This commit doesn't fix that issue but doesn't make it worse either.
When reinstalling only some tools (e.g., after removing one version), the lockfile update was replacing all versions with just the ones being installed. This fix merges new versions with existing ones instead of replacing them entirely. Also ensures platform information is preserved during lockfile updates by preferring fresh platform data from newly installed tools over existing lockfile data. Fixes: #5906
…versions When new_versions contains platform information from a fresh install, make sure to update existing tool versions with this information rather than ignoring it. This ensures platform data is preserved in lockfiles.
The lockfile update logic was incorrectly preserving versions that had been removed from the user's configuration. This occurred because existing lockfile entries not present in the current toolset were unconditionally added back, leading to stale versions and inconsistency between the lockfile and the active configuration. This fix removes the code that was adding back existing tools that weren't in the new toolset, ensuring the lockfile only contains versions that are currently specified in the configuration. Fixes the issue reported in discussions/5906 where multiple version constraints with lockfile caused incorrect version selection. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
The lockfile was incorrectly preserving both old and new versions after `mise up` upgraded a tool. When upgrading from version 1.0.0 to 1.1.0 with a single request like `tiny = "1"`, the lockfile would contain both versions instead of just the upgraded version. This fix ensures that when new versions are added (e.g., during upgrade), they replace existing versions with the same request rather than being added alongside them. This maintains correct lockfile state after upgrades while still supporting legitimate multiple version configurations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
…ol>` runs (#8599) ## Summary When running `mise lock <tool>` after a tool's resolved version changes, the old lockfile entry was preserved and a new one appended, resulting in duplicate `[[tools.<tool>]]` sections with different versions. For example, if `mise.toml` has `node = "24"` and `mise.lock` previously resolved to `24.13.0`, then after `24.14.0` becomes available and is installed, running `mise lock node` produces: ```toml [[tools.node]] version = "24.13.0" backend = "core:node" # ... platform entries ... [[tools.node]] version = "24.14.0" backend = "core:node" # ... platform entries ... ``` The expected behavior is that `mise lock node` should replace the stale `24.13.0` entry with the `24.14.0` entry, not append alongside it. ## Cause Two code locations interact to produce this bug: 1. **Filtered lock runs skip all pruning** ([`src/cli/lock.rs:168-176`](https://github.com/jdx/mise/blob/010812ac19e14101c9225221da534fd83a4e0060/src/cli/lock.rs#L168-L176)): `prune_stale_entries_if_needed()` returns early for filtered runs (`mise lock <tool>`) to avoid removing non-targeted tools. But this also prevents pruning old versions of the *targeted* tool itself. 2. **`set_platform_info` matches by `(version, options)`** ([`src/lockfile.rs:655-705`](https://github.com/jdx/mise/blob/010812ac19e14101c9225221da534fd83a4e0060/src/lockfile.rs#L655-L705)): a version change always creates a new entry rather than updating the existing one. ## Prior art - **#5907** fixed this same class of bug for the `update_lockfiles()` code path used by `mise install`/`mise upgrade`. The `mise lock` CLI uses a separate code path that was not covered. - **#8265** added pruning of stale entries to `mise lock`, but only for unfiltered runs and only by tool name (tools removed from config entirely), not by stale versions within a targeted tool. ## Fix - Add `Lockfile::retain_tool_versions(short, keep_versions)` that removes entries for a tool whose version is not in the given set. - Add `Lock::prune_stale_versions_for_targeted_tools()` that, for filtered runs only, collects the current resolved versions per tool and calls `retain_tool_versions` before processing. - The existing guard against pruning non-targeted tools remains intact — only versions of the targeted tool(s) are pruned. ## Tests - 4 unit tests covering: stale version pruned, current version preserved, non-targeted tools untouched, unfiltered runs skip this path. - 1 e2e test (`test_lockfile_lock_filtered_prune_stale_versions`) verifying the end-to-end scenario. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: jdx <216188+jdx@users.noreply.github.com>
Summary
This PR fixes multiple critical issues with lockfile version management, addressing problems with stale versions persisting after configuration changes and incorrect handling of version upgrades.
Problems Fixed
1. Stale versions persisting after removal from configuration
When versions were removed from the configuration, they would incorrectly remain in the lockfile, causing inconsistency between the configuration and lockfile state.
2. Version upgrades keeping both old and new versions
When
mise upupgraded a tool (e.g., from 1.0.0 to 1.1.0), the lockfile would incorrectly contain both versions instead of just the upgraded version.3. Platform information not being preserved
When updating existing tool versions, fresh platform information from new versions wasn't being properly merged.
4. Partial reinstalls losing other versions
When using multiple version constraints and partially reinstalling tools, the lockfile would lose track of non-reinstalled versions.
Solution
Completely refactored the lockfile update logic to:
Changes
update_lockfilesfunction: Now properly handles version replacement during upgrades while preserving multiple versions when legitimately specifiedtest_multiple_version_constraints_lockfilevalidates all fixed scenariostest_lockfile_useand other tests to work with the corrected behaviorTesting
test_lockfile_usetest which was exposing the upgrade bugmise upcorrectly replaces old versions with new onesFixes: #5906