Skip to content

Commit 9b9c683

Browse files
committed
feat(spec): Track source kind
1 parent 005b55f commit 9b9c683

File tree

4 files changed

+155
-28
lines changed

4 files changed

+155
-28
lines changed

src/cargo/core/package_id_spec.rs

+119-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use semver::Version;
66
use serde::{de, ser};
77
use url::Url;
88

9+
use crate::core::GitReference;
910
use crate::core::PackageId;
1011
use crate::core::SourceKind;
1112
use crate::util::edit_distance;
@@ -104,17 +105,47 @@ impl PackageIdSpec {
104105
name: String::from(package_id.name().as_str()),
105106
version: Some(package_id.version().clone().into()),
106107
url: Some(package_id.source_id().url().clone()),
107-
kind: None,
108+
kind: Some(package_id.source_id().kind().clone()),
108109
}
109110
}
110111

111112
/// Tries to convert a valid `Url` to a `PackageIdSpec`.
112113
fn from_url(mut url: Url) -> CargoResult<PackageIdSpec> {
114+
let mut kind = None;
115+
if let Some((kind_str, scheme)) = url.scheme().split_once('+') {
116+
match kind_str {
117+
"git" => {
118+
let git_ref = GitReference::DefaultBranch;
119+
kind = Some(SourceKind::Git(git_ref));
120+
url = strip_url_protocol(&url);
121+
}
122+
"registry" => {
123+
kind = Some(SourceKind::Registry);
124+
url = strip_url_protocol(&url);
125+
}
126+
"sparse" => {
127+
kind = Some(SourceKind::SparseRegistry);
128+
// Leave `sparse` as part of URL, see `SourceId::new`
129+
// url = strip_url_protocol(&url);
130+
}
131+
"path" => {
132+
if scheme != "file" {
133+
anyhow::bail!("`path+{scheme}` is unsupported; `path+file` and `file` schemes are supported");
134+
}
135+
kind = Some(SourceKind::Path);
136+
url = strip_url_protocol(&url);
137+
}
138+
kind => anyhow::bail!("unsupported source protocol: {kind}"),
139+
}
140+
}
141+
113142
if url.query().is_some() {
114143
bail!("cannot have a query string in a pkgid: {}", url)
115144
}
145+
116146
let frag = url.fragment().map(|s| s.to_owned());
117147
url.set_fragment(None);
148+
118149
let (name, version) = {
119150
let mut path = url
120151
.path_segments()
@@ -148,7 +179,7 @@ impl PackageIdSpec {
148179
name,
149180
version,
150181
url: Some(url),
151-
kind: None,
182+
kind,
152183
})
153184
}
154185

@@ -173,6 +204,14 @@ impl PackageIdSpec {
173204
self.url = Some(url);
174205
}
175206

207+
pub fn kind(&self) -> Option<&SourceKind> {
208+
self.kind.as_ref()
209+
}
210+
211+
pub fn set_kind(&mut self, kind: SourceKind) {
212+
self.kind = Some(kind);
213+
}
214+
176215
/// Checks whether the given `PackageId` matches the `PackageIdSpec`.
177216
pub fn matches(&self, package_id: PackageId) -> bool {
178217
if self.name() != package_id.name().as_str() {
@@ -191,6 +230,12 @@ impl PackageIdSpec {
191230
}
192231
}
193232

233+
if let Some(k) = &self.kind {
234+
if k != package_id.source_id().kind() {
235+
return false;
236+
}
237+
}
238+
194239
true
195240
}
196241

@@ -287,11 +332,20 @@ impl PackageIdSpec {
287332
}
288333
}
289334

