Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/backend/test_github
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ EOF
mise uninstall github:jdx/mise-test-fixtures
assert "mise install"
# Verify mise.lock is written correctly with checksums
assert_contains "cat mise.lock" '[tools."github:jdx/mise-test-fixtures"]'
assert_contains "cat mise.lock" '[[tools."github:jdx/mise-test-fixtures"]]'
assert_contains "cat mise.lock" 'version = "1.0.0"'
assert_contains "cat mise.lock" 'backend = "github:jdx/mise-test-fixtures"'
# Get the current platform key
Expand Down
2 changes: 1 addition & 1 deletion e2e/backend/test_github_url_tracking
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ assert_contains "mise x -- hello-world" "hello world"

# Verify URL is stored in lockfile
echo "=== Verifying URL is stored in lockfile ==="
assert_contains "cat mise.lock" '[tools."github:jdx/mise-test-fixtures"]'
assert_contains "cat mise.lock" '[[tools."github:jdx/mise-test-fixtures"]]'
assert_contains "cat mise.lock" 'version = "1.0.0"'
assert_contains "cat mise.lock" 'backend = "github:jdx/mise-test-fixtures[asset_pattern=hello-world-1.0.0.tar.gz,bin_path=hello-world-1.0.0/bin]"'
# Get the current platform key
Expand Down
2 changes: 1 addition & 1 deletion e2e/backend/test_gitlab
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ EOF

mise install
# Verify mise.lock is written correctly with checksums
assert_contains "cat mise.lock" '[tools."gitlab:jdxcode/mise-test-fixtures"]'
assert_contains "cat mise.lock" '[[tools."gitlab:jdxcode/mise-test-fixtures"]]'
assert_contains "cat mise.lock" 'version = "1.0.0"'
assert_contains "cat mise.lock" 'backend = "gitlab:jdxcode/mise-test-fixtures[asset_pattern=hello-world-1.0.0.tar.gz,bin_path=hello-world-1.0.0/bin]"'
# Get the current platform key
Expand Down
2 changes: 1 addition & 1 deletion e2e/backend/test_http
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ cat <<EOF >mise.toml
EOF

mise install
assert_contains "cat mise.lock" '[tools."http:hello-lock"]'
assert_contains "cat mise.lock" '[[tools."http:hello-lock"]]'
assert_contains "cat mise.lock" 'version = "1.0.0"'
assert_contains "cat mise.lock" 'backend = "http:hello-lock[url=https://mise.jdx.dev/test-fixtures/hello-world-1.0.0.tar.gz,bin_path=hello-world-1.0.0/bin]"'
assert_contains "cat mise.lock" 'platforms'
Expand Down
2 changes: 1 addition & 1 deletion e2e/core/test_java_url_tracking_slow
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ assert_contains "mise x java -- java -version 2>&1" 'openjdk version "17.0.2"'

# Verify URL is stored in lockfile
echo "=== Verifying URL is stored in lockfile ==="
assert_contains "cat mise.lock" '[tools.java]'
assert_contains "cat mise.lock" '[[tools.java]]'
assert_contains "cat mise.lock" 'version = "17.0.2"'
assert_contains "cat mise.lock" 'backend = "core:java"'
assert_contains "cat mise.lock" 'url ='
Expand Down
4 changes: 2 additions & 2 deletions e2e/lockfile/test_lockfile_assets
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ export MISE_EXPERIMENTAL=1

# Test creating a lockfile with platforms sections
cat <<EOF >mise.lock
[tools.tiny]
[[tools.tiny]]
version = "1.0.0"
backend = "asdf:tiny"

[tools.tiny.platforms.linux-x64]
checksum = "sha256:abc123"
size = 1024

[tools.dummy]
[[tools.dummy]]
version = "2.0.0"
backend = "core:dummy"

Expand Down
2 changes: 1 addition & 1 deletion e2e/lockfile/test_lockfile_backend
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ cat <<EOF >mise.toml
version = "1.0.0"
EOF
cat <<EOF >mise.lock
[tools.gh]
[[tools.gh]]
version = "1.0.0"
backend = "ubi:cli/cli[exe=gh]"
EOF
Expand Down
4 changes: 2 additions & 2 deletions e2e/lockfile/test_lockfile_install
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ esac
touch mise.lock
mise use tiny
cat <<EOF >mise.lock
[tools.tiny]
[[tools.tiny]]
version = "1.0.0"
EOF
rm -rf "$MISE_DATA_DIR/installs/tiny"
Expand All @@ -45,7 +45,7 @@ assert_contains "cat mise.lock" 'checksum = "sha256:'

# Test rewriting existing lockfile section with same content
cat <<EOF >mise.lock
[tools.gh]
[[tools.gh]]
version = "2.62.0"
backend = "aqua:cli/cli"

