Skip to content

Commit b0df265

Browse files
committed
Auto merge of #13372 - epage:update-report, r=weihanglo
feat(update): Tell users when they are still behind ### What does this PR try to resolve? Part of this is an offshoot of #12425 which is about pulling some of `cargo upgrade`s behavior into `cargo update`. One of the "'Potential related `cargo update` improvements" is informing the user when they are behind. Part of this is to help close the gap of users being behind on their dependencies unaware. This is commonly raised when discussing an MSRV-aware resolver (see rust-lang/rfcs#3537) but breaking changes are just as big of a deal so I'm starting this now. See also #7167, #4309 Compared to `cargo upgrade` / `cargo outdated`, I'm taking a fairly conservative approach and tweaking the existing output as a starting point / MVP. We can experiment with a richer or easier-to-consume way of expressing this over time. I view us telling people they aren't on the latest as a warning, so I made that text yellow. `clap $ cargo update --dry-run` ![image](https://github.com/rust-lang/cargo/assets/60961/4bf151e3-6b57-4073-8822-9140dd731d5e) `clap $ cargo update --dry-run --verbose` ![image](https://github.com/rust-lang/cargo/assets/60961/fbf802fb-3a6a-4e8b-a6ec-4ce49fb505f6) ### How should we test and review this PR? This sets up the minimal implementation and slowly adds bits at a time, with a test first that demonstrates it. ### Additional information I'm expecting that the `cargo upgrade` integration will extend the notes to say something like "X dependencies may be updated with `--breaking`"
2 parents 7bc317b + 93e369a commit b0df265

File tree

8 files changed

+301
-48
lines changed

8 files changed

+301
-48
lines changed

