fix(lockfile): prune orphan tool entries on mise lock#8265
Conversation
Summary of ChangesHello @mackwic, 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 an issue where Highlights
Changelog
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.
Code Review
This pull request introduces a mechanism to prune orphan tool entries from the lockfile during full mise lock runs. This ensures that mise.lock remains synchronized with the current configuration when tools are removed. The implementation includes a new retain_tools_by_short method in the Lockfile struct, logic in the lock command to trigger pruning when no tool filter is applied, and comprehensive tests.
One logic issue was identified: the pruning process is currently skipped if the resulting toolset is entirely empty (e.g., when all tools are removed from the configuration). This prevents the lockfile from being cleaned up or removed in such cases.
There was a problem hiding this comment.
Pull request overview
This PR adds functionality to automatically prune orphaned tool entries from lockfiles during full mise lock runs, ensuring lockfiles stay synchronized with the current configuration.
Changes:
- Added
Lockfile::retain_tools_by_short()method to prune tool entries not present in the current configuration - Modified
mise lockcommand to call the pruning logic only when no tool filter is specified - Added comprehensive unit and e2e tests for the pruning behavior
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/lockfile.rs | Added retain_tools_by_short() method with conda package cleanup, plus test helpers and unit tests |
| src/cli/lock.rs | Integrated pruning logic with should_prune_stale_lockfile_entries() check, added unit tests |
| e2e/lockfile/test_lockfile_prune_removed_tool | Added regression test verifying stale tool entries are removed during full lock |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
66e63c9 to
764cf97
Compare
jdx
left a comment
There was a problem hiding this comment.
This review was AI-generated.
Overall the core logic is sound — pruning stale lockfile entries on full mise lock runs is the right approach, and the decision to skip pruning on filtered runs (mise lock <tool>) is sensible.
A few observations:
1. Redundant should_prune_stale_lockfile_entries() check
should_prune_stale_lockfile_entries() is called both at the call site in the tools.is_empty() branch (line 85) and inside prune_stale_entries_if_needed. The guard is duplicated — the method already short-circuits if self.tool is not empty.
2. configured_backends includes shorts redundantly
In prune_stale_entries_if_needed:
let configured_backends: BTreeSet<String> = tools
.iter()
.flat_map(|(ba, _)| [ba.short.clone(), ba.full()])
.collect();configured_tools already contains all the shorts, and retain_tools_by_short_or_backend checks keep_shorts first. Having shorts in configured_backends is redundant — not a bug, just unnecessary.
3. Dry-run doesn't show pruning info
When --dry-run is used, the code skips past the prune call. Users won't see that stale entries would be pruned. Similarly in the tools.is_empty() + dry-run path, there's no message about what would happen.
4. The tools.is_empty() prune path is indirectly "remove everything"
When tools.is_empty() and pruning triggers, prune_stale_entries_if_needed builds empty configured_tools/configured_backends sets, so retain_tools_by_short_or_backend removes all entries. This is the intended behavior but reads indirectly — a comment noting this is "prune all since no tools are configured" would help readability.
What looks good:
retain_tools_by_short_or_backendchecking both short name and backend to preserve legacy lockfile entries is well-designed- Conda package cleanup after pruning is a nice touch
- Test coverage is thorough — unit tests for each scenario plus e2e covering replacement and full removal
764cf97 to
763e2a8
Compare
|
AI Generated Bug: silent lockfile deletion with misleading messageWhen all tools are removed and
The deletion happens silently and the message is misleading. Fix: let pruned_tools = self.prune_stale_entries_if_needed(&mut lockfile, &tools);
if !pruned_tools.is_empty() {
lockfile.write(&lockfile_path)?;
any_tools = true;
// + some user-visible message
}Minor:
|
ad196c2 to
8a44249
Compare
|
Thanks ! Addressed in Implemented all points:
Also did a small readability cleanup:
Tests updated:
|
e82fa65 to
65d39c2
Compare
jdx
left a comment
There was a problem hiding this comment.
Code Review (AI-generated)
Good feature — pruning stale lockfile entries when tools are removed from config is a sensible improvement. The approach of only pruning on unfiltered mise lock runs (not mise lock <tool>) is correct. Test coverage is thorough. A few issues worth addressing:
Issues
1. Duplicated prune logic — prune runs twice on non-empty tool sets
When tools is not empty, the code calls prune_stale_entries_if_needed after show_dry_run/before process_tools, but the prune also runs in the tools.is_empty() branch. For a normal mise lock with tools, the lockfile is read once for pruning, then read again for process_tools. The prune result from the first read is discarded because process_tools reads the lockfile fresh:
// dry_run path: prune happens on a NEW lockfile read
let lockfile = Lockfile::read(&lockfile_path)?;
let stale_tools = self.stale_entries_if_pruned(&lockfile, &tools);
// non-dry-run path: reads lockfile AGAIN
let mut lockfile = Lockfile::read(&lockfile_path)?;
self.prune_stale_entries_if_needed(&mut lockfile, &tools);The non-dry-run path does prune correctly since it mutates the same lockfile that gets written, but this reads the lockfile from disk twice unnecessarily. Consider consolidating to a single Lockfile::read call per pass.
2. did_lockfile_change is always true when tools exist, even if nothing changed
if tools.is_empty() {
// ...
continue;
}
did_lockfile_change = true; // Always true, even if dry_run or nothing actually changedPreviously any_tools had the same behavior so this isn't a regression, but the rename to did_lockfile_change implies semantic accuracy that isn't there. In dry-run mode the lockfile doesn't actually change. Consider keeping the original name any_tools or making the semantics accurate.
3. configured_tool_selectors is called twice per prune check
prune_stale_entries_if_needed calls stale_entries_if_pruned (which calls configured_tool_selectors), then calls configured_tool_selectors again:
fn prune_stale_entries_if_needed(&self, lockfile: &mut Lockfile, tools: &[...]) -> BTreeSet<String> {
let stale_tools = self.stale_entries_if_pruned(lockfile, tools); // calls configured_tool_selectors
if !stale_tools.is_empty() {
let (configured_tools, configured_backends) = self.configured_tool_selectors(tools); // again
lockfile.retain_tools_by_short_or_backend(&configured_tools, &configured_backends);
}
stale_tools
}Could compute the selectors once and pass them to both.
4. retain_tools_by_short_or_backend and stale_tool_shorts duplicate the same filter predicate
These two methods in lockfile.rs have near-identical keep/discard logic. Consider extracting a shared helper, or have retain call stale_tool_shorts to determine what to remove.
5. E2E test file naming
The new test file test_lockfile_prunes_orphaned_tools_and_removes_empty_lockfile is quite long. The PR body references test_lockfile_prune_removed_tool (a different name). Existing tests use shorter names like test_lock_platform_merge. Consider something shorter like test_lockfile_prune_stale.
Looks Good
- Legacy lockfile compatibility (matching by backend identifier, not just short name) is well thought out
- Conda package cleanup cascading from tool removal is correct
- Unit test coverage is comprehensive — short-name matching, backend-identifier matching, conda cleanup, empty toolset, filtered runs
- Dry-run preview message is a nice touch
This review was generated with the assistance of AI.
f4dc63c to
8891451
Compare
|
Thanks for the review. I addressed points 2–5:
For point (1), I did not change behavior because the current control flow does not prune twice in a single run. In if tools.is_empty() {
// Path A
...
continue;
}
if self.dry_run {
// Path B
...
continue;
}
// Path C (non-dry-run)
let mut lockfile = Lockfile::read(&lockfile_path)?;
self.prune_stale_entries_if_needed(&mut lockfile, &tools);
self.process_tools(..., &mut lockfile).await?;
lockfile.write(&lockfile_path)?;References: So Path A/B/C are mutually exclusive. We do not do "prune twice then discard" in one run. There is still a separate optimization opportunity (not a prune correctness issue): avoiding an extra lockfile read across platform detection and the mutable update path. I also ran targeted formatting/tests locally for the touched lock/prune paths. |
|
Hmmm there's no explicit mention about that.. Do you prefer I disclose the amount of AI-generated text in my work ? |
|
Thanks for the thorough responses and the updates — the changes look solid. The reworked naming ( Re: AI disclosure — no, you don't need to disclose that. There's no requirement for it on this project. Note: this review was AI-generated using Claude Code. |
### 🚀 Features - **(conda)** replace custom backend with rattler crates by @jdx in [#8325](#8325) - **(task)** enforce per-task timeout configuration by @tvararu in [#8250](#8250) - **(vsix)** added vsix archives to http backend by @sosumappu in [#8306](#8306) - add core dotnet plugin for .NET SDK management by @jdx in [#8326](#8326) ### 🐛 Bug Fixes - **(conda)** preserve conda_packages on locked install and fix temp file race by @jdx in [#8335](#8335) - **(conda)** deduplicate repodata records to fix solver error on Linux by @jdx in [#8337](#8337) - **(env)** include watch_files in fast-path early exit check by @jdx in [#8317](#8317) - **(env)** clear fish completions when setting/unsetting shell aliases by @jdx in [#8324](#8324) - **(lockfile)** prevent lockfile writes when --locked is set by @jdx in [#8308](#8308) - **(lockfile)** prune orphan tool entries on mise lock by @mackwic in [#8265](#8265) - **(lockfile)** error on contradictory locked=true + lockfile=false config by @jdx in [#8329](#8329) - **(regal)** Update package location by @charlieegan3 in [#8315](#8315) - **(release)** strip markdown heading prefix from communique release title by @jdx in [#8303](#8303) - **(schema)** enforce additionalProperties constraint for env by @adamliang0 in [#8328](#8328) ### 📚 Documentation - Remove incorrect oh-my-zsh plugin ordering comment by @bvosk in [#8323](#8323) - require AI disclosure on GitHub comments by @jdx in [#8330](#8330) ### 📦 Registry - add `oxfmt` by @taoufik07 in [#8316](#8316) ### New Contributors - @adamliang0 made their first contribution in [#8328](#8328) - @tvararu made their first contribution in [#8250](#8250) - @bvosk made their first contribution in [#8323](#8323) - @taoufik07 made their first contribution in [#8316](#8316) - @charlieegan3 made their first contribution in [#8315](#8315) - @sosumappu made their first contribution in [#8306](#8306) ## 📦 Aqua Registry Updates #### New Packages (3) - [`Tyrrrz/FFmpegBin`](https://github.com/Tyrrrz/FFmpegBin) - [`elixir-lang/expert`](https://github.com/elixir-lang/expert) - [`erikjuhani/basalt`](https://github.com/erikjuhani/basalt) #### Updated Packages (5) - [`caarlos0/fork-cleaner`](https://github.com/caarlos0/fork-cleaner) - [`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local) - [`jackchuka/mdschema`](https://github.com/jackchuka/mdschema) - [`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases) - [`peco/peco`](https://github.com/peco/peco)
## Summary
When tools are removed from config, `mise lock` could keep stale entries
in `mise.lock`.
This PR makes full lock runs prune orphan tool entries so lockfiles stay
aligned with current config.
Follow-up fixes were added after initial review to cover subtle lockfile
compatibility cases.
## Changes
- Prune stale lockfile tool entries during full `mise lock` runs (no
tool filter).
- Keep existing behavior for filtered runs (`mise lock <tool>`), which
do not prune other tools.
- Handle the “all tools removed” case:
- even when the resolved toolset is empty, full `mise lock` now
prunes/removes stale lockfile entries.
- Preserve compatibility with legacy lockfile keying:
- pruning now keeps entries by either configured tool short name or
backend identifier
- this avoids dropping legacy entries like `[[tools.jq]]` with `backend
= "aqua:jqlang/jq"`.
- Add `Lockfile::retain_tools_by_short_or_backend(...)` and run conda
package cleanup after prune.
- Add/extend regression e2e coverage:
- `e2e/lockfile/test_lockfile_prune_removed_tool`
- tool replacement prune
- remove-all-tools prune
- Keep `e2e/cli/test_lock_platform_merge` passing with legacy lockfile
shape.
## Why
Prevents orphan entries in `mise.lock`, keeps lockfiles
deterministic/config-aligned, and avoids regressions with existing
lockfile formats.
## Validation
- `cargo fmt --all --check`
- `cargo clippy -- -D warnings`
- `cargo clippy --all-features --all-targets -- -D warnings`
- `cargo test -q test_retain_tools_by_short_prunes_removed_tools`
- `cargo test -q
test_retain_tools_by_short_cleans_unreferenced_conda_packages`
- `cargo test -q
test_retain_tools_by_short_or_backend_preserves_legacy_keyed_entries`
- `cargo test -q
test_should_prune_stale_lockfile_entries_without_tool_filter`
- `cargo test -q
test_should_not_prune_stale_lockfile_entries_with_tool_filter`
- `cargo test -q
test_prune_stale_entries_with_empty_tools_prunes_all_entries`
- `cargo test -q
test_prune_stale_entries_with_filter_keeps_existing_entries`
- `cargo test -q
test_prune_stale_entries_preserves_legacy_keyed_backend_match`
- `./e2e/run_test e2e/lockfile/test_lockfile_prune_removed_tool`
- `./e2e/run_test e2e/cli/test_lock_platform_merge`
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
### 🚀 Features - **(conda)** replace custom backend with rattler crates by @jdx in [jdx#8325](jdx#8325) - **(task)** enforce per-task timeout configuration by @tvararu in [jdx#8250](jdx#8250) - **(vsix)** added vsix archives to http backend by @sosumappu in [jdx#8306](jdx#8306) - add core dotnet plugin for .NET SDK management by @jdx in [jdx#8326](jdx#8326) ### 🐛 Bug Fixes - **(conda)** preserve conda_packages on locked install and fix temp file race by @jdx in [jdx#8335](jdx#8335) - **(conda)** deduplicate repodata records to fix solver error on Linux by @jdx in [jdx#8337](jdx#8337) - **(env)** include watch_files in fast-path early exit check by @jdx in [jdx#8317](jdx#8317) - **(env)** clear fish completions when setting/unsetting shell aliases by @jdx in [jdx#8324](jdx#8324) - **(lockfile)** prevent lockfile writes when --locked is set by @jdx in [jdx#8308](jdx#8308) - **(lockfile)** prune orphan tool entries on mise lock by @mackwic in [jdx#8265](jdx#8265) - **(lockfile)** error on contradictory locked=true + lockfile=false config by @jdx in [jdx#8329](jdx#8329) - **(regal)** Update package location by @charlieegan3 in [jdx#8315](jdx#8315) - **(release)** strip markdown heading prefix from communique release title by @jdx in [jdx#8303](jdx#8303) - **(schema)** enforce additionalProperties constraint for env by @adamliang0 in [jdx#8328](jdx#8328) ### 📚 Documentation - Remove incorrect oh-my-zsh plugin ordering comment by @bvosk in [jdx#8323](jdx#8323) - require AI disclosure on GitHub comments by @jdx in [jdx#8330](jdx#8330) ### 📦 Registry - add `oxfmt` by @taoufik07 in [jdx#8316](jdx#8316) ### New Contributors - @adamliang0 made their first contribution in [jdx#8328](jdx#8328) - @tvararu made their first contribution in [jdx#8250](jdx#8250) - @bvosk made their first contribution in [jdx#8323](jdx#8323) - @taoufik07 made their first contribution in [jdx#8316](jdx#8316) - @charlieegan3 made their first contribution in [jdx#8315](jdx#8315) - @sosumappu made their first contribution in [jdx#8306](jdx#8306) ## 📦 Aqua Registry Updates #### New Packages (3) - [`Tyrrrz/FFmpegBin`](https://github.com/Tyrrrz/FFmpegBin) - [`elixir-lang/expert`](https://github.com/elixir-lang/expert) - [`erikjuhani/basalt`](https://github.com/erikjuhani/basalt) #### Updated Packages (5) - [`caarlos0/fork-cleaner`](https://github.com/caarlos0/fork-cleaner) - [`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local) - [`jackchuka/mdschema`](https://github.com/jackchuka/mdschema) - [`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases) - [`peco/peco`](https://github.com/peco/peco)
Summary
When tools are removed from config,
mise lockcould keep stale entries inmise.lock.This PR makes full lock runs prune orphan tool entries so lockfiles stay aligned with current config.
Follow-up fixes were added after initial review to cover subtle lockfile compatibility cases.
Changes
mise lockruns (no tool filter).mise lock <tool>), which do not prune other tools.mise locknow prunes/removes stale lockfile entries.[[tools.jq]]withbackend = "aqua:jqlang/jq".Lockfile::retain_tools_by_short_or_backend(...)and run conda package cleanup after prune.e2e/lockfile/test_lockfile_prune_removed_toole2e/cli/test_lock_platform_mergepassing with legacy lockfile shape.Why
Prevents orphan entries in
mise.lock, keeps lockfiles deterministic/config-aligned, and avoids regressions with existing lockfile formats.Validation
cargo fmt --all --checkcargo clippy -- -D warningscargo clippy --all-features --all-targets -- -D warningscargo test -q test_retain_tools_by_short_prunes_removed_toolscargo test -q test_retain_tools_by_short_cleans_unreferenced_conda_packagescargo test -q test_retain_tools_by_short_or_backend_preserves_legacy_keyed_entriescargo test -q test_should_prune_stale_lockfile_entries_without_tool_filtercargo test -q test_should_not_prune_stale_lockfile_entries_with_tool_filtercargo test -q test_prune_stale_entries_with_empty_tools_prunes_all_entriescargo test -q test_prune_stale_entries_with_filter_keeps_existing_entriescargo test -q test_prune_stale_entries_preserves_legacy_keyed_backend_match./e2e/run_test e2e/lockfile/test_lockfile_prune_removed_tool./e2e/run_test e2e/cli/test_lock_platform_merge