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
7 changes: 5 additions & 2 deletions docs/cli/use.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ Supports absolute dates like "2024-06-01" and relative durations like "90d" or "

### `--pin`

Save exact version to config file
e.g.: `mise use --pin node@20` will save 20.0.0 as the version
Save the resolved concrete version to the config file

If the request exactly matches an available release, that release is preferred over
installed fuzzy matches. Use `prefix:` to explicitly request recursive prefix matching.
e.g.: `mise use --pin node@20` will save the resolved `20.x.y` version
Set `MISE_PIN=1` to make this the default behavior

Consider using mise.lock as a better alternative to pinning in mise.toml:
Expand Down
79 changes: 79 additions & 0 deletions e2e/cli/test_use_pin_exact
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash

# Pinning prefers an exact remote release over a more-specific installed match.
ln -s "$ROOT/test/data/plugins/tiny" "$MISE_DATA_DIR/plugins/tiny-pin"
mkdir -p "$MISE_DATA_DIR/installs/tiny-pin/1.0.0.1"
assert_contains \
"mise use --pin --dry-run --path pin.toml tiny-pin@1.0.0 2>&1" \
"tiny-pin@1.0.0 ⇢ would install"

# An absent exact release, normal fuzzy requests, and explicit prefixes retain
# the existing installed-version fast path.
assert_contains \
"mise use --pin --dry-run --path pin.toml tiny-pin@1.0 2>&1" \
"tiny-pin@1.0.0.1 ⇢ already installed"
assert_contains \
"mise use --dry-run --path pin.toml tiny-pin@1.0.0 2>&1" \
"tiny-pin@1.0.0.1 ⇢ already installed"
assert_contains \
"mise use --pin --dry-run --path pin.toml tiny-pin@prefix:1.0.0 2>&1" \
"tiny-pin@1.0.0.1 ⇢ already installed"

# Settings-based pinning uses the same behavior, while --fuzzy overrides it.
assert_contains \
"MISE_PIN=1 mise use --dry-run --path pin.toml tiny-pin@1.0.0 2>&1" \
"tiny-pin@1.0.0 ⇢ would install"
assert_contains \
"MISE_ASDF_COMPAT=1 mise use --dry-run --path pin.toml tiny-pin@1.0.0 2>&1" \
"tiny-pin@1.0.0 ⇢ would install"
assert_contains \
"MISE_PIN=1 mise use --fuzzy --dry-run --path pin.toml tiny-pin@1.0.0 2>&1" \
"tiny-pin@1.0.0.1 ⇢ already installed"

# Explicit offline mode cannot verify remote releases and keeps local resolution.
assert_contains \
"MISE_OFFLINE=1 mise use --pin --dry-run --path pin.toml tiny-pin@1.0.0 2>&1" \
"tiny-pin@1.0.0.1 ⇢ already installed"

mise use --pin --path pin.toml tiny-pin@1.0.0
assert "cat pin.toml" '[tools]
tiny-pin = "1.0.0"'

# An exact installed version does not need a remote lookup.
ln -s "$ROOT/test/data/plugins/tiny" "$MISE_DATA_DIR/plugins/tiny-pin-installed"
mkdir -p "$MISE_DATA_DIR/installs/tiny-pin-installed/1.0.0"
assert_contains \
"RTX_TINY_LIST_ALL_FAIL=1 mise use --pin --dry-run --path installed.toml tiny-pin-installed@1.0.0 2>&1" \
"tiny-pin-installed@1.0.0 ⇢ already installed"

# Online exact-match lookup failures must not silently pin a fuzzy installed version.
ln -s "$ROOT/test/data/plugins/tiny" "$MISE_DATA_DIR/plugins/tiny-pin-fail"
mkdir -p "$MISE_DATA_DIR/installs/tiny-pin-fail/1.0.0.1"
assert_fail \
"RTX_TINY_LIST_ALL_FAIL=1 mise use --pin --path pin-fail.toml tiny-pin-fail@1.0.0" \
"error running list-all"
assert_fail "test -f pin-fail.toml"

