Skip to content

Commit 82f9bd3

Browse files
committed
feat(spec): Allow partial versions when unambigious
This was proposed in #12425 to help improve usability of the existing `cargo update` when dealing with the added workflows.
1 parent 9008647 commit 82f9bd3

File tree

10 files changed

+110
-55
lines changed

10 files changed

+110
-55
lines changed

src/bin/cargo/commands/remove.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ fn spec_has_match(
288288
}
289289

290290
let version_matches = match (spec.version(), dep.version()) {
291-
(Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(v),
291+
(Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(&v),
292292
(Some(_), None) => false,
293293
(None, None | Some(_)) => true,
294294
};

src/cargo/core/package_id_spec.rs

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use crate::core::PackageId;
1010
use crate::util::edit_distance;
1111
use crate::util::errors::CargoResult;
1212
use crate::util::interning::InternedString;
13-
use crate::util::{validate_package_name, IntoUrl, ToSemver};
13+
use crate::util::PartialVersion;
14+
use crate::util::{validate_package_name, IntoUrl};
1415

1516
/// Some or all of the data required to identify a package:
1617
///
@@ -24,7 +25,7 @@ use crate::util::{validate_package_name, IntoUrl, ToSemver};
2425
#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
2526
pub struct PackageIdSpec {
2627
name: InternedString,
27-
version: Option<Version>,
28+
version: Option<PartialVersion>,
2829
url: Option<Url>,
2930
}
3031

@@ -70,7 +71,7 @@ impl PackageIdSpec {
7071
let mut parts = spec.splitn(2, [':', '@']);
7172
let name = parts.next().unwrap();
7273
let version = match parts.next() {
73-
Some(version) => Some(version.to_semver()?),
74+
Some(version) => Some(version.parse::<PartialVersion>()?),
7475
None => None,
7576
};
7677
validate_package_name(name, "pkgid", "")?;
@@ -94,12 +95,12 @@ impl PackageIdSpec {
9495
spec.query(i)
9596
}
9697

97-
/// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `Version` and `Url`
98+
/// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `PartialVersion` and `Url`
9899
/// fields filled in.
99100
pub fn from_package_id(package_id: PackageId) -> PackageIdSpec {
100101
PackageIdSpec {
101102
name: package_id.name(),
102-
version: Some(package_id.version().clone()),
103+
version: Some(package_id.version().clone().into()),
103104
url: Some(package_id.source_id().url().clone()),
104105
}
105106
}
@@ -125,14 +126,14 @@ impl PackageIdSpec {
125126
match frag {
126127
Some(fragment) => match fragment.split_once([':', '@']) {
127128
Some((name, part)) => {
128-
let version = part.to_semver()?;
129+
let version = part.parse::<PartialVersion>()?;
129130
(InternedString::new(name), Some(version))
130131
}
131132
None => {
132133
if fragment.chars().next().unwrap().is_alphabetic() {
133134
(InternedString::new(&fragment), None)
134135
} else {
135-
let version = fragment.to_semver()?;
136+
let version = fragment.parse::<PartialVersion>()?;
136137
(InternedString::new(path_name), Some(version))
137138
}
138139
}
@@ -151,7 +152,12 @@ impl PackageIdSpec {
151152
self.name
152153
}
153154

154-
pub fn version(&self) -> Option<&Version> {
155+
/// Full `semver::Version`, if present
156+
pub fn version(&self) -> Option<Version> {
157+
self.version.as_ref().and_then(|v| v.version())
158+
}
159+
160+
pub fn partial_version(&self) -> Option<&PartialVersion> {
155161
self.version.as_ref()
156162
}
157163

@@ -170,7 +176,8 @@ impl PackageIdSpec {
170176
}
171177

172178
if let Some(ref v) = self.version {
173-
if v != package_id.version() {
179+
let req = v.exact_req();
180+
if !req.matches(package_id.version()) {
174181
return false;
175182
}
176183
}
@@ -319,7 +326,6 @@ mod tests {
319326
use super::PackageIdSpec;
320327
use crate::core::{PackageId, SourceId};
321328
use crate::util::interning::InternedString;
322-
use crate::util::ToSemver;
323329
use url::Url;
324330

325331
#[test]
@@ -344,16 +350,25 @@ mod tests {
344350
"https://crates.io/foo#1.2.3",
345351
PackageIdSpec {
346352
name: InternedString::new("foo"),
347-
version: Some("1.2.3".to_semver().unwrap()),
353+
version: Some("1.2.3".parse().unwrap()),
348354
url: Some(Url::parse("https://crates.io/foo").unwrap()),
349355
},
350356
"https://crates.io/foo#1.2.3",
351357
);
358+
ok(
359+
"https://crates.io/foo#1.2",
360+
PackageIdSpec {
361+
name: InternedString::new("foo"),
362+
version: Some("1.2".parse().unwrap()),
363+
url: Some(Url::parse("https://crates.io/foo").unwrap()),
364+
},
365+
"https://crates.io/foo#1.2",
366+
);
352367
ok(
353368
"https://crates.io/foo#bar:1.2.3",
354369
PackageIdSpec {
355370
name: InternedString::new("bar"),
356-
version: Some("1.2.3".to_semver().unwrap()),
371+
version: Some("1.2.3".parse().unwrap()),
357372
url: Some(Url::parse("https://crates.io/foo").unwrap()),
358373
},
359374
"https://crates.io/foo#[email protected]",
@@ -362,11 +377,20 @@ mod tests {
362377
"https://crates.io/foo#[email protected]",
363378
PackageIdSpec {
364379
name: InternedString::new("bar"),
365-
version: Some("1.2.3".to_semver().unwrap()),
380+
version: Some("1.2.3".parse().unwrap()),
366381
url: Some(Url::parse("https://crates.io/foo").unwrap()),
367382
},
368383
"https://crates.io/foo#[email protected]",
369384
);
385+
ok(
386+
"https://crates.io/foo#[email protected]",
387+
PackageIdSpec {
388+
name: InternedString::new("bar"),
389+
version: Some("1.2".parse().unwrap()),
390+
url: Some(Url::parse("https://crates.io/foo").unwrap()),
391+
},
392+
"https://crates.io/foo#[email protected]",
393+
);
370394
ok(
371395
"foo",
372396
PackageIdSpec {
@@ -380,7 +404,7 @@ mod tests {
380404
"foo:1.2.3",
381405
PackageIdSpec {
382406
name: InternedString::new("foo"),
383-
version: Some("1.2.3".to_semver().unwrap()),
407+
version: Some("1.2.3".parse().unwrap()),
384408
url: None,
385409
},
386410
@@ -389,21 +413,29 @@ mod tests {
389413
390414
PackageIdSpec {
391415
name: InternedString::new("foo"),
392-
version: Some("1.2.3".to_semver().unwrap()),
416+
version: Some("1.2.3".parse().unwrap()),
393417
url: None,
394418
},
395419
396420
);
421+
ok(
422+
423+
PackageIdSpec {
424+
name: InternedString::new("foo"),
425+
version: Some("1.2".parse().unwrap()),
426+
url: None,
427+
},
428+
429+
);
397430
}
398431

399432
#[test]
400433
fn bad_parsing() {
401434
assert!(PackageIdSpec::parse("baz:").is_err());
402435
assert!(PackageIdSpec::parse("baz:*").is_err());
403-
assert!(PackageIdSpec::parse("baz:1.0").is_err());
404436
assert!(PackageIdSpec::parse("baz@").is_err());
405437
assert!(PackageIdSpec::parse("baz@*").is_err());
406-
assert!(PackageIdSpec::parse("[email protected]").is_err());
438+
assert!(PackageIdSpec::parse("baz@^1.0").is_err());
407439
assert!(PackageIdSpec::parse("https://baz:1.0").is_err());
408440
assert!(PackageIdSpec::parse("https://#baz:1.0").is_err());
409441
}
@@ -421,5 +453,6 @@ mod tests {
421453
assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo));
422454
assert!(PackageIdSpec::parse("[email protected]").unwrap().matches(foo));
423455
assert!(!PackageIdSpec::parse("[email protected]").unwrap().matches(foo));
456+
assert!(PackageIdSpec::parse("[email protected]").unwrap().matches(foo));
424457
}
425458
}

src/cargo/ops/cargo_clean.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
108108
for spec_str in opts.spec.iter() {
109109
// Translate the spec to a Package.
110110
let spec = PackageIdSpec::parse(spec_str)?;
111-
if spec.version().is_some() {
111+
if spec.partial_version().is_some() {
112112
config.shell().warn(&format!(
113113
"version qualifier in `-p {}` is ignored, \
114114
cleaning all versions of `{}` found",

src/cargo/util/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub use self::progress::{Progress, ProgressStyle};
2222
pub use self::queue::Queue;
2323
pub use self::restricted_names::validate_package_name;
2424
pub use self::rustc::Rustc;
25-
pub use self::semver_ext::{OptVersionReq, RustVersion, VersionExt, VersionReqExt};
25+
pub use self::semver_ext::{OptVersionReq, PartialVersion, RustVersion, VersionExt, VersionReqExt};
2626
pub use self::to_semver::ToSemver;
2727
pub use self::vcs::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo};
2828
pub use self::workspace::{

src/cargo/util/semver_ext.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,16 @@ pub struct PartialVersion {
164164
}
165165

166166
impl PartialVersion {
167+
pub fn version(&self) -> Option<Version> {
168+
Some(Version {
169+
major: self.major,
170+
minor: self.minor?,
171+
patch: self.patch?,
172+
pre: self.pre.clone().unwrap_or_default(),
173+
build: self.build.clone().unwrap_or_default(),
174+
})
175+
}
176+
167177
pub fn caret_req(&self) -> VersionReq {
168178
VersionReq {
169179
comparators: vec![Comparator {
@@ -175,6 +185,18 @@ impl PartialVersion {
175185
}],
176186
}
177187
}
188+
189+
pub fn exact_req(&self) -> VersionReq {
190+
VersionReq {
191+
comparators: vec![Comparator {
192+
op: semver::Op::Exact,
193+
major: self.major,
194+
minor: self.minor,
195+
patch: self.patch,
196+
pre: self.pre.as_ref().cloned().unwrap_or_default(),
197+
}],
198+
}
199+
}
178200
}
179201

180202
impl From<semver::Version> for PartialVersion {

src/cargo/util/toml/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,8 +2652,8 @@ impl TomlManifest {
26522652
replacement.unused_keys(),
26532653
&mut cx.warnings,
26542654
);
2655-
dep.set_version_req(OptVersionReq::exact(version))
2656-
.lock_version(version);
2655+
dep.set_version_req(OptVersionReq::exact(&version))
2656+
.lock_version(&version);
26572657
replace.push((spec, dep));
26582658
}
26592659
Ok(replace)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ The formal grammar for a Package Id Specification is:
2424
spec := pkgname
2525
| proto "://" hostname-and-path [ "#" ( pkgname | semver ) ]
2626
pkgname := name [ ("@" | ":" ) semver ]
27+
semver := digits [ "." digits [ "." digits [ "-" prerelease ] [ "+" build ]]]
2728
2829
proto := "http" | "git" | ...
2930
```
@@ -40,6 +41,7 @@ The following are references to the `regex` package on `crates.io`:
4041
| Spec | Name | Version |
4142
|:------------------------------------------------------------|:-------:|:-------:|
4243
| `regex` | `regex` | `*` |
44+
| `[email protected]` | `regex` | `1.4.*` |
4345
| `[email protected]` | `regex` | `1.4.3` |
4446
| `https://github.com/rust-lang/crates.io-index#regex` | `regex` | `*` |
4547
| `https://github.com/rust-lang/crates.io-index#[email protected]` | `regex` | `1.4.3` |

tests/testsuite/clean.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -659,13 +659,21 @@ error: package ID specification `baz` did not match any packages
659659
.run();
660660

661661
p.cargo("clean -p bar:0.1")
662-
.with_status(101)
663662
.with_stderr(
664-
"\
665-
error: cannot parse '0.1' as a SemVer version
666-
",
663+
"warning: version qualifier in `-p bar:0.1` is ignored, \
664+
cleaning all versions of `bar` found",
667665
)
668666
.run();
667+
let mut walker = walkdir::WalkDir::new(p.build_dir())
668+
.into_iter()
669+
.filter_map(|e| e.ok())
670+
.filter(|e| {
671+
let n = e.file_name().to_str().unwrap();
672+
n.starts_with("bar") || n.starts_with("libbar")
673+
});
674+
if let Some(e) = walker.next() {
675+
panic!("{:?} was not cleaned", e.path());
676+
}
669677
}
670678

671679
#[cargo_test]
@@ -705,13 +713,21 @@ error: package ID specification `baz` did not match any packages
705713
.run();
706714

707715
p.cargo("clean -p bar:0")
708-
.with_status(101)
709716
.with_stderr(
710-
"\
711-
error: cannot parse '0' as a SemVer version
712-
",
717+
"warning: version qualifier in `-p bar:0` is ignored, \
718+
cleaning all versions of `bar` found",
713719
)
714720
.run();
721+
let mut walker = walkdir::WalkDir::new(p.build_dir())
722+
.into_iter()
723+
.filter_map(|e| e.ok())
724+
.filter(|e| {
725+
let n = e.file_name().to_str().unwrap();
726+
n.starts_with("bar") || n.starts_with("libbar")
727+
});
728+
if let Some(e) = walker.next() {
729+
panic!("{:?} was not cleaned", e.path());
730+
}
715731
}
716732

717733
#[cargo_test]

tests/testsuite/pkgid.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,25 +151,19 @@ fn multiple_versions() {
151151
.with_status(101)
152152
.with_stderr(
153153
"\
154-
error: invalid package ID specification: `two-ver@0`
155-
156-
<tab>Did you mean `two-ver`?
157-
158-
Caused by:
159-
cannot parse '0' as a SemVer version
154+
error: There are multiple `two-ver` packages in your project, and the specification `two-ver@0` is ambiguous.
155+
Please re-run this command with `-p <spec>` where `<spec>` is one of the following:
156+
157+
160158
",
161159
)
162160
.run();
163161

164162
// Incomplete version.
165163
p.cargo("pkgid [email protected]")
166-
.with_status(101)
167-
.with_stderr(
164+
.with_stdout(
168165
"\
169-
error: invalid package ID specification: `[email protected]`
170-
171-
Caused by:
172-
cannot parse '0.2' as a SemVer version
166+
https://github.com/rust-lang/crates.io-index#[email protected]
173167
",
174168
)
175169
.run();

tests/testsuite/profile_overrides.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -317,19 +317,7 @@ fn profile_override_spec_with_partial_version() {
317317
.build();
318318

319319
p.cargo("check -v")
320-
.with_status(101)
321-
.with_stderr_contains(
322-
"\
323-
error: failed to parse manifest at `[CWD]/Cargo.toml`
324-
325-
Caused by:
326-
TOML parse error at line 9, column 34
327-
|
328-
9 | [profile.dev.package.\"bar:0.5\"]
329-
| ^^^^^^^^^
330-
cannot parse '0.5' as a SemVer version
331-
",
332-
)
320+
.with_stderr_contains("[RUNNING] `rustc [..]bar/src/lib.rs [..] -C codegen-units=2 [..]")
333321
.run();
334322
}
335323

0 commit comments

Comments
 (0)