Skip to content

Commit dc73967

Browse files
committed
update: report error for non-matching package specs with --breaking
When using `cargo update --breaking <spec>`, package specifications that don't match any upgradeable dependency are now properly reported as errors instead of being silently ignored. The implementation tracks which specs match direct dependencies during the upgrade process. After processing all workspace members, it validates that each requested spec either: 1. Matched a direct registry dependency (and was processed), or 2. Exists in the lockfile but cannot be upgraded Specs that match neither category produce clear error messages: - "did not match any packages" for completely non-existent packages - "matched a package... but did not match any direct dependencies" for transitive/non-upgradeable packages, with a note explaining that --breaking can only upgrade direct dependencies Multiple errors are collected and reported together for better UX. This fixes the confusing behavior where users could specify packages that don't exist without receiving any feedback.
1 parent e619bf0 commit dc73967

File tree

2 files changed

+80
-11
lines changed

2 files changed

+80
-11
lines changed

src/cargo/ops/cargo_update.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::util::{CargoResult, VersionExt};
1717
use crate::util::{OptVersionReq, style};
1818
use anyhow::Context as _;
1919
use cargo_util_schemas::core::PartialVersion;
20-
use indexmap::IndexMap;
20+
use indexmap::{IndexMap, IndexSet};
2121
use itertools::Itertools;
2222
use semver::{Op, Version, VersionReq};
2323
use std::cmp::Ordering;
@@ -238,6 +238,8 @@ pub fn upgrade_manifests(
238238
let mut registry = ws.package_registry()?;
239239
registry.lock_patches();
240240

241+
let mut remaining_specs: IndexSet<_> = to_update.iter().cloned().collect();
242+
241243
for member in ws.members_mut().sorted() {
242244
debug!("upgrading manifest for `{}`", member.name());
243245

@@ -252,11 +254,28 @@ pub fn upgrade_manifests(
252254
&mut registry,
253255
&mut upgrades,
254256
&mut upgrade_messages,
257+
&mut remaining_specs,
255258
d,
256259
)
257260
})?;
258261
}
259262

263+
if !remaining_specs.is_empty() {
264+
let mut error_msg = String::from(
265+
"package ID specifications did not match any direct dependencies that could be upgraded",
266+
);
267+
268+
for spec in &remaining_specs {
269+
error_msg.push_str(&format!("\n {}", spec));
270+
}
271+
272+
error_msg.push_str(
273+
"\nnote: `--breaking` can only upgrade direct dependencies of workspace members",
274+
);
275+
276+
anyhow::bail!("{}", error_msg);
277+
}
278+
260279
Ok(upgrades)
261280
}
262281