# Backends whose exact resolver only validates syntax must not treat a
# syntactically valid but unavailable release as proof that it exists.
NPM_BIN="$TMPDIR/npm-bin"
mkdir -p "$NPM_BIN" "$MISE_DATA_DIR/installs/npm-syntax-only/1.0.0+local.1"
cat >"$MISE_DATA_DIR/installs/npm-syntax-only/.mise.backend.toml" <<'EOF'
short = "npm:syntax-only"
full = "npm:syntax-only"
explicit_backend = true
EOF
cat >"$NPM_BIN/npm" <<'EOF'
#!/usr/bin/env bash
if [[ $1 == view && $3 == versions ]]; then
echo '{"versions":["1.0.1"],"time":{}}'
exit 0
fi
echo "unexpected npm invocation: $*" >&2
exit 1
EOF
chmod +x "$NPM_BIN/npm"

assert_contains \
"PATH='$NPM_BIN:$PATH' MISE_NPM_SHELL_OUT=1 mise use --pin --dry-run --path npm.toml npm:syntax-only@1.0.0 2>&1" \
"npm:syntax-only@1.0.0+local.1 ⇢ already installed"
7 changes: 5 additions & 2 deletions man/man1/mise.1
Original file line number Diff line number Diff line change
Expand Up @@ -4846,8 +4846,11 @@ Only install versions released before this date or older than this duration
Supports absolute dates like "2024\-06\-01" and relative durations like "90d" or "1y".
.TP
\fB\-\-pin\fR
Save exact version to config file
e.g.: `mise use \-\-pin node@20` will save 20.0.0 as the version
Save the resolved concrete version to the config file

If the request exactly matches an available release, that release is preferred over
installed fuzzy matches. Use `prefix:` to explicitly request recursive prefix matching.
e.g.: `mise use \-\-pin node@20` will save the resolved `20.x.y` version
Set `MISE_PIN=1` to make this the default behavior

Consider using mise.lock as a better alternative to pinning in mise.toml:
Expand Down
13 changes: 6 additions & 7 deletions mise.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -4764,14 +4764,13 @@ Supports absolute dates like "2024-06-01" and relative durations like "90d" or "
"""#
arg <MINIMUM_RELEASE_AGE>
}
flag --pin help=#"""
Save exact version to config file
e.g.: `mise use --pin node@20` will save 20.0.0 as the version
Set `MISE_PIN=1` to make this the default behavior
"""# {
flag --pin help="Save the resolved concrete version to the config file" {
long_help #"""
Save exact version to config file
e.g.: `mise use --pin node@20` will save 20.0.0 as the version
Save the resolved concrete version to the config file

If the request exactly matches an available release, that release is preferred over
installed fuzzy matches. Use `prefix:` to explicitly request recursive prefix matching.
e.g.: `mise use --pin node@20` will save the resolved `20.x.y` version
Set `MISE_PIN=1` to make this the default behavior

