feat(spec): Track source kind

This commit is contained in:
Ed Page 2023-11-07 16:26:49 -06:00
parent 005b55fdbe
commit 9b9c683810
4 changed files with 155 additions and 28 deletions

View file

@ -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<PackageIdSpec> {
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, see `SourceId::new`
// 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));

View file

@ -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!(
@ -748,7 +752,7 @@ impl SourceKind {
SourceKind::Path => Some("path"),
SourceKind::Git(_) => Some("git"),
SourceKind::Registry => Some("registry"),
// Sparse registry URL already includes the `sparse+` prefix
// Sparse registry URL already includes the `sparse+` prefix, see `SourceId::new`
SourceKind::SparseRegistry => None,
SourceKind::LocalRegistry => Some("local-registry"),
SourceKind::Directory => Some("directory"),

View file

@ -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 = "registry" | "git" | "file"
proto := "http" | "git" | ...
```
@ -39,27 +40,30 @@ 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` |
| `registry+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` | <nobr>`cargo-platform`</nobr> | `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` |
| `path+file:///path/to/my/project/foo#1.1.8` | `foo` | `1.1.8` |
### Brevity of specifications

View file

@ -36,7 +36,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.
@ -91,7 +94,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.
@ -145,7 +148,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.
@ -165,7 +168,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();
@ -274,8 +277,8 @@ foo v0.1.0 ([..]/foo)
"\
error: There are multiple `xyz` packages in your project, and the specification `xyz` is ambiguous.
Please re-run this command with one of the following specifications:
file://[..]/xyz#0.5.0
file://[..]/xyz#0.5.0
git+file://[..]/xyz#0.5.0
git+file://[..]/xyz#0.5.0
",
)
.run();