@@ -266,11 +285,15 @@ fn upgrade_dependency(
266285
registry: &mut PackageRegistry<'_>,
267286
upgrades: &mut UpgradeMap,
268287
upgrade_messages: &mut HashSet<String>,
288+
remaining_specs: &mut IndexSet<PackageIdSpec>,
269289
dependency: Dependency,
270290
) -> CargoResult<Dependency> {
271291
let name = dependency.package_name();
272292
let renamed_to = dependency.name_in_toml();
273293

294+
remaining_specs
295+
.retain(|spec| !(spec.name() == name.as_str() && dependency.source_id().is_registry()));
296+
274297
if name != renamed_to {
275298
trace!("skipping dependency renamed from `{name}` to `{renamed_to}`");
276299
return Ok(dependency);

tests/testsuite/update.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,10 +2161,25 @@ fn update_breaking_specific_packages_that_wont_update() {
21612161
Package::new("non-semver", "2.0.0").publish();
21622162
Package::new("transitive-incompatible", "2.0.0").publish();
21632163

2164-
p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver transitive-compatible transitive-incompatible")
2164+
// Test that transitive dependencies produce helpful errors
2165+
p.cargo("update -Zunstable-options --breaking transitive-compatible transitive-incompatible")
21652166
.masquerade_as_nightly_cargo(&["update-breaking"])
2167+
.with_status(101)
21662168
.with_stderr_data(str![[r#"
2167-
[UPDATING] `[..]` index
2169+
[ERROR] package ID specification `transitive-compatible` matched a package in the dependency graph but did not match any direct dependencies that could be upgraded.
2170+
Note: `--breaking` can only upgrade direct dependencies of workspace members.
2171+
2172+
package ID specification `transitive-incompatible` matched a package in the dependency graph but did not match any direct dependencies that could be upgraded.
2173+
Note: `--breaking` can only upgrade direct dependencies of workspace members.
2174+
2175+
"#]])
2176+
.run();
2177+
2178+
// Test that renamed, non-semver, no-breaking-update dependencies are silently skipped (no errors)
2179+
p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver")
2180+
.masquerade_as_nightly_cargo(&["update-breaking"])
2181+
.with_stderr_data(str![[r#"
2182+
[UPDATING] `dummy-registry` index
21682183
21692184
"#]])
21702185
.run();
@@ -2779,21 +2794,34 @@ fn update_breaking_missing_package_error() {
27792794
.add_dep(Dependency::new("transitive", "1.0.0").build())
27802795
.publish();
27812796

2782-
// This test demonstrates the current buggy behavior where invalid package
2783-
// specs are silently ignored instead of reporting an error. A subsequent
2784-
// commit will fix this behavior and update this test to verify proper
2785-
// error reporting.
2786-
2787-
// Non-existent package is silently ignored
2797+
// Non-existent package reports an error
27882798
p.cargo("update -Zunstable-options --breaking no_such_crate")
27892799
.masquerade_as_nightly_cargo(&["update-breaking"])
2800+
.with_status(101)
27902801
.with_stderr_data(str![[r#"
2802+
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
2803+
no_such_crate
2804+
[NOTE] `--breaking` can only upgrade direct dependencies of workspace members
27912805
27922806
"#]])
27932807
.run();
27942808

2795-
// Valid package processes, invalid package silently ignored
2809+
// Valid package processes, invalid package reports error
27962810
p.cargo("update -Zunstable-options --breaking bar no_such_crate")
2811+
.masquerade_as_nightly_cargo(&["update-breaking"])
2812+
.with_status(101)
2813+
.with_stderr_data(str![[r#"
2814+
[UPDATING] `dummy-registry` index
2815+
[UPGRADING] bar ^1.0 -> ^2.0
2816+
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
2817+
no_such_crate
2818+
[NOTE] `--breaking` can only upgrade direct dependencies of workspace members
2819+
2820+
"#]])
2821+
.run();
2822+
2823+
// Successfully upgrade bar to add transitive to lockfile
2824+
p.cargo("update -Zunstable-options --breaking bar")
27972825
.masquerade_as_nightly_cargo(&["update-breaking"])
27982826
.with_stderr_data(str![[r#"
27992827
[UPDATING] `dummy-registry` index
@@ -2805,10 +2833,28 @@ fn update_breaking_missing_package_error() {
28052833
"#]])
28062834
.run();
28072835

2808-
// Transitive dependency is silently ignored (produces no output)
2836+
// Transitive dependency reports helpful error
28092837
p.cargo("update -Zunstable-options --breaking transitive")
28102838
.masquerade_as_nightly_cargo(&["update-breaking"])
2839+
.with_status(101)
2840+
.with_stderr_data(str![[r#"
2841+
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
2842+
transitive
2843+
[NOTE] `--breaking` can only upgrade direct dependencies of workspace members
2844+
2845+
"#]])
2846+
.run();
2847+
2848+
// Multiple error types reported together
2849+
p.cargo("update -Zunstable-options --breaking no_such_crate transitive another_missing")
2850+
.masquerade_as_nightly_cargo(&["update-breaking"])
2851+
.with_status(101)
28112852
.with_stderr_data(str![[r#"
2853+
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
2854+
no_such_crate
2855+
transitive
2856+
another_missing
2857+
[NOTE] `--breaking` can only upgrade direct dependencies of workspace members
28122858
28132859
"#]])
28142860
.run();

0 commit comments

Comments
 (0)