fix(link): linked versions override lockfile during resolution#8050
Conversation
When a tool has a user-linked version (created by `mise link`), skip lockfile resolution so the linked version takes priority. Previously, the lockfile would pin a remote version (e.g., 1.35.0) that doesn't exist locally, causing the linked tool to be reported as missing. Fixes #8049 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 resolves a critical issue where Highlights
Changelog
Activity
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
Ensures tools linked via mise link take precedence over lockfile-pinned versions during resolution, preventing linked tools from being incorrectly reported as missing.
Changes:
- Skip lockfile resolution when a linked tool version exists.
- Add
has_linked_version()to detect user-linked (absolute-target) symlinks in the installs directory. - Add an e2e regression test covering linked-version vs lockfile precedence.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/toolset/tool_version.rs | Adds linked-version detection and updates version resolution to prefer links over lockfile entries |
| e2e/cli/test_link_lockfile | Adds an e2e regression test validating linked versions override lockfile pins without “missing” warnings |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn has_linked_version(ba: &BackendArg) -> bool { | ||
| let installs_dir = &ba.installs_path; | ||
| let Ok(entries) = std::fs::read_dir(installs_dir) else { | ||
| return false; | ||
| }; | ||
| for entry in entries.flatten() { | ||
| let path = entry.path(); | ||
| if let Ok(Some(target)) = crate::file::resolve_symlink(&path) { | ||
| // Runtime symlinks start with "./" (e.g., latest -> ./1.35.0) | ||
| // User-linked symlinks point to absolute paths (e.g., brew -> /opt/homebrew/opt/hk) | ||
| if target.is_absolute() { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| false | ||
| } |
There was a problem hiding this comment.
has_linked_version() assumes crate::file::resolve_symlink(&path) returns the raw symlink target so that target.is_absolute() can distinguish user links from runtime ./... links. If resolve_symlink instead returns a resolved/canonicalized path (which the name strongly suggests), then runtime symlinks like latest -> ./1.35.0 will resolve to an absolute path and be misclassified as a user-linked version, changing resolution behavior broadly. Prefer using a helper that returns the direct read_link() value (unresolved), or rename/split helpers so this function explicitly uses the non-resolving variant.
| if opts.use_locked_version | ||
| && !has_linked_version(request.ba()) | ||
| && let Some(lt) = request.lockfile_resolve(config)? | ||
| { |
There was a problem hiding this comment.
has_linked_version() performs a filesystem read_dir scan of the installs directory and is now on the hot path for every resolution when use_locked_version is enabled. Consider caching the result per backend/tool for the duration of the process (or at least per ResolveOptions/resolution run) to avoid repeated directory scans during commands that resolve many tools.
|
|
||
| echo "=== Setup: install tiny and create a linked version ===" | ||
| rm -f mise.toml mise.lock | ||
| mise install tiny@3.1.0 |
There was a problem hiding this comment.
This test installs tiny@3.1.0 but does not uninstall it during cleanup. If the e2e suite shares a persistent MISE data directory across tests, that leftover install can cause cross-test state leakage. Consider uninstalling tiny@3.1.0 in cleanup as well (or run the test under an isolated temp data dir if that’s the suite convention).
|
|
||
| echo "=== Cleanup ===" | ||
| rm -rf mise.toml mise.lock tmp/tiny-brew | ||
| mise uninstall tiny@brew 2>/dev/null || true |
There was a problem hiding this comment.
This test installs tiny@3.1.0 but does not uninstall it during cleanup. If the e2e suite shares a persistent MISE data directory across tests, that leftover install can cause cross-test state leakage. Consider uninstalling tiny@3.1.0 in cleanup as well (or run the test under an isolated temp data dir if that’s the suite convention).
| mise uninstall tiny@brew 2>/dev/null || true | |
| mise uninstall tiny@brew 2>/dev/null || true | |
| mise uninstall tiny@3.1.0 2>/dev/null || true |
There was a problem hiding this comment.
Code Review
This pull request correctly implements the desired behavior of prioritizing linked tool versions over lockfile entries. The logic added to ToolVersion::resolve and the new has_linked_version function are sound. The new e2e test provides good coverage for the fix. I have one suggestion to refactor has_linked_version to be more idiomatic, but overall this is a great change.
| for entry in entries.flatten() { | ||
| let path = entry.path(); | ||
| if let Ok(Some(target)) = crate::file::resolve_symlink(&path) { | ||
| // Runtime symlinks start with "./" (e.g., latest -> ./1.35.0) | ||
| // User-linked symlinks point to absolute paths (e.g., brew -> /opt/homebrew/opt/hk) | ||
| if target.is_absolute() { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| false |
There was a problem hiding this comment.
This for loop can be refactored into a more concise and idiomatic iterator-based approach using any(). This improves readability by expressing the intent of checking if any entry satisfies the condition directly. The inline comment can be removed as its content is already covered by the function's docstring.
entries.flatten().any(|entry| {
if let Ok(Some(target)) = crate::file::resolve_symlink(&entry.path()) {
target.is_absolute()
} else {
false
}
})
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.6 x -- echo |
20.6 ± 0.4 | 19.9 | 22.1 | 1.00 |
mise x -- echo |
21.7 ± 0.9 | 20.7 | 30.6 | 1.05 ± 0.05 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.6 env |
20.4 ± 0.5 | 19.2 | 21.8 | 1.00 |
mise env |
20.5 ± 0.7 | 19.6 | 29.9 | 1.01 ± 0.04 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.6 hook-env |
20.7 ± 0.4 | 19.9 | 25.2 | 1.00 |
mise hook-env |
21.6 ± 0.6 | 20.6 | 23.9 | 1.05 ± 0.04 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.6 ls |
18.7 ± 0.3 | 18.1 | 20.1 | 1.00 |
mise ls |
19.5 ± 0.5 | 18.6 | 21.4 | 1.05 ± 0.03 |
xtasks/test/perf
| Command | mise-2026.2.6 | mise | Variance |
|---|---|---|---|
| install (cached) | 110ms | 119ms | -7% |
| ls (cached) | 70ms | 71ms | -1% |
| bin-paths (cached) | 74ms | 75ms | -1% |
| task-ls (cached) | 533ms | 539ms | -1% |
### 🚀 Features - **(shim)** add native .exe shim mode for Windows by @jdx in [#8045](#8045) ### 🐛 Bug Fixes - **(install)** preserve config options and registry defaults by @jdx in [#8044](#8044) - **(link)** linked versions override lockfile during resolution by @jdx in [#8050](#8050) - **(release)** preserve aqua-registry sections in changelog across releases by @jdx in [#8047](#8047) - ls --all-sources shows duplicate entries by @roele in [#8042](#8042) ### 📚 Documentation - replace "inherit" terminology with config layering by @jdx in [#8046](#8046) ### 📦 Registry - switch oxlint to npm backend by default by @risu729 in [#8038](#8038) - add orval (npm:orval) by @zdunecki in [#8051](#8051) ### New Contributors - @zdunecki made their first contribution in [#8051](#8051)
) ## Summary - Linked tool versions (created by `mise link`) now always take priority over lockfile entries during version resolution - Previously, if a lockfile pinned a version (e.g., `hk@1.35.0`), it would override a linked version (e.g., `hk@brew`), causing the tool to be reported as missing - Added `has_linked_version()` check that detects user-created symlinks (absolute path targets) vs runtime symlinks (relative `./` targets) Fixes jdx#8049 ## Test plan - [x] Added e2e test `test_link_lockfile` that creates a linked tool version alongside a lockfile and verifies the linked version is used without "missing" warnings - [x] Existing `test_link` e2e test passes - [x] Existing `test_lock` e2e tests pass - [x] All 447 unit tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches core tool version resolution logic and changes precedence rules between linked installs and lockfiles, which could affect installs/listing behavior across tools. > > **Overview** > Linked tool versions created via `mise link` now **override lockfile resolution**: `ToolVersion::resolve` skips `lockfile_resolve` when a user-linked install exists (detected by scanning the installs dir for symlinks with absolute targets). > > Adds an e2e regression test `e2e/cli/test_link_lockfile` that sets up a linked `tiny@brew` alongside a lockfile pin, then asserts `mise ls` shows the linked version and does not report the lockfile version as missing. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fd2b4c6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
### 🚀 Features - **(shim)** add native .exe shim mode for Windows by @jdx in [jdx#8045](jdx#8045) ### 🐛 Bug Fixes - **(install)** preserve config options and registry defaults by @jdx in [jdx#8044](jdx#8044) - **(link)** linked versions override lockfile during resolution by @jdx in [jdx#8050](jdx#8050) - **(release)** preserve aqua-registry sections in changelog across releases by @jdx in [jdx#8047](jdx#8047) - ls --all-sources shows duplicate entries by @roele in [jdx#8042](jdx#8042) ### 📚 Documentation - replace "inherit" terminology with config layering by @jdx in [jdx#8046](jdx#8046) ### 📦 Registry - switch oxlint to npm backend by default by @risu729 in [jdx#8038](jdx#8038) - add orval (npm:orval) by @zdunecki in [jdx#8051](jdx#8051) ### New Contributors - @zdunecki made their first contribution in [jdx#8051](jdx#8051)
Summary
mise link) now always take priority over lockfile entries during version resolutionhk@1.35.0), it would override a linked version (e.g.,hk@brew), causing the tool to be reported as missinghas_linked_version()check that detects user-created symlinks (absolute path targets) vs runtime symlinks (relative./targets)Fixes #8049
Test plan
test_link_lockfilethat creates a linked tool version alongside a lockfile and verifies the linked version is used without "missing" warningstest_linke2e test passestest_locke2e tests pass🤖 Generated with Claude Code
Note
Medium Risk
Touches core tool version resolution logic and changes precedence rules between linked installs and lockfiles, which could affect installs/listing behavior across tools.
Overview
Linked tool versions created via
mise linknow override lockfile resolution:ToolVersion::resolveskipslockfile_resolvewhen a user-linked install exists (detected by scanning the installs dir for symlinks with absolute targets).Adds an e2e regression test
e2e/cli/test_link_lockfilethat sets up a linkedtiny@brewalongside a lockfile pin, then assertsmise lsshows the linked version and does not report the lockfile version as missing.Written by Cursor Bugbot for commit fd2b4c6. This will update automatically on new commits. Configure here.