Skip to content

Commit

Permalink
feat(spec): Track source kind
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Nov 8, 2023
1 parent 08b8160 commit 717cb32
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 25 deletions.
122 changes: 119 additions & 3 deletions src/cargo/core/package_id_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
// 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()
Expand Down Expand Up @@ -148,7 +179,7 @@ impl PackageIdSpec {
name,
version,
url: Some(url),
kind: None,
kind,
})
}

Expand All @@ -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() {
Expand All @@ -191,6 +230,12 @@ impl PackageIdSpec {
}
}

if let Some(k) = &self.kind {
if k != package_id.source_id().kind() {
return false;
}
}

true
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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));
Expand Down
4 changes: 4 additions & 0 deletions src/cargo/core/source_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
40 changes: 22 additions & 18 deletions src/doc/src/reference/pkgid-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" | ...
```

Expand All @@ -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` | <nobr>`cargo-platform`</nobr> | `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` | <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` |
| 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

Expand Down
11 changes: 7 additions & 4 deletions tests/testsuite/pkgid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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();
Expand Down

0 comments on commit 717cb32

Please sign in to comment.