From 5e6744b489e6c2fc28dde158043410fd7dd389bd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 7 Nov 2023 16:26:49 -0600 Subject: [PATCH] feat(spec): Track source kind --- src/cargo/core/package_id_spec.rs | 122 +++++++++++++++++++++++++++- src/cargo/core/source_id.rs | 4 + src/doc/src/reference/pkgid-spec.md | 40 +++++---- tests/testsuite/pkgid.rs | 11 ++- 4 files changed, 152 insertions(+), 25 deletions(-) diff --git a/src/cargo/core/package_id_spec.rs b/src/cargo/core/package_id_spec.rs index 122c40f1976e..d3ea238dfbd1 100644 --- a/src/cargo/core/package_id_spec.rs +++ b/src/cargo/core/package_id_spec.rs @@ -6,6 +6,7 @@ use semver::Version; use serde::{de, ser}; use url::Url; +use crate::core::GitReference; use crate::core::PackageId; use crate::core::SourceKind; use crate::util::edit_distance; @@ -104,17 +105,47 @@ impl PackageIdSpec { name: String::from(package_id.name().as_str()), version: Some(package_id.version().clone().into()), url: Some(package_id.source_id().url().clone()), - kind: None, + kind: Some(package_id.source_id().kind().clone()), } } /// Tries to convert a valid `Url` to a `PackageIdSpec`. fn from_url(mut url: Url) -> CargoResult { + let mut kind = None; + if let Some((kind_str, scheme)) = url.scheme().split_once('+') { + match kind_str { + "git" => { + let git_ref = GitReference::DefaultBranch; + kind = Some(SourceKind::Git(git_ref)); + url = strip_url_protocol(&url); + } + "registry" => { + kind = Some(SourceKind::Registry); + url = strip_url_protocol(&url); + } + "sparse" => { + kind = Some(SourceKind::SparseRegistry); + // Leave `sparse` as part of URL + // url = strip_url_protocol(&url); + } + "path" => { + if scheme != "file" { + anyhow::bail!("`path+{scheme}` is unsupported; `path+file` and `file` schemes are supported"); + } + kind = Some(SourceKind::Path); + url = strip_url_protocol(&url); + } + kind => anyhow::bail!("unsupported source protocol: {kind}"), + } + } + if url.query().is_some() { bail!("cannot have a query string in a pkgid: {}", url) } + let frag = url.fragment().map(|s| s.to_owned()); url.set_fragment(None); + let (name, version) = { let mut path = url .path_segments() @@ -148,7 +179,7 @@ impl PackageIdSpec { name, version, url: Some(url), - kind: None, + kind, }) } @@ -173,6 +204,14 @@ impl PackageIdSpec { self.url = Some(url); } + pub fn kind(&self) -> Option<&SourceKind> { + self.kind.as_ref() + } + + pub fn set_kind(&mut self, kind: SourceKind) { + self.kind = Some(kind); + } + /// Checks whether the given `PackageId` matches the `PackageIdSpec`. pub fn matches(&self, package_id: PackageId) -> bool { if self.name() != package_id.name().as_str() { @@ -191,6 +230,12 @@ impl PackageIdSpec { } } + if let Some(k) = &self.kind { + if k != package_id.source_id().kind() { + return false; + } + } + true } @@ -287,11 +332,20 @@ impl PackageIdSpec { } } +fn strip_url_protocol(url: &Url) -> Url { + // Ridiculous hoop because `Url::set_scheme` errors when changing to http/https + let raw = url.to_string(); + raw.split_once('+').unwrap().1.parse().unwrap() +} + impl fmt::Display for PackageIdSpec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut printed_name = false; match self.url { Some(ref url) => { + if let Some(protocol) = self.kind.as_ref().and_then(|k| k.protocol()) { + write!(f, "{protocol}+")?; + } write!(f, "{}", url)?; if url.path_segments().unwrap().next_back().unwrap() != &*self.name { printed_name = true; @@ -332,7 +386,7 @@ impl<'de> de::Deserialize<'de> for PackageIdSpec { #[cfg(test)] mod tests { use super::PackageIdSpec; - use crate::core::{PackageId, SourceId}; + use crate::core::{GitReference, PackageId, SourceId, SourceKind}; use url::Url; #[test] @@ -407,6 +461,26 @@ mod tests { }, "https://crates.io/foo#bar@1.2", ); + ok( + "registry+https://crates.io/foo#bar@1.2", + PackageIdSpec { + name: String::from("bar"), + version: Some("1.2".parse().unwrap()), + url: Some(Url::parse("https://crates.io/foo").unwrap()), + kind: Some(SourceKind::Registry), + }, + "registry+https://crates.io/foo#bar@1.2", + ); + ok( + "sparse+https://crates.io/foo#bar@1.2", + PackageIdSpec { + name: String::from("bar"), + version: Some("1.2".parse().unwrap()), + url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()), + kind: Some(SourceKind::SparseRegistry), + }, + "sparse+https://crates.io/foo#bar@1.2", + ); ok( "foo", PackageIdSpec { @@ -499,6 +573,18 @@ mod tests { }, "https://github.com/rust-lang/crates.io-index#regex@1.4.3", ); + ok( + "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3", + PackageIdSpec { + name: String::from("regex"), + version: Some("1.4.3".parse().unwrap()), + url: Some( + Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(), + ), + kind: Some(SourceKind::SparseRegistry), + }, + "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3", + ); ok( "https://github.com/rust-lang/cargo#0.52.0", PackageIdSpec { @@ -529,6 +615,16 @@ mod tests { }, "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3", ); + ok( + "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3", + PackageIdSpec { + name: String::from("regex"), + version: Some("1.4.3".parse().unwrap()), + url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()), + kind: Some(SourceKind::Git(GitReference::DefaultBranch)), + }, + "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3", + ); ok( "file:///path/to/my/project/foo", PackageIdSpec { @@ -549,6 +645,16 @@ mod tests { }, "file:///path/to/my/project/foo#1.1.8", ); + ok( + "path+file:///path/to/my/project/foo#1.1.8", + PackageIdSpec { + name: String::from("foo"), + version: Some("1.1.8".parse().unwrap()), + url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), + kind: Some(SourceKind::Path), + }, + "path+file:///path/to/my/project/foo#1.1.8", + ); } #[test] @@ -560,6 +666,10 @@ mod tests { assert!(PackageIdSpec::parse("baz@^1.0").is_err()); assert!(PackageIdSpec::parse("https://baz:1.0").is_err()); assert!(PackageIdSpec::parse("https://#baz:1.0").is_err()); + assert!( + PackageIdSpec::parse("foobar+https://github.com/rust-lang/crates.io-index").is_err() + ); + assert!(PackageIdSpec::parse("path+https://github.com/rust-lang/crates.io-index").is_err()); } #[test] @@ -581,6 +691,12 @@ mod tests { assert!(!PackageIdSpec::parse("https://bob.com#foo@1.2") .unwrap() .matches(foo)); + assert!(PackageIdSpec::parse("registry+https://example.com#foo@1.2") + .unwrap() + .matches(foo)); + assert!(!PackageIdSpec::parse("git+https://example.com#foo@1.2") + .unwrap() + .matches(foo)); let meta = PackageId::new("meta", "1.2.3+hello", sid).unwrap(); assert!(PackageIdSpec::parse("meta").unwrap().matches(meta)); diff --git a/src/cargo/core/source_id.rs b/src/cargo/core/source_id.rs index 2d8d9711370f..618d6a7139f9 100644 --- a/src/cargo/core/source_id.rs +++ b/src/cargo/core/source_id.rs @@ -373,6 +373,10 @@ impl SourceId { Some(self.inner.url.to_file_path().unwrap()) } + pub fn kind(&self) -> &SourceKind { + &self.inner.kind + } + /// Returns `true` if this source is from a registry (either local or not). pub fn is_registry(self) -> bool { matches!( diff --git a/src/doc/src/reference/pkgid-spec.md b/src/doc/src/reference/pkgid-spec.md index 7f20973b5092..4883fd8e1b8a 100644 --- a/src/doc/src/reference/pkgid-spec.md +++ b/src/doc/src/reference/pkgid-spec.md @@ -21,11 +21,12 @@ qualified with a version to make it unique, such as `regex@1.4.3`. The formal grammar for a Package Id Specification is: ```notrust -spec := pkgname - | proto "://" hostname-and-path [ "#" ( pkgname | semver ) ] +spec := pkgname | + [ kind "+" ] proto "://" hostname-and-path [ "#" ( pkgname | semver ) ] pkgname := name [ ("@" | ":" ) semver ] semver := digits [ "." digits [ "." digits [ "-" prerelease ] [ "+" build ]]] +kind = "sparse" | "registry" | "git" | "file" proto := "http" | "git" | ... ``` @@ -38,28 +39,31 @@ that come from different sources such as different registries. The following are references to the `regex` package on `crates.io`: -| Spec | Name | Version | -|:------------------------------------------------------------|:-------:|:-------:| -| `regex` | `regex` | `*` | -| `regex@1.4` | `regex` | `1.4.*` | -| `regex@1.4.3` | `regex` | `1.4.3` | -| `https://github.com/rust-lang/crates.io-index#regex` | `regex` | `*` | -| `https://github.com/rust-lang/crates.io-index#regex@1.4.3` | `regex` | `1.4.3` | +| Spec | Name | Version | +|:------------------------------------------------------------------|:-------:|:-------:| +| `regex` | `regex` | `*` | +| `regex@1.4` | `regex` | `1.4.*` | +| `regex@1.4.3` | `regex` | `1.4.3` | +| `https://github.com/rust-lang/crates.io-index#regex` | `regex` | `*` | +| `https://github.com/rust-lang/crates.io-index#regex@1.4.3` | `regex` | `1.4.3` | +| `sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3` | `regex` | `1.4.3` | The following are some examples of specs for several different git dependencies: -| Spec | Name | Version | -|:----------------------------------------------------------|:----------------:|:--------:| -| `https://github.com/rust-lang/cargo#0.52.0` | `cargo` | `0.52.0` | -| `https://github.com/rust-lang/cargo#cargo-platform@0.1.2` | `cargo-platform` | `0.1.2` | -| `ssh://git@github.com/rust-lang/regex.git#regex@1.4.3` | `regex` | `1.4.3` | +| Spec | Name | Version | +|:-----------------------------------------------------------|:----------------:|:--------:| +| `https://github.com/rust-lang/cargo#0.52.0` | `cargo` | `0.52.0` | +| `https://github.com/rust-lang/cargo#cargo-platform@0.1.2` | `cargo-platform` | `0.1.2` | +| `ssh://git@github.com/rust-lang/regex.git#regex@1.4.3` | `regex` | `1.4.3` | +| `git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3` | `regex` | `1.4.3` | Local packages on the filesystem can use `file://` URLs to reference them: -| Spec | Name | Version | -|:---------------------------------------|:-----:|:-------:| -| `file:///path/to/my/project/foo` | `foo` | `*` | -| `file:///path/to/my/project/foo#1.1.8` | `foo` | `1.1.8` | +| Spec | Name | Version | +|:--------------------------------------------|:-----:|:-------:| +| `file:///path/to/my/project/foo` | `foo` | `*` | +| `file:///path/to/my/project/foo#1.1.8` | `foo` | `1.1.8` | +| `path+file:///path/to/my/project/foo#1.1.8` | `foo` | `1.1.8` | ### Brevity of specifications diff --git a/tests/testsuite/pkgid.rs b/tests/testsuite/pkgid.rs index 88d991e80d66..87bac8a22d55 100644 --- a/tests/testsuite/pkgid.rs +++ b/tests/testsuite/pkgid.rs @@ -34,7 +34,10 @@ fn local() { p.cargo("generate-lockfile").run(); p.cargo("pkgid foo") - .with_stdout(format!("file://[..]{}#0.1.0", p.root().to_str().unwrap())) + .with_stdout(format!( + "path+file://[..]{}#0.1.0", + p.root().to_str().unwrap() + )) .run(); // Bad file URL. @@ -89,7 +92,7 @@ fn registry() { p.cargo("generate-lockfile").run(); p.cargo("pkgid crates-io") - .with_stdout("https://github.com/rust-lang/crates.io-index#crates-io@0.1.0") + .with_stdout("registry+https://github.com/rust-lang/crates.io-index#crates-io@0.1.0") .run(); // Bad URL. @@ -143,7 +146,7 @@ fn multiple_versions() { p.cargo("generate-lockfile").run(); p.cargo("pkgid two-ver:0.2.0") - .with_stdout("https://github.com/rust-lang/crates.io-index#two-ver@0.2.0") + .with_stdout("registry+https://github.com/rust-lang/crates.io-index#two-ver@0.2.0") .run(); // Incomplete version. @@ -163,7 +166,7 @@ Please re-run this command with one of the following specifications: p.cargo("pkgid two-ver@0.2") .with_stdout( "\ -https://github.com/rust-lang/crates.io-index#two-ver@0.2.0 +registry+https://github.com/rust-lang/crates.io-index#two-ver@0.2.0 ", ) .run();