Expand Down
56 changes: 14 additions & 42 deletions src/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,31 +136,20 @@ impl Lockfile {
.try_into()?;

let mut lockfile = Lockfile::default();
let mut has_single_version_format = false;

for (short, value) in tools {
let versions = match value {
toml::Value::Array(arr) => arr
.into_iter()
.map(LockfileTool::try_from)
.collect::<Result<Vec<_>>>()?,
_ => {
// Single-Version format detected - will be auto-migrated
has_single_version_format = true;
trace!("Auto-migrating single-version format for tool: {}", short);
vec![LockfileTool::try_from(value)?]
}
_ => bail!(
"invalid lockfile format for tool {short}: expected array ([[tools.{short}]])"
),
Copy link

Choose a reason for hiding this comment

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

Bug: Old-format lockfiles silently lose locked version data

The refactoring rejects old single-version lockfile format with a bail!() error, but the callers of Lockfile::read catch this error via handle_missing_lockfile and silently return an empty lockfile. This means users with old-format lockfiles will have their locked versions silently discarded (with only a warning logged), and the lockfile will be overwritten with fresh version data. The intent to reject old format conflicts with the lenient error handling that treats parse failures the same as missing files.

Additional Locations (1)

Fix in Cursor Fix in Web

};
lockfile.tools.insert(short, versions);
}

if has_single_version_format {
debug!(
"Auto-migrated lockfile from single-version to multi-version format: {}",
path.display()
);
}

Ok(lockfile)
}

Expand Down Expand Up @@ -559,24 +548,8 @@ impl From<ToolVersionList> for Vec<LockfileTool> {
fn format(mut doc: DocumentMut) -> String {
if let Some(tools) = doc.get_mut("tools") {
for (_k, v) in tools.as_table_mut().unwrap().iter_mut() {
match v {
toml_edit::Item::ArrayOfTables(art) => {
for t in art.iter_mut() {
t.sort_values_by(|a, _, b, _| {
if a == "version" {
return std::cmp::Ordering::Less;
}
a.to_string().cmp(&b.to_string())
});
// Sort platforms section within each tool
if let Some(toml_edit::Item::Table(platforms_table)) =
t.get_mut("platforms")
{
platforms_table.sort_values();
}
}
}
toml_edit::Item::Table(t) => {
if let toml_edit::Item::ArrayOfTables(art) = v {
for t in art.iter_mut() {
t.sort_values_by(|a, _, b, _| {
Comment on lines +551 to 553
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The sorting logic for table values appears to be duplicated from the removed Item::Table branch. The indentation suggests this code block may not be properly nested within the if let statement. Please verify that the sorting logic (lines 514-524) is correctly indented and nested within the if let block to ensure it only applies to ArrayOfTables items.

Copilot uses AI. Check for mistakes.
if a == "version" {
return std::cmp::Ordering::Less;
Expand All @@ -588,7 +561,6 @@ fn format(mut doc: DocumentMut) -> String {
platforms_table.sort_values();
}
}
_ => {}
}
}
}
Expand All @@ -602,10 +574,10 @@ mod tests {
use std::collections::BTreeMap;

#[test]
fn test_multi_version_format_migration() {
// Test that single-version format is read correctly and writes as multi-version
let single_version_toml = r#"
[tools.node]
fn test_array_format_required() {
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

[nitpick] The test name 'test_array_format_required' doesn't clearly communicate what aspect of the array format is being tested. Consider renaming to 'test_lockfile_parses_array_format' or 'test_array_format_parsing' to better describe the test's purpose.

Suggested change
fn test_array_format_required() {
fn test_lockfile_parses_array_format() {

Copilot uses AI. Check for mistakes.
// Test that multi-version (array) format is read correctly
let multi_version_toml = r#"
[[tools.node]]
version = "20.10.0"
backend = "core:node"

Expand All @@ -614,7 +586,7 @@ version = "3.11.0"
backend = "core:python"
"#;

let table: toml::Table = toml::from_str(single_version_toml).unwrap();
let table: toml::Table = toml::from_str(multi_version_toml).unwrap();
let tools: toml::Table = table.get("tools").unwrap().clone().try_into().unwrap();

let mut lockfile = Lockfile::default();
Expand All @@ -625,7 +597,7 @@ backend = "core:python"
.map(LockfileTool::try_from)
.collect::<Result<Vec<_>>>()
.unwrap(),
_ => vec![LockfileTool::try_from(value).unwrap()],
_ => panic!("expected array format"),
};
lockfile.tools.insert(short, versions);
}
Expand All @@ -635,20 +607,20 @@ backend = "core:python"
assert!(lockfile.tools.contains_key("node"));
assert!(lockfile.tools.contains_key("python"));

// Verify node was migrated from single-version
// Verify node
let node_versions = &lockfile.tools["node"];
assert_eq!(node_versions.len(), 1);
assert_eq!(node_versions[0].version, "20.10.0");
assert_eq!(node_versions[0].backend, Some("core:node".to_string()));

// Verify python was already multi-version
// Verify python
let python_versions = &lockfile.tools["python"];
assert_eq!(python_versions.len(), 1);
assert_eq!(python_versions[0].version, "3.11.0");
}

#[test]
fn test_save_always_uses_multi_version_format() {
fn test_save_uses_array_format() {
let mut lockfile = Lockfile::default();
let mut platforms = BTreeMap::new();
platforms.insert(
Expand Down