335+
fn strip_url_protocol(url: &Url) -> Url {
336+
// Ridiculous hoop because `Url::set_scheme` errors when changing to http/https
337+
let raw = url.to_string();
338+
raw.split_once('+').unwrap().1.parse().unwrap()
339+
}
340+
290341
impl fmt::Display for PackageIdSpec {
291342
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292343
let mut printed_name = false;
293344
match self.url {
294345
Some(ref url) => {
346+
if let Some(protocol) = self.kind.as_ref().and_then(|k| k.protocol()) {
347+
write!(f, "{protocol}+")?;
348+
}
295349
write!(f, "{}", url)?;
296350
if url.path_segments().unwrap().next_back().unwrap() != &*self.name {
297351
printed_name = true;
@@ -332,7 +386,7 @@ impl<'de> de::Deserialize<'de> for PackageIdSpec {
332386
#[cfg(test)]
333387
mod tests {
334388
use super::PackageIdSpec;
335-
use crate::core::{PackageId, SourceId};
389+
use crate::core::{GitReference, PackageId, SourceId, SourceKind};
336390
use url::Url;
337391

338392
#[test]
@@ -407,6 +461,26 @@ mod tests {
407461
},
408462
"https://crates.io/foo#[email protected]",
409463
);
464+
ok(
465+
"registry+https://crates.io/foo#[email protected]",
466+
PackageIdSpec {
467+
name: String::from("bar"),
468+
version: Some("1.2".parse().unwrap()),
469+
url: Some(Url::parse("https://crates.io/foo").unwrap()),
470+
kind: Some(SourceKind::Registry),
471+
},
472+
"registry+https://crates.io/foo#[email protected]",
473+
);
474+
ok(
475+
"sparse+https://crates.io/foo#[email protected]",
476+
PackageIdSpec {
477+
name: String::from("bar"),
478+
version: Some("1.2".parse().unwrap()),
479+
url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()),
480+
kind: Some(SourceKind::SparseRegistry),
481+
},
482+
"sparse+https://crates.io/foo#[email protected]",
483+
);
410484
ok(
411485
"foo",
412486
PackageIdSpec {
@@ -499,6 +573,18 @@ mod tests {
499573
},
500574
"https://github.com/rust-lang/crates.io-index#[email protected]",
501575
);
576+
ok(
577+
"sparse+https://github.com/rust-lang/crates.io-index#[email protected]",
578+
PackageIdSpec {
579+
name: String::from("regex"),
580+
version: Some("1.4.3".parse().unwrap()),
581+
url: Some(
582+
Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(),
583+
),
584+
kind: Some(SourceKind::SparseRegistry),
585+
},
586+
"sparse+https://github.com/rust-lang/crates.io-index#[email protected]",
587+
);
502588
ok(
503589
"https://github.com/rust-lang/cargo#0.52.0",
504590
PackageIdSpec {
@@ -529,6 +615,16 @@ mod tests {
529615
},
530616
"ssh://[email protected]/rust-lang/regex.git#[email protected]",
531617
);
618+
ok(
619+
"git+ssh://[email protected]/rust-lang/regex.git#[email protected]",
620+
PackageIdSpec {
621+
name: String::from("regex"),
622+
version: Some("1.4.3".parse().unwrap()),
623+
url: Some(Url::parse("ssh://[email protected]/rust-lang/regex.git").unwrap()),
624+
kind: Some(SourceKind::Git(GitReference::DefaultBranch)),
625+
},
626+
"git+ssh://[email protected]/rust-lang/regex.git#[email protected]",
627+
);
532628
ok(
533629
"file:///path/to/my/project/foo",
534630
PackageIdSpec {
@@ -549,6 +645,16 @@ mod tests {
549645
},
550646
"file:///path/to/my/project/foo#1.1.8",
551647
);
648+
ok(
649+
"path+file:///path/to/my/project/foo#1.1.8",
650+
PackageIdSpec {
651+
name: String::from("foo"),
652+
version: Some("1.1.8".parse().unwrap()),
653+
url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
654+
kind: Some(SourceKind::Path),
655+
},
656+
"path+file:///path/to/my/project/foo#1.1.8",
657+
);
552658
}
553659

554660
#[test]
@@ -560,6 +666,10 @@ mod tests {
560666
assert!(PackageIdSpec::parse("baz@^1.0").is_err());
561667
assert!(PackageIdSpec::parse("https://baz:1.0").is_err());
562668
assert!(PackageIdSpec::parse("https://#baz:1.0").is_err());
669+
assert!(
670+
PackageIdSpec::parse("foobar+https://github.com/rust-lang/crates.io-index").is_err()
671+
);
672+
assert!(PackageIdSpec::parse("path+https://github.com/rust-lang/crates.io-index").is_err());
563673
}
564674

565675
#[test]
@@ -581,6 +691,12 @@ mod tests {
581691
assert!(!PackageIdSpec::parse("https://bob.com#[email protected]")
582692
.unwrap()
583693
.matches(foo));
694+
assert!(PackageIdSpec::parse("registry+https://example.com#[email protected]")
695+
.unwrap()
696+
.matches(foo));
697+
assert!(!PackageIdSpec::parse("git+https://example.com#[email protected]")
698+
.unwrap()
699+
.matches(foo));
584700

585701
let meta = PackageId::new("meta", "1.2.3+hello", sid).unwrap();
586702
assert!(PackageIdSpec::parse("meta").unwrap().matches(meta));

src/cargo/core/source_id.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ impl SourceId {
373373
Some(self.inner.url.to_file_path().unwrap())
374374
}
375375

376+
pub fn kind(&self) -> &SourceKind {
377+
&self.inner.kind
378+
}
379+
376380
/// Returns `true` if this source is from a registry (either local or not).
377381
pub fn is_registry(self) -> bool {
378382
matches!(
@@ -748,7 +752,7 @@ impl SourceKind {
748752
SourceKind::Path => Some("path"),
749753
SourceKind::Git(_) => Some("git"),
750754
SourceKind::Registry => Some("registry"),
751-
// Sparse registry URL already includes the `sparse+` prefix
755+
// Sparse registry URL already includes the `sparse+` prefix, see `SourceId::new`
752756
SourceKind::SparseRegistry => None,
753757
SourceKind::LocalRegistry => Some("local-registry"),
754758
SourceKind::Directory => Some("directory"),

src/doc/src/reference/pkgid-spec.md

+22-18
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ qualified with a version to make it unique, such as `[email protected]`.
2121
The formal grammar for a Package Id Specification is:
2222

2323
```notrust
24-
spec := pkgname
25-
| proto "://" hostname-and-path [ "#" ( pkgname | semver ) ]
24+
spec := pkgname |
25+
[ kind "+" ] proto "://" hostname-and-path [ "#" ( pkgname | semver ) ]
2626
pkgname := name [ ("@" | ":" ) semver ]
2727
semver := digits [ "." digits [ "." digits [ "-" prerelease ] [ "+" build ]]]
2828
29+
kind = "registry" | "git" | "file"
2930
proto := "http" | "git" | ...
3031
```
3132

@@ -38,28 +39,31 @@ that come from different sources such as different registries.
3839

3940
The following are references to the `regex` package on `crates.io`:
4041

41-
| Spec | Name | Version |
42-
|:------------------------------------------------------------|:-------:|:-------:|
43-
| `regex` | `regex` | `*` |
44-
| `[email protected]` | `regex` | `1.4.*` |
45-
| `[email protected]` | `regex` | `1.4.3` |
46-
| `https://github.com/rust-lang/crates.io-index#regex` | `regex` | `*` |
47-
| `https://github.com/rust-lang/crates.io-index#[email protected]` | `regex` | `1.4.3` |
42+
| Spec | Name | Version |
43+
|:------------------------------------------------------------------|:-------:|:-------:|
44+
| `regex` | `regex` | `*` |
45+
| `[email protected]` | `regex` | `1.4.*` |
46+
| `[email protected]` | `regex` | `1.4.3` |
47+
| `https://github.com/rust-lang/crates.io-index#regex` | `regex` | `*` |
48+
| `https://github.com/rust-lang/crates.io-index#[email protected]` | `regex` | `1.4.3` |
49+
| `registry+https://github.com/rust-lang/crates.io-index#[email protected]` | `regex` | `1.4.3` |
4850

4951
The following are some examples of specs for several different git dependencies:
5052

51-
| Spec | Name | Version |
52-
|:----------------------------------------------------------|:----------------:|:--------:|
53-
| `https://github.com/rust-lang/cargo#0.52.0` | `cargo` | `0.52.0` |
54-
| `https://github.com/rust-lang/cargo#[email protected]` | <nobr>`cargo-platform`</nobr> | `0.1.2` |
55-
| `ssh://[email protected]/rust-lang/regex.git#[email protected]` | `regex` | `1.4.3` |
53+
| Spec | Name | Version |
54+
|:-----------------------------------------------------------|:----------------:|:--------:|
55+
| `https://github.com/rust-lang/cargo#0.52.0` | `cargo` | `0.52.0` |
56+
| `https://github.com/rust-lang/cargo#[email protected]` | <nobr>`cargo-platform`</nobr> | `0.1.2` |
57+
| `ssh://[email protected]/rust-lang/regex.git#[email protected]` | `regex` | `1.4.3` |
58+
| `git+ssh://[email protected]/rust-lang/regex.git#[email protected]` | `regex` | `1.4.3` |
5659

5760
Local packages on the filesystem can use `file://` URLs to reference them:
5861

59-
| Spec | Name | Version |
60-
|:---------------------------------------|:-----:|:-------:|
61-
| `file:///path/to/my/project/foo` | `foo` | `*` |
62-
| `file:///path/to/my/project/foo#1.1.8` | `foo` | `1.1.8` |
62+
| Spec | Name | Version |
63+
|:--------------------------------------------|:-----:|:-------:|
64+
| `file:///path/to/my/project/foo` | `foo` | `*` |
65+
| `file:///path/to/my/project/foo#1.1.8` | `foo` | `1.1.8` |
66+
| `path+file:///path/to/my/project/foo#1.1.8` | `foo` | `1.1.8` |
6367

6468
### Brevity of specifications
6569

tests/testsuite/pkgid.rs

+9-6
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ fn local() {
3636
p.cargo("generate-lockfile").run();
3737

3838
p.cargo("pkgid foo")
39-
.with_stdout(format!("file://[..]{}#0.1.0", p.root().to_str().unwrap()))
39+
.with_stdout(format!(
40+
"path+file://[..]{}#0.1.0",
41+
p.root().to_str().unwrap()
42+
))
4043
.run();
4144

4245
// Bad file URL.
@@ -91,7 +94,7 @@ fn registry() {
9194
p.cargo("generate-lockfile").run();
9295

9396
p.cargo("pkgid crates-io")
94-
.with_stdout("https://github.com/rust-lang/crates.io-index#[email protected]")
97+
.with_stdout("registry+https://github.com/rust-lang/crates.io-index#[email protected]")
9598
.run();
9699

97100
// Bad URL.
@@ -145,7 +148,7 @@ fn multiple_versions() {
145148
p.cargo("generate-lockfile").run();
146149

147150
p.cargo("pkgid two-ver:0.2.0")
148-
.with_stdout("https://github.com/rust-lang/crates.io-index#[email protected]")
151+
.with_stdout("registry+https://github.com/rust-lang/crates.io-index#[email protected]")
149152
.run();
150153

151154
// Incomplete version.
@@ -165,7 +168,7 @@ Please re-run this command with one of the following specifications:
165168
p.cargo("pkgid [email protected]")
166169
.with_stdout(
167170
"\
168-
https://github.com/rust-lang/crates.io-index#[email protected]
171+
registry+https://github.com/rust-lang/crates.io-index#[email protected]
169172
",
170173
)
171174
.run();
@@ -274,8 +277,8 @@ foo v0.1.0 ([..]/foo)
274277
"\
275278
error: There are multiple `xyz` packages in your project, and the specification `xyz` is ambiguous.
276279
Please re-run this command with one of the following specifications:
277-
file://[..]/xyz#0.5.0
278-
file://[..]/xyz#0.5.0
280+
git+file://[..]/xyz#0.5.0
281+
git+file://[..]/xyz#0.5.0
279282
",
280283
)
281284
.run();

0 commit comments

Comments
 (0)