Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Support calver (calendar versioning). #505

Merged
merged 17 commits into from
Jun 16, 2024
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@

## Unreleased

#### 💥 Breaking

- WASM API
- Changed `SyncManifestOutput` `versions` field to the `VersionSpec` type instead of `Version`.
- Changed `LoadVersionsOutput` `canary`, `latest`, and `aliases` fields to the `UnresolvedVersionSpec` type instead of `Version`.
- Changed `LoadVersionsOutput` `versions` fields to the `VersionSpec` type instead of `Version`.
- Renamed `VersionSpec::Version` to `VersionSpec::Semantic`. The inner `Version` must also be wrapped in a `SemVer` type.

#### 🚀 Updates

- Added experimental support for the [calver](https://calver.org) (calendar versioning) specification. For example: 2024-04, 2024-06-10, etc.
- There are some caveats to this approach. Please refer to the documentation.
- WASM API
- Added `VersionSpec::Calendar` and `UnresolvedVersionSpec::Calendar` variant types.

#### ⚙️ Internal

- Improved command execution. May see some slight performance gains.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions crates/cli/src/commands/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ pub async fn clean_tool(mut tool: Tool, now: u128, days: u8, yes: bool) -> miett
continue;
}

let version = VersionSpec::parse(&dir_name).map_err(|error| ProtoError::Semver {
version: dir_name,
error: Box::new(error),
})?;
let version =
VersionSpec::parse(&dir_name).map_err(|error| ProtoError::VersionSpec {
version: dir_name,
error: Box::new(error),
})?;

