Skip to content

fix(lockfile): fix multiple lockfile issues with version management#5907

Merged
jdx merged 11 commits intomainfrom
fix/lockfile-multiple-versions
Aug 6, 2025
Merged

fix(lockfile): fix multiple lockfile issues with version management#5907
jdx merged 11 commits intomainfrom
fix/lockfile-multiple-versions

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Aug 4, 2025

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 up upgraded 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:

  1. Properly replace old versions with upgraded ones when requests match
  2. Preserve legitimate multiple versions when specified in configuration
  3. Remove stale versions that no longer match any configuration request
  4. Correctly merge platform information during updates
  5. Use BTreeMap for consistent ordering in lockfile output

Changes

  1. Refactored update_lockfiles function: Now properly handles version replacement during upgrades while preserving multiple versions when legitimately specified
  2. Added version request tracking: Tool versions now properly track their requests to enable correct merging behavior
  3. Fixed platform info preservation: Platform information is now correctly merged when updating existing versions
  4. Added comprehensive e2e test: test_multiple_version_constraints_lockfile validates all fixed scenarios
  5. Fixed existing tests: Updated test_lockfile_use and other tests to work with the corrected behavior

Testing

  • Added comprehensive e2e test covering multiple version scenarios
  • Fixed failing test_lockfile_use test which was exposing the upgrade bug
  • All existing lockfile tests pass
  • Manual testing confirms:
    • mise up correctly replaces old versions with new ones
    • Multiple version configurations still work correctly
    • Removed versions no longer persist in lockfile
    • Platform information is preserved during updates

Fixes: #5906

…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>
Copilot AI review requested due to automatic review settings August 4, 2025 09:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_lockfiles function 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

Comment thread src/lockfile.rs Outdated
// 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();
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())

Suggested change
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());

Copilot uses AI. Check for mistakes.
Comment thread src/lockfile.rs Outdated
Comment on lines +257 to +260
let merged_tools: Vec<LockfileTool> = version_map
.into_values()
.sorted_by(|a, b| a.version.cmp(&b.version))
.collect();
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
lockfile_versions=$(grep -c "version =" mise.lock || echo 0)
lockfile_versions=$(grep -c '^version =' mise.lock || echo 0)

Copilot uses AI. Check for mistakes.
… 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>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 4, 2025

Hyperfine Performance

mise x -- echo

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%

jdx and others added 3 commits August 4, 2025 05:13
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.
cursor[bot]

This comment was marked as outdated.

jdx and others added 6 commits August 4, 2025 08:44
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>
@jdx jdx changed the title fix(lockfile): preserve all versions when updating lockfiles with multiple constraints fix(lockfile): fix multiple lockfile issues with version management Aug 6, 2025
@jdx jdx enabled auto-merge (squash) August 6, 2025 11:53
@jdx jdx merged commit 8107ae7 into main Aug 6, 2025
19 checks passed
@jdx jdx deleted the fix/lockfile-multiple-versions branch August 6, 2025 11:53
jdx added a commit that referenced this pull request Apr 4, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants