Skip to content

Commit

Permalink
new: Support calver (calendar versioning). (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj authored Jun 16, 2024
1 parent 803eda8 commit d3a11c2
Show file tree
Hide file tree
Showing 27 changed files with 1,918 additions and 785 deletions.
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

0 comments on commit d3a11c2

Please sign in to comment.