if !tool.inventory.manifest.versions.contains_key(&version) {
debug!(
Expand Down
5 changes: 4 additions & 1 deletion crates/cli/src/commands/outdated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ pub struct OutdatedItem {

fn get_in_major_range(spec: &UnresolvedVersionSpec) -> UnresolvedVersionSpec {
match spec {
UnresolvedVersionSpec::Version(version) => UnresolvedVersionSpec::Req(
UnresolvedVersionSpec::Calendar(version) => UnresolvedVersionSpec::Req(
VersionReq::parse(format!("~{}", version.major).as_str()).unwrap(),
),
UnresolvedVersionSpec::Semantic(version) => UnresolvedVersionSpec::Req(
VersionReq::parse(format!("~{}", version.major).as_str()).unwrap(),
),
_ => spec.clone(),
Expand Down
4 changes: 2 additions & 2 deletions crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ pub enum ProtoError {

#[diagnostic(code(proto::version::invalid))]
#[error("Invalid version or requirement {}.", .version.style(Style::Hash))]
Semver {
VersionSpec {
version: String,
#[source]
error: Box<semver::Error>,
error: Box<version_spec::SpecError>,
},

#[diagnostic(code(proto::shim::create_failed))]
Expand Down
11 changes: 7 additions & 4 deletions crates/core/src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,7 @@ impl Tool {
let mut entries = FxHashMap::default();
let mut installed = FxHashSet::default();

for version in versions {
let key = VersionSpec::Version(version);
for key in versions {
let value = manifest.versions.get(&key).cloned().unwrap_or_default();

installed.insert(key.clone());
Expand Down Expand Up @@ -415,7 +414,11 @@ impl Tool {
// If we have a fully qualified semantic version,
// exit early and assume the version is legitimate!
// Also canary is a special type that we can simply just use.
if short_circuit && matches!(initial_version, UnresolvedVersionSpec::Version(_))
if short_circuit
&& matches!(
initial_version,
UnresolvedVersionSpec::Calendar(_) | UnresolvedVersionSpec::Semantic(_)
)
|| matches!(initial_version, UnresolvedVersionSpec::Canary)
{
let version = initial_version.to_resolved_spec();
Expand Down Expand Up @@ -568,7 +571,7 @@ impl Tool {

result.version.unwrap()
} else {
UnresolvedVersionSpec::parse(&content).map_err(|error| ProtoError::Semver {
UnresolvedVersionSpec::parse(&content).map_err(|error| ProtoError::VersionSpec {
version: content,
error: Box::new(error),
})?
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/version_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ pub async fn detect_version(

return Ok(
UnresolvedVersionSpec::parse(&session_version).map_err(|error| {
ProtoError::Semver {
ProtoError::VersionSpec {
version: session_version,
error: Box::new(error),
}
Expand Down
70 changes: 29 additions & 41 deletions crates/core/src/version_resolver.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use crate::proto_config::ProtoToolConfig;
use crate::tool_manifest::ToolManifest;
use proto_pdk_api::LoadVersionsOutput;
use rustc_hash::FxHashSet;
use semver::{Version, VersionReq};
use semver::VersionReq;
use std::collections::BTreeMap;
use tracing::trace;
use version_spec::*;

#[derive(Default)]
pub struct VersionResolver<'tool> {
pub aliases: BTreeMap<String, UnresolvedVersionSpec>,
pub versions: Vec<Version>,
pub versions: Vec<VersionSpec>,

manifest: Option<&'tool ToolManifest>,
config: Option<&'tool ProtoToolConfig>,
Expand All @@ -21,26 +20,21 @@ impl<'tool> VersionResolver<'tool> {
let mut resolver = Self::default();
resolver.versions.extend(output.versions);

for (alias, version) in output.aliases {
resolver
.aliases
.insert(alias, UnresolvedVersionSpec::Version(version));
for (alias, spec) in output.aliases {
resolver.aliases.insert(alias, spec);
}

if let Some(latest) = output.latest {
resolver
.aliases
.insert("latest".into(), UnresolvedVersionSpec::Version(latest));
resolver.aliases.insert("latest".into(), latest);
}

// Sort from newest to oldest
resolver.versions.sort_by(|a, d| d.cmp(a));

if !resolver.aliases.contains_key("latest") && !resolver.versions.is_empty() {
resolver.aliases.insert(
"latest".into(),
UnresolvedVersionSpec::Version(resolver.versions[0].clone()),
);
resolver
.aliases
.insert("latest".into(), resolver.versions[0].to_unresolved_spec());
}

resolver
Expand Down Expand Up @@ -72,41 +66,32 @@ impl<'tool> VersionResolver<'tool> {
}
}

pub fn match_highest_version(req: &VersionReq, versions: &[&Version]) -> Option<VersionSpec> {
let mut highest_match: Option<Version> = None;
pub fn match_highest_version(req: &VersionReq, specs: &[&VersionSpec]) -> Option<VersionSpec> {
let mut highest_match: Option<VersionSpec> = None;

for version in versions {
if req.matches(version)
&& (highest_match.is_none() || highest_match.as_ref().is_some_and(|v| *version > v))
{
highest_match = Some((*version).clone());
for spec in specs {
if let Some(version) = spec.as_version() {
if req.matches(version)
&& (highest_match.is_none() || highest_match.as_ref().is_some_and(|v| *spec > v))
{
highest_match = Some((*spec).clone());
}
}
}

highest_match.map(VersionSpec::Version)
}

// Filter out aliases because they cannot be matched against
fn extract_installed_versions(installed: &FxHashSet<VersionSpec>) -> Vec<&Version> {
installed
.iter()
.filter_map(|item| match item {
VersionSpec::Version(v) => Some(v),
_ => None,
})
.collect()
highest_match
}

pub fn resolve_version(
candidate: &UnresolvedVersionSpec,
versions: &[Version],
versions: &[VersionSpec],
aliases: &BTreeMap<String, UnresolvedVersionSpec>,
manifest: Option<&ToolManifest>,
config: Option<&ProtoToolConfig>,
) -> Option<VersionSpec> {
let remote_versions = versions.iter().collect::<Vec<_>>();
let installed_versions = if let Some(manifest) = manifest {
extract_installed_versions(&manifest.installed_versions)
Vec::from_iter(&manifest.installed_versions)
} else {
vec![]
};
Expand Down Expand Up @@ -226,33 +211,36 @@ pub fn resolve_version(
"No match for range, trying others",
);
}
UnresolvedVersionSpec::Version(ver) => {
let version_string = ver.to_string();
// Calendar
// Semantic
_ => {
let version_string = candidate.to_string();
let resolved_spec = candidate.to_resolved_spec();

trace!(
version = &version_string,
"Found an explicit version, resolving further"
);

// Check locally installed versions first
if installed_versions.contains(&ver) {
if installed_versions.contains(&&resolved_spec) {
trace!(
version = &version_string,
"Resolved to locally installed version"
);

return Some(VersionSpec::Version(ver.to_owned()));
return Some(resolved_spec);
}

// Otherwise we'll need to download from remote
for version in versions {
if ver == version {
if &resolved_spec == version {
trace!(
version = &version_string,
"Resolved to remote available version"
);

return Some(VersionSpec::Version(ver.to_owned()));
return Some(resolved_spec);
}
}

Expand Down
36 changes: 18 additions & 18 deletions crates/core/tests/version_resolver_test.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
use proto_core::{
resolve_version, ProtoToolConfig, ToolManifest, UnresolvedVersionSpec, VersionSpec,
resolve_version, ProtoToolConfig, SemVer, ToolManifest, UnresolvedVersionSpec, VersionSpec,
};
use semver::Version;
use std::collections::BTreeMap;

mod version_resolver {
use super::*;

fn create_versions() -> Vec<Version> {
fn create_versions() -> Vec<VersionSpec> {
vec![
Version::new(1, 0, 0),
Version::new(1, 2, 3),
Version::new(1, 1, 1),
Version::new(1, 5, 9),
Version::new(1, 10, 5),
Version::new(4, 5, 6),
Version::new(7, 8, 9),
Version::new(8, 0, 0),
Version::new(10, 0, 0),
VersionSpec::Semantic(SemVer(Version::new(1, 0, 0))),
VersionSpec::Semantic(SemVer(Version::new(1, 2, 3))),
VersionSpec::Semantic(SemVer(Version::new(1, 1, 1))),
VersionSpec::Semantic(SemVer(Version::new(1, 5, 9))),
VersionSpec::Semantic(SemVer(Version::new(1, 10, 5))),
VersionSpec::Semantic(SemVer(Version::new(4, 5, 6))),
VersionSpec::Semantic(SemVer(Version::new(7, 8, 9))),
VersionSpec::Semantic(SemVer(Version::new(8, 0, 0))),
VersionSpec::Semantic(SemVer(Version::new(10, 0, 0))),
]
}

fn create_aliases() -> BTreeMap<String, UnresolvedVersionSpec> {
BTreeMap::from_iter([
(
"latest".into(),
UnresolvedVersionSpec::Version(Version::new(10, 0, 0)),
UnresolvedVersionSpec::Semantic(SemVer(Version::new(10, 0, 0))),
),
(
"stable".into(),
UnresolvedVersionSpec::Alias("latest".into()),
),
(
"no-version".into(),
UnresolvedVersionSpec::Version(Version::new(20, 0, 0)),
UnresolvedVersionSpec::Semantic(SemVer(Version::new(20, 0, 0))),
),
(
"no-alias".into(),
Expand All @@ -60,7 +60,7 @@ mod version_resolver {

config.aliases.insert(
"latest-manifest".into(),
UnresolvedVersionSpec::Version(Version::new(8, 0, 0)),
UnresolvedVersionSpec::Semantic(SemVer(Version::new(8, 0, 0))),
);
config.aliases.insert(
"stable-manifest".into(),
Expand Down Expand Up @@ -187,7 +187,7 @@ mod version_resolver {

assert_eq!(
resolve_version(
&UnresolvedVersionSpec::Version(Version::new(1, 10, 5)),
&UnresolvedVersionSpec::Semantic(SemVer(Version::new(1, 10, 5))),
&versions,
&aliases,
None,
Expand All @@ -199,7 +199,7 @@ mod version_resolver {

assert_eq!(
resolve_version(
&UnresolvedVersionSpec::Version(Version::new(8, 0, 0)),
&UnresolvedVersionSpec::Semantic(SemVer(Version::new(8, 0, 0))),
&versions,
&aliases,
None,
Expand All @@ -219,7 +219,7 @@ mod version_resolver {

assert_eq!(
resolve_version(
&UnresolvedVersionSpec::Version(Version::new(3, 0, 0)),
&UnresolvedVersionSpec::Semantic(SemVer(Version::new(3, 0, 0))),
&versions,
&aliases,
Some(&manifest),
Expand Down Expand Up @@ -341,7 +341,7 @@ mod version_resolver {
let aliases = create_aliases();

resolve_version(
&UnresolvedVersionSpec::Version(Version::new(20, 0, 0)),
&UnresolvedVersionSpec::Semantic(SemVer(Version::new(20, 0, 0))),
&versions,
&aliases,
None,
Expand Down
Loading