Skip to content

Commit 483868d

Browse files
committed
fix(backend): allow unresolved latest opt-in
1 parent aff9814 commit 483868d

3 files changed

Lines changed: 81 additions & 14 deletions

File tree

src/backend/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,20 @@ pub trait Backend: Debug + Send + Sync {
671671
async fn latest_stable_version(&self, _config: &Arc<Config>) -> eyre::Result<Option<String>> {
672672
Ok(None)
673673
}
674+
675+
/// Backend opt-in for installing an unresolved `latest` request.
676+
///
677+
/// Most backends must resolve `latest` to a concrete version before install.
678+
/// Override this only when the backend can pass an unresolved selector through
679+
/// to its installer, and only for requests where the selector is meaningful.
680+
///
681+
/// `ToolVersion::resolve_version` uses this as a last resort after normal
682+
/// latest resolution fails, and only when the backend's unfiltered remote
683+
/// version list is empty. If remote versions exist but are all filtered out by
684+
/// `minimum_release_age` / `--before`, this hook is not used.
685+
fn unresolved_latest_version(&self) -> Option<String> {
686+
None
687+
}
674688
fn list_installed_versions(&self) -> Vec<String> {
675689
install_state::list_versions(&self.ba().short)
676690
}

src/backend/pipx.rs

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::cmd::CmdLineRunner;
77
use crate::config::{Config, Settings};
88
use crate::env;
99
use crate::file;
10-
use crate::github;
10+
use crate::github::{self, GithubRelease};
1111
use crate::http::HTTP_FETCH;
1212
use crate::install_context::InstallContext;
1313
use crate::timeout;
@@ -122,20 +122,9 @@ impl Backend for PIPXBackend {
122122
PipxRequest::Git(url) if url.starts_with("https://github.com/") => {
123123
let repo = url.strip_prefix("https://github.com/").unwrap();
124124
let data = github::list_releases(repo).await?;
125-
Ok(data
126-
.into_iter()
127-
.rev()
128-
.map(|r| VersionInfo {
129-
version: r.tag_name,
130-
created_at: Some(r.created_at),
131-
..Default::default()
132-
})
133-
.collect())
125+
Ok(Self::versions_from_github_releases(data))
134126
}
135-
PipxRequest::Git { .. } => Ok(vec![VersionInfo {
136-
version: "latest".to_string(),
137-
..Default::default()
138-
}]),
127+
PipxRequest::Git { .. } => Ok(vec![]),
139128
}
140129
}
141130

@@ -195,6 +184,13 @@ impl Backend for PIPXBackend {
195184
.cloned()
196185
}
197186

187+
fn unresolved_latest_version(&self) -> Option<String> {
188+
match self.tool_name().parse() {
189+
Ok(PipxRequest::Git(_)) => Some("latest".to_string()),
190+
_ => None,
191+
}
192+
}
193+
198194
async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result<ToolVersion> {
199195
// Check if pipx is available (unless uvx is being used)
200196
let use_uvx = self.uv_is_installed(&ctx.config).await
@@ -292,6 +288,18 @@ pub fn install_time_option_keys() -> Vec<String> {
292288
}
293289

294290
impl PIPXBackend {
291+
fn versions_from_github_releases(releases: Vec<GithubRelease>) -> Vec<VersionInfo> {
292+
releases
293+
.into_iter()
294+
.rev()
295+
.map(|r| VersionInfo {
296+
version: r.tag_name,
297+
created_at: Some(r.created_at),
298+
..Default::default()
299+
})
300+
.collect()
301+
}
302+
295303
fn uv_exclude_newer_args(before_date: Option<Timestamp>) -> Vec<OsString> {
296304
match before_date {
297305
Some(before_date) => vec!["--exclude-newer".into(), before_date.to_string().into()],
@@ -666,9 +674,36 @@ fn fix_venv_python_symlink(_install_path: &Path, _pkg_name: &str) -> Result<()>
666674
#[cfg(test)]
667675
mod tests {
668676
use super::PIPXBackend;
677+
use crate::github::GithubRelease;
669678
use pretty_assertions::assert_eq;
670679
use std::ffi::OsString;
671680

681+
#[test]
682+
fn test_versions_from_empty_github_releases_stays_empty() {
683+
let versions = PIPXBackend::versions_from_github_releases(vec![]);
684+
685+
assert!(versions.is_empty());
686+
}
687+
688+
#[test]
689+
fn test_versions_from_github_releases_preserves_tags() {
690+
let versions = PIPXBackend::versions_from_github_releases(vec![
691+
github_release("2.0.0", "2024-02-01T00:00:00Z"),
692+
github_release("1.0.0", "2024-01-01T00:00:00Z"),
693+
]);
694+
695+
assert_eq!(
696+
versions
697+
.iter()
698+
.map(|v| (v.version.as_str(), v.created_at.as_deref()))
699+
.collect::<Vec<_>>(),
700+
vec![
701+
("1.0.0", Some("2024-01-01T00:00:00Z")),
702+
("2.0.0", Some("2024-02-01T00:00:00Z")),
703+
]
704+
);
705+
}
706+
672707
#[test]
673708
fn test_uv_exclude_newer_args_with_cutoff() {
674709
let before_date = "2024-01-02T03:04:05Z".parse().unwrap();
@@ -712,4 +747,14 @@ mod tests {
712747
Vec::<OsString>::new()
713748
);
714749
}
750+
751+
fn github_release(tag_name: &str, created_at: &str) -> GithubRelease {
752+
GithubRelease {
753+
tag_name: tag_name.to_string(),
754+
draft: false,
755+
prerelease: false,
756+
created_at: created_at.to_string(),
757+
assets: vec![],
758+
}
759+
}
715760
}

src/toolset/tool_version.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,14 @@ impl ToolVersion {
354354
{
355355
return build(v);
356356
}
357+
if !is_offline {
358+
let versions = backend.list_remote_versions(config).await?;
359+
if versions.is_empty()
360+
&& let Some(v) = backend.unresolved_latest_version()
361+
{
362+
return build(v);
363+
}
364+
}
357365
return Err(Self::no_versions_found(&backend, opts.before_date));
358366
}
359367
if !opts.latest_versions {

0 commit comments

Comments
 (0)