Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
70 changes: 45 additions & 25 deletions src/cli/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ use std::sync::Arc;

use crate::backend::platform_target::PlatformTarget;
use crate::config::Config;
use crate::config::config_file::config_root;
use crate::file::display_path;
use crate::lockfile::{Lockfile, PlatformInfo};
use crate::lockfile::{self, Lockfile, PlatformInfo};
use crate::platform::Platform;
use crate::toolset::Toolset;
use crate::ui::multi_progress_report::MultiProgressReport;
Expand Down Expand Up @@ -137,20 +136,26 @@ impl Lock {
}

fn get_lockfile_path(&self, config: &Config) -> PathBuf {
// Get config root from the first config file, or use current dir
let root = config
.config_files
.keys()
.next()
.map(|p| config_root::config_root(p))
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());

let lockfile_name = if self.local {
"mise.local.lock"
// Get lockfile path from the first config file
if let Some(config_path) = config.config_files.keys().next() {
let (lockfile_path, _) = lockfile::lockfile_path_for_config(config_path);
if self.local {
// Replace mise.lock with mise.local.lock
lockfile_path.with_file_name("mise.local.lock")
} else {
lockfile_path
}
} else {
"mise.lock"
};
root.join(lockfile_name)
// Fallback to current dir
let lockfile_name = if self.local {
"mise.local.lock"
} else {
"mise.lock"
};
std::env::current_dir()
.unwrap_or_default()
.join(lockfile_name)
}
}

fn determine_target_platforms(&self, lockfile_path: &PathBuf) -> Result<Vec<Platform>> {
Expand Down Expand Up @@ -183,24 +188,39 @@ impl Lock {
config: &Config,
ts: &Toolset,
) -> Vec<(crate::cli::args::BackendArg, crate::toolset::ToolVersion)> {
// Calculate target config_root (same logic as get_lockfile_path)
let target_root = config
// Calculate target lockfile directory (same logic as get_lockfile_path)
let target_lockfile_dir = config
.config_files
.keys()
.next()
.map(|p| config_root::config_root(p))
.map(|p| {
let (lockfile_path, _) = lockfile::lockfile_path_for_config(p);
lockfile_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_default()
})
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());

// Collect tools from config files in the target config_root only
// Collect tools from config files that share the same lockfile directory
let mut all_tools: Vec<_> = Vec::new();
let mut seen: BTreeSet<(String, String)> = BTreeSet::new();

// Helper to get lockfile directory for a config path
let get_lockfile_dir = |path: &std::path::Path| -> PathBuf {
let (lockfile_path, _) = lockfile::lockfile_path_for_config(path);
lockfile_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_default()
};

// First, get all tools from the resolved toolset (these are the "current" versions)
// but only if they come from a config file in the target config_root
// but only if they come from a config file with the same lockfile directory
for (backend, tv) in ts.list_current_versions() {
// Check if this tool's source is in the target config_root
// Check if this tool's source shares the same lockfile directory
if let Some(source_path) = tv.request.source().path()
&& config_root::config_root(source_path) != target_root
&& get_lockfile_dir(source_path) != target_lockfile_dir
{
continue;
}
Expand All @@ -210,10 +230,10 @@ impl Lock {
}
}

// Then, iterate config files in the target config_root to find tools that may have been overridden
// Then, iterate config files with the same lockfile directory to find tools that may have been overridden
for (path, cf) in config.config_files.iter() {
// Skip config files not in the target config_root
if config_root::config_root(path) != target_root {
// Skip config files that don't share the same lockfile directory
if get_lockfile_dir(path) != target_lockfile_dir {
continue;
}
if let Ok(trs) = cf.to_tool_request_set() {
Expand Down
78 changes: 54 additions & 24 deletions src/lockfile.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::config::config_file::config_root;
use crate::config::{Config, Settings};
use crate::env;
use crate::file;
Expand Down Expand Up @@ -244,6 +243,12 @@ impl Lockfile {

/// Determines the lockfile path for a given config file path
/// Returns (lockfile_path, is_local)
///
/// Lockfiles are placed alongside their config files:
/// - `mise.toml` -> `mise.lock`
/// - `.config/mise.toml` -> `.config/mise.lock`
/// - `.mise/config.toml` -> `.mise/mise.lock`
/// - `.mise/conf.d/foo.toml` -> `.mise/mise.lock` (conf.d files share parent's lockfile)
pub fn lockfile_path_for_config(config_path: &Path) -> (PathBuf, bool) {
let is_local = is_local_config(config_path);
let lockfile_name = if is_local {
Expand All @@ -252,30 +257,20 @@ pub fn lockfile_path_for_config(config_path: &Path) -> (PathBuf, bool) {
"mise.lock"
};

// Fast path: for simple project configs (mise.toml, mise.local.toml, etc.)
// just use the parent directory. This avoids the expensive config_root call.
if let Some(parent) = config_path.parent() {
let filename = config_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
let parent_name = parent
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();

// If the config is directly in a project dir (not in .mise, .config, etc.)
// we can skip the full config_root calculation
if !matches!(parent_name, ".mise" | "mise" | ".config" | "conf.d")
&& (filename.starts_with("mise.") || filename.starts_with(".mise."))
{
return (parent.join(lockfile_name), is_local);
}
}
let parent = config_path.parent().unwrap_or(Path::new("."));
let parent_name = parent
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();

// Full path calculation for complex cases (.mise/, .config/mise/, etc.)
let root = config_root::config_root(config_path);
(root.join(lockfile_name), is_local)
// For conf.d files, place lockfile at parent of conf.d so all conf.d files share one lockfile
let lockfile_dir = if parent_name == "conf.d" {
parent.parent().unwrap_or(parent)
} else {
parent
};

(lockfile_dir.join(lockfile_name), is_local)
}
Comment thread
cursor[bot] marked this conversation as resolved.

/// Checks if a config path is a "local" config (should go to mise.local.lock)
Expand Down Expand Up @@ -1123,4 +1118,39 @@ options = { exe = "rg" }
assert_eq!(lockfile.tools["ripgrep"][0].options.len(), 2);
assert_eq!(lockfile.tools["ripgrep"][1].options.len(), 1);
}

#[test]
fn test_lockfile_path_for_config() {
// Simple case: mise.toml in project root
let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/mise.toml"));
assert_eq!(path, PathBuf::from("/foo/bar/mise.lock"));
assert!(!is_local);

// Local config
let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/mise.local.toml"));
assert_eq!(path, PathBuf::from("/foo/bar/mise.local.lock"));
assert!(is_local);

// Config in .config directory
let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/.config/mise.toml"));
assert_eq!(path, PathBuf::from("/foo/bar/.config/mise.lock"));
assert!(!is_local);

// Config in .mise directory
let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/.mise/config.toml"));
assert_eq!(path, PathBuf::from("/foo/bar/.mise/mise.lock"));
assert!(!is_local);

// Config in conf.d directory - should go to parent of conf.d
let (path, is_local) =
lockfile_path_for_config(Path::new("/foo/bar/.mise/conf.d/foo.toml"));
assert_eq!(path, PathBuf::from("/foo/bar/.mise/mise.lock"));
assert!(!is_local);

// Config in .config/mise/conf.d directory
let (path, is_local) =
lockfile_path_for_config(Path::new("/foo/bar/.config/mise/conf.d/foo.toml"));
assert_eq!(path, PathBuf::from("/foo/bar/.config/mise/mise.lock"));
assert!(!is_local);
}
}
Loading