crates/cargo-test-support/src/compare.rs

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ fn substitute_macros(input: &str) -> String {
208208
("[ADDING]", " Adding"),
209209
("[REMOVING]", " Removing"),
210210
("[REMOVED]", " Removed"),
211+
("[UNCHANGED]", " Unchanged"),
211212
("[DOCTEST]", " Doc-tests"),
212213
("[PACKAGING]", " Packaging"),
213214
("[PACKAGED]", " Packaged"),

src/cargo/ops/cargo_generate_lockfile.rs

+164-46
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use crate::core::registry::PackageRegistry;
22
use crate::core::resolver::features::{CliFeatures, HasDevUnits};
3+
use crate::core::shell::Verbosity;
4+
use crate::core::Registry as _;
35
use crate::core::{PackageId, PackageIdSpec, PackageIdSpecQuery};
46
use crate::core::{Resolve, SourceId, Workspace};
57
use crate::ops;
8+
use crate::sources::source::QueryKind;
69
use crate::util::cache_lock::CacheLockMode;
710
use crate::util::config::Config;
811
use crate::util::style;
@@ -161,36 +164,137 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
161164
let print_change = |status: &str, msg: String, color: &Style| {
162165
opts.config.shell().status_with_color(status, msg, color)
163166
};
164-
for (removed, added) in compare_dependency_graphs(&previous_resolve, &resolve) {
167+
let mut unchanged_behind = 0;
168+
for ResolvedPackageVersions {
169+
removed,
170+
added,
171+
unchanged,
172+
} in compare_dependency_graphs(&previous_resolve, &resolve)
173+
{
174+
fn format_latest(version: semver::Version) -> String {
175+
let warn = style::WARN;
176+
format!(" {warn}(latest: v{version}){warn:#}")
177+
}
178+
fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
179+
current < candidate
180+
// Only match pre-release if major.minor.patch are the same
181+
&& (candidate.pre.is_empty()
182+
|| (candidate.major == current.major
183+
&& candidate.minor == current.minor
184+
&& candidate.patch == current.patch))
185+
}
186+
let possibilities = if let Some(query) = [added.iter(), unchanged.iter()]
187+
.into_iter()
188+
.flatten()
189+
.next()
190+
.filter(|s| s.source_id().is_registry())
191+
{
192+
let query =
193+
crate::core::dependency::Dependency::parse(query.name(), None, query.source_id())?;
194+
loop {
195+
match registry.query_vec(&query, QueryKind::Exact) {
196+
std::task::Poll::Ready(res) => {
197+
break res?;
198+
}
199+
std::task::Poll::Pending => registry.block_until_ready()?,
200+
}
201+
}
202+
} else {
203+
vec![]
204+
};
205+
165206
if removed.len() == 1 && added.len() == 1 {
166-
let msg = if removed[0].source_id().is_git() {
207+
let added = added.into_iter().next().unwrap();
208+
let removed = removed.into_iter().next().unwrap();
209+
210+
let latest = if !possibilities.is_empty() {
211+
possibilities
212+
.iter()
213+
.map(|s| s.as_summary())
214+
.filter(|s| is_latest(s.version(), added.version()))
215+
.map(|s| s.version().clone())
216+
.max()
217+
.map(format_latest)
218+
} else {
219+
None
220+
}
221+
.unwrap_or_default();
222+
223+
let msg = if removed.source_id().is_git() {
167224
format!(
168-
"{} -> #{}",
169-
removed[0],
170-
&added[0].source_id().precise_git_fragment().unwrap()[..8],
225+
"{removed} -> #{}",
226+
&added.source_id().precise_git_fragment().unwrap()[..8],
171227
)
172228
} else {
173-
format!("{} -> v{}", removed[0], added[0].version())
229+
format!("{removed} -> v{}{latest}", added.version())
174230
};
175231

176232
// If versions differ only in build metadata, we call it an "update"
177233
// regardless of whether the build metadata has gone up or down.
178234
// This metadata is often stuff like git commit hashes, which are
179235
// not meaningfully ordered.
180-
if removed[0].version().cmp_precedence(added[0].version()) == Ordering::Greater {
236+
if removed.version().cmp_precedence(added.version()) == Ordering::Greater {
181237
print_change("Downgrading", msg, &style::WARN)?;
182238
} else {
183239
print_change("Updating", msg, &style::GOOD)?;
184240
}
185241
} else {
186242
for package in removed.iter() {
187-
print_change("Removing", format!("{}", package), &style::ERROR)?;
243+
print_change("Removing", format!("{package}"), &style::ERROR)?;
188244
}
189245
for package in added.iter() {
190-
print_change("Adding", format!("{}", package), &style::NOTE)?;
246+
let latest = if !possibilities.is_empty() {
247+
possibilities
248+
.iter()
249+
.map(|s| s.as_summary())
250+
.filter(|s| is_latest(s.version(), package.version()))
251+
.map(|s| s.version().clone())
252+
.max()
253+
.map(format_latest)
254+
} else {
255+
None
256+
}
257+
.unwrap_or_default();
258+
259+
print_change("Adding", format!("{package}{latest}"), &style::NOTE)?;
260+
}
261+
}
262+
for package in &unchanged {
263+
let latest = if !possibilities.is_empty() {
264+
possibilities
265+
.iter()
266+
.map(|s| s.as_summary())
267+
.filter(|s| is_latest(s.version(), package.version()))
268+
.map(|s| s.version().clone())
269+
.max()
270+
.map(format_latest)
271+
} else {
272+
None
273+
};
274+
275+
if let Some(latest) = latest {
276+
unchanged_behind += 1;
277+
if opts.config.shell().verbosity() == Verbosity::Verbose {
278+
opts.config.shell().status_with_color(
279+
"Unchanged",
280+
format!("{package}{latest}"),
281+
&anstyle::Style::new().bold(),
282+
)?;
283+
}
191284
}
192285
}
193286
}
287+
if opts.config.shell().verbosity() == Verbosity::Verbose {
288+
opts.config.shell().note(
289+
"to see how you depend on a package, run `cargo tree --invert --package <dep>@<ver>`",
290+
)?;
291+
} else {
292+
if 0 < unchanged_behind {
293+
opts.config.shell().note(format!(
294+
"pass `--verbose` to see {unchanged_behind} unchanged dependencies behind latest"
295+
))?;
296+
}
297+
}
194298
if opts.dry_run {
195299
opts.config
196300
.shell()
@@ -215,73 +319,87 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
215319
}
216320
}
217321

322+
#[derive(Default, Clone, Debug)]
323+
struct ResolvedPackageVersions {
324+
removed: Vec<PackageId>,
325+
added: Vec<PackageId>,
326+
unchanged: Vec<PackageId>,
327+
}
218328
fn compare_dependency_graphs(
219329
previous_resolve: &Resolve,
220330
resolve: &Resolve,
221-
) -> Vec<(Vec<PackageId>, Vec<PackageId>)> {
331+
) -> Vec<ResolvedPackageVersions> {
222332
fn key(dep: PackageId) -> (&'static str, SourceId) {
223333
(dep.name().as_str(), dep.source_id())
224334
}
225335

226-
// Removes all package IDs in `b` from `a`. Note that this is somewhat
227-
// more complicated because the equality for source IDs does not take
228-
// precise versions into account (e.g., git shas), but we want to take
229-
// that into account here.
230-
fn vec_subtract(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
231-
a.iter()
232-
.filter(|a| {
233-
// If this package ID is not found in `b`, then it's definitely
234-
// in the subtracted set.
235-
let Ok(i) = b.binary_search(a) else {
236-
return true;
237-
};
336+
fn vec_subset(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
337+
a.iter().filter(|a| !contains_id(b, a)).cloned().collect()
338+
}
238339

239-
// If we've found `a` in `b`, then we iterate over all instances
240-
// (we know `b` is sorted) and see if they all have different
241-
// precise versions. If so, then `a` isn't actually in `b` so
242-
// we'll let it through.
243-
//
244-
// Note that we only check this for non-registry sources,
245-
// however, as registries contain enough version information in
246-
// the package ID to disambiguate.
247-
if a.source_id().is_registry() {
248-
return false;
249-
}
250-
b[i..]
251-
.iter()
252-
.take_while(|b| a == b)
253-
.all(|b| !a.source_id().has_same_precise_as(b.source_id()))
254-
})
255-
.cloned()
256-
.collect()
340+
fn vec_intersection(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
341+
a.iter().filter(|a| contains_id(b, a)).cloned().collect()
342+
}
343+
344+
// Check if a PackageId is present `b` from `a`.
345+
//
346+
// Note that this is somewhat more complicated because the equality for source IDs does not
347+
// take precise versions into account (e.g., git shas), but we want to take that into
348+
// account here.
349+
fn contains_id(haystack: &[PackageId], needle: &PackageId) -> bool {
350+
let Ok(i) = haystack.binary_search(needle) else {
351+
return false;
352+
};
353+
354+
// If we've found `a` in `b`, then we iterate over all instances
355+
// (we know `b` is sorted) and see if they all have different
356+
// precise versions. If so, then `a` isn't actually in `b` so
357+
// we'll let it through.
358+
//
359+
// Note that we only check this for non-registry sources,
360+
// however, as registries contain enough version information in
361+
// the package ID to disambiguate.
362+
if needle.source_id().is_registry() {
363+
return true;
364+
}
365+
haystack[i..]
366+
.iter()
367+
.take_while(|b| &needle == b)
368+
.any(|b| needle.source_id().has_same_precise_as(b.source_id()))
257369
}
258370

259371
// Map `(package name, package source)` to `(removed versions, added versions)`.
260372
let mut changes = BTreeMap::new();
261-
let empty = (Vec::new(), Vec::new());
373+
let empty = ResolvedPackageVersions::default();
262374
for dep in previous_resolve.iter() {
263375
changes
264376
.entry(key(dep))
265377
.or_insert_with(|| empty.clone())
266-
.0
378+
.removed
267379
.push(dep);
268380
}
269381
for dep in resolve.iter() {
270382
changes
271383
.entry(key(dep))
272384
.or_insert_with(|| empty.clone())
273-
.1
385+
.added
274386
.push(dep);
275387
}
276388

277389
for v in changes.values_mut() {
278-
let (ref mut old, ref mut new) = *v;
390+
let ResolvedPackageVersions {
391+
removed: ref mut old,
392+
added: ref mut new,
393+
unchanged: ref mut other,
394+
} = *v;
279395
old.sort();
280396
new.sort();
281-
let removed = vec_subtract(old, new);
282-
let added = vec_subtract(new, old);
397+
let removed = vec_subset(old, new);
398+
let added = vec_subset(new, old);
399+
let unchanged = vec_intersection(new, old);
283400
*old = removed;
284401
*new = added;
402+
*other = unchanged;
285403
}
286404
debug!("{:#?}", changes);
287405

tests/testsuite/git.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1372,7 +1372,7 @@ fn dep_with_changed_submodule() {
13721372
sleep_ms(1000);
13731373
// Update the dependency and carry on!
13741374
println!("update");
1375-
p.cargo("update -v")
1375+
p.cargo("update")
13761376
.with_stderr("")
13771377
.with_stderr(&format!(
13781378
"[UPDATING] git repository `{}`\n\

tests/testsuite/local_registry.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ fn multiple_versions() {
183183
.file("src/lib.rs", "pub fn bar() {}")
184184
.publish();
185185

186-
p.cargo("update -v")
186+
p.cargo("update")
187187
.with_stderr("[UPDATING] bar v0.1.0 -> v0.2.0")
188188
.run();
189189
}

tests/testsuite/patch.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,7 @@ fn can_update_with_alt_reg() {
24302430
"\
24312431
[UPDATING] `alternative` index
24322432
[UPDATING] `dummy-registry` index
2433+
[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest
24332434
",
24342435
)
24352436
.run();

tests/testsuite/registry.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,7 @@ fn update_multiple_packages() {
15721572
[UPDATING] `[..]` index
15731573
[UPDATING] a v0.1.0 -> v0.1.1
15741574
[UPDATING] b v0.1.0 -> v0.1.1
1575+
[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest
15751576
",
15761577
)
15771578
.run();

tests/testsuite/replace.rs

+2
Original file line numberDiff line numberDiff line change
@@ -539,13 +539,15 @@ fn override_adds_some_deps() {
539539
"\
540540
[UPDATING] git repository `file://[..]`
541541
[UPDATING] `dummy-registry` index
542+
[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest
542543
",
543544
)
544545
.run();
545546
p.cargo("update https://github.com/rust-lang/crates.io-index#bar")
546547
.with_stderr(
547548
"\
548549
[UPDATING] `dummy-registry` index
550+
[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest
549551
",
550552
)
551553
.run();

0 commit comments

Comments
 (0)