Consider using mise.lock as a better alternative to pinning in mise.toml:
Expand Down
1 change: 1 addition & 0 deletions src/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ impl Install {
resolve_options: ResolveOptions {
use_locked_version: true,
latest_versions: true,
prefer_exact_version: false,
before_date: self.get_before_date()?,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
Expand Down
2 changes: 2 additions & 0 deletions src/cli/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ impl Upgrade {
let opts = ResolveOptions {
use_locked_version: false,
latest_versions: true,
prefer_exact_version: false,
before_date,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
Expand Down Expand Up @@ -341,6 +342,7 @@ impl Upgrade {
resolve_options: ResolveOptions {
use_locked_version: false,
latest_versions: true,
prefer_exact_version: false,
before_date,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
Expand Down
11 changes: 7 additions & 4 deletions src/cli/use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ pub struct Use {
#[clap(long, alias = "before", verbatim_doc_comment)]
minimum_release_age: Option<String>,

/// Save exact version to config file
/// e.g.: `mise use --pin node@20` will save 20.0.0 as the version
/// Save the resolved concrete version to the config file
///
/// If the request exactly matches an available release, that release is preferred over
/// installed fuzzy matches. Use `prefix:` to explicitly request recursive prefix matching.
/// e.g.: `mise use --pin node@20` will save the resolved `20.x.y` version
/// Set `MISE_PIN=1` to make this the default behavior
///
/// Consider using mise.lock as a better alternative to pinning in mise.toml:
Expand Down Expand Up @@ -145,9 +148,11 @@ impl Use {
.build(&config)
.await?;
let cf = self.get_config_file().await?;
let pin = self.pin || !self.fuzzy && (Settings::get().pin || Settings::get().asdf_compat);
let mut resolve_options = ResolveOptions {
latest_versions: false,
use_locked_version: true,
prefer_exact_version: pin,
before_date: self.get_before_date()?,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
Expand Down Expand Up @@ -192,8 +197,6 @@ impl Use {
)
.await?;

let pin = self.pin || !self.fuzzy && (Settings::get().pin || Settings::get().asdf_compat);

for (ba, tvl) in &versions.iter().chunk_by(|tv| tv.ba()) {
let versions: Vec<_> = tvl
.into_iter()
Expand Down
59 changes: 47 additions & 12 deletions src/toolset/tool_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ impl ToolVersion {
let opts = ResolveOptions {
latest_versions: true,
use_locked_version: false,
prefer_exact_version: false,
before_date: base_opts.before_date,
before_date_from_default: base_opts.before_date_from_default,
filter_installed_versions_by_release_date: base_opts
Expand Down Expand Up @@ -492,15 +493,38 @@ impl ToolVersion {
// through to normal resolution, which still matches the literal
// channel name in the backend's version list as before.
}
if !opts.latest_versions {
let matches = backend.list_installed_versions_matching(&v);
let installed_matches =
(!opts.latest_versions).then(|| backend.list_installed_versions_matching(&v));
if installed_matches
.as_ref()
.is_some_and(|matches| matches.contains(&v))
{
return build(v);
}

let mut pin_remote_matches = None;
if opts.prefer_exact_version && !is_offline && !crate::semver::is_npm_semver_range_query(&v)
{
// Pinning must prove that the exact release exists before it
// bypasses an installed fuzzy match. Some backend exact resolvers
// only validate syntax and defer existence checks to installation.
let matches = backend
.list_versions_matching_with_opts(config, &v, None, opts.refresh_remote_versions)
.await?;
if matches.contains(&v) {
return build(v);
}
if !should_filter_installed_versions && let Some(v) = matches.last() {
return build(v.clone());
if opts.before_date.is_none() {
pin_remote_matches = Some(matches);
}
}
if !should_filter_installed_versions
&& let Some(v) = installed_matches
.as_ref()
.and_then(|matches| matches.last())
{
return build(v.clone());
}
if matches!(
request.source(),
ToolSource::IdiomaticVersionFile(path)
Expand Down Expand Up @@ -566,14 +590,19 @@ impl ToolVersion {
return build(v);
}
// First try with date filter (common case)
let matches = backend
.list_versions_matching_with_opts(
config,
&v,
opts.before_date,
opts.refresh_remote_versions,
)
.await?;
let matches = match pin_remote_matches {
Some(matches) => matches,
None => {
backend
.list_versions_matching_with_opts(
config,
&v,
opts.before_date,
opts.refresh_remote_versions,
)
.await?
}
};
if matches.contains(&v) {
return build(v);
}
Expand Down Expand Up @@ -773,6 +802,8 @@ impl Hash for ToolVersion {
pub struct ResolveOptions {
pub latest_versions: bool,
pub use_locked_version: bool,
/// Prefer an exact remote release over a fuzzy installed match.
pub prefer_exact_version: bool,
/// Only consider versions released before this timestamp
pub before_date: Option<Timestamp>,
/// `before_date` came from the built-in default release age rather than
Expand All @@ -799,6 +830,7 @@ impl Default for ResolveOptions {
Self {
latest_versions: false,
use_locked_version: true,
prefer_exact_version: false,
before_date: None,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
Expand Down Expand Up @@ -899,6 +931,9 @@ impl Display for ResolveOptions {
if self.use_locked_version {
opts.push("use_locked_version".to_string());
}
if self.prefer_exact_version {
opts.push("prefer_exact_version".to_string());
}
if let Some(ts) = &self.before_date {
if self.before_date_from_default {
opts.push(format!("before_date={ts} (default)"));
Expand Down