Skip to main content

cargo/ops/
cargo_update.rs

1use crate::context::CargoResolverConfig;
2use crate::context::GlobalContext;
3use crate::context::IncompatiblePublishAge;
4use crate::ops;
5use crate::resolver::PublishAgePolicy;
6use crate::resolver::Resolve;
7use crate::resolver::features::{CliFeatures, HasDevUnits};
8use crate::sources::IndexSummary;
9use crate::sources::source::QueryKind;
10use crate::util::cache_lock::CacheLockMode;
11use crate::util::{CargoResult, VersionExt};
12use crate::util::{OptVersionReq, style};
13use crate::workspace::Registry as _;
14use crate::workspace::dependency::Dependency;
15use crate::workspace::editor::dependency::{MaybeWorkspace, Source};
16use crate::workspace::editor::manifest::LocalManifest;
17use crate::workspace::editor::upgrade::upgrade_requirement;
18use crate::workspace::registry::PackageRegistry;
19use crate::workspace::{PackageId, PackageIdSpec, PackageIdSpecQuery};
20use crate::workspace::{SourceId, Workspace};
21
22use crate::util::data_structures::{HashMap, HashSet};
23use crate::util::data_structures::{IndexMap, IndexSet};
24use anyhow::Context as _;
25use cargo_util_schemas::core::PartialVersion;
26use cargo_util_terminal::Verbosity;
27use itertools::Itertools;
28use semver::{Op, Version, VersionReq};
29use std::cmp::Ordering;
30use std::collections::BTreeMap;
31use tracing::{debug, trace};
32
33pub type UpgradeMap = HashMap<(String, SourceId), Version>;
34
35pub struct UpdateOptions<'a> {
36    pub gctx: &'a GlobalContext,
37    pub to_update: Vec<String>,
38    pub precise: Option<&'a str>,
39    pub recursive: bool,
40    pub dry_run: bool,
41    pub workspace: bool,
42}
43
44pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> {
45    let mut registry = ws.package_registry()?;
46    let previous_resolve = None;
47    let mut resolve = ops::resolve_with_previous(
48        &mut registry,
49        ws,
50        &CliFeatures::new_all(true),
51        HasDevUnits::Yes,
52        previous_resolve,
53        None,
54        &[],
55        true,
56    )?;
57    ops::write_pkg_lockfile(ws, &mut resolve)?;
58    print_lockfile_changes(ws, previous_resolve, &resolve, &mut registry)?;
59    Ok(())
60}
61
62pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoResult<()> {
63    if opts.recursive && opts.precise.is_some() {
64        anyhow::bail!("cannot specify both recursive and precise simultaneously")
65    }
66
67    if ws.members().count() == 0 {
68        anyhow::bail!("you can't generate a lockfile for an empty workspace.")
69    }
70
71    // Updates often require a lot of modifications to the registry, so ensure
72    // that we're synchronized against other Cargos.
73    let _lock = ws
74        .gctx()
75        .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
76
77    let previous_resolve = match ops::load_pkg_lockfile(ws)? {
78        Some(resolve) => resolve,
79        None => {
80            match opts.precise {
81                None => return generate_lockfile(ws),
82
83                // Precise option specified, so calculate a previous_resolve required
84                // by precise package update later.
85                Some(_) => {
86                    let mut registry = ws.package_registry()?;
87                    ops::resolve_with_previous(
88                        &mut registry,
89                        ws,
90                        &CliFeatures::new_all(true),
91                        HasDevUnits::Yes,
92                        None,
93                        None,
94                        &[],
95                        true,
96                    )?
97                }
98            }
99        }
100    };
101    let mut registry = ws.package_registry()?;
102    let mut to_avoid = HashSet::default();
103
104    if opts.to_update.is_empty() {
105        if !opts.workspace {
106            to_avoid.extend(previous_resolve.iter());
107            to_avoid.extend(previous_resolve.unused_patches());
108        }
109    } else {
110        let mut sources = Vec::new();
111        for name in opts.to_update.iter() {
112            let pid = previous_resolve.query(name)?;
113            if opts.recursive {
114                fill_with_deps(
115                    &previous_resolve,
116                    pid,
117                    &mut to_avoid,
118                    &mut HashSet::default(),
119                );
120            } else {
121                to_avoid.insert(pid);
122                sources.push(match opts.precise {
123                    Some(precise) => {
124                        // TODO: see comment in `resolve.rs` as well, but this
125                        //       seems like a pretty hokey reason to single out
126                        //       the registry as well.
127                        if pid.source_id().is_registry() {
128                            pid.source_id().with_precise_registry_version(
129                                pid.name(),
130                                pid.version().clone(),
131                                precise,
132                            )?
133                        } else {
134                            pid.source_id().with_git_precise(Some(precise.to_string()))
135                        }
136                    }
137                    None => pid.source_id().without_precise(),
138                });
139            }
140            if let Ok(unused_id) =
141                PackageIdSpec::query_str(name, previous_resolve.unused_patches().iter().cloned())
142            {
143                to_avoid.insert(unused_id);
144            }
145        }
146
147        // Mirror `--workspace` and never avoid workspace members.
148        // Filtering them out here so the above processes them normally
149        // so their dependencies can be updated as requested
150        to_avoid.retain(|id| {
151            for package in ws.members() {
152                let member_id = package.package_id();
153                // Skip checking the `version` because `previous_resolve` might have a stale
154                // value.
155                // When dealing with workspace members, the other fields should be a
156                // sufficiently unique match.
157                if id.name() == member_id.name() && id.source_id() == member_id.source_id() {
158                    return false;
159                }
160            }
161            true
162        });
163
164        registry.add_sources(sources)?;
165    }
166
167    // Here we place an artificial limitation that all non-registry sources
168    // cannot be locked at more than one revision. This means that if a Git
169    // repository provides more than one package, they must all be updated in
170    // step when any of them are updated.
171    //
172    // TODO: this seems like a hokey reason to single out the registry as being
173    // different.
174    let to_avoid_sources: HashSet<_> = to_avoid
175        .iter()
176        .map(|p| p.source_id())
177        .filter(|s| !s.is_registry())
178        .collect();
179
180    let keep = |p: &PackageId| !to_avoid_sources.contains(&p.source_id()) && !to_avoid.contains(p);
181
182    let mut resolve = ops::resolve_with_previous(
183        &mut registry,
184        ws,
185        &CliFeatures::new_all(true),
186        HasDevUnits::Yes,
187        Some(&previous_resolve),
188        Some(&keep),
189        &[],
190        true,
191    )?;
192
193    print_lockfile_updates(
194        ws,
195        &previous_resolve,
196        &resolve,
197        opts.precise.is_some(),
198        &mut registry,
199    )?;
200    if opts.dry_run {
201        opts.gctx
202            .shell()
203            .warn("not updating lockfile due to dry run")?;
204    } else {
205        ops::write_pkg_lockfile(ws, &mut resolve)?;
206    }
207    Ok(())
208}
209
210/// Prints lockfile change statuses.
211///
212/// This would acquire the package-cache lock, as it may update the index to
213/// show users latest available versions.
214pub fn print_lockfile_changes(
215    ws: &Workspace<'_>,
216    previous_resolve: Option<&Resolve>,
217    resolve: &Resolve,
218    registry: &mut PackageRegistry<'_>,
219) -> CargoResult<()> {
220    let _lock = ws
221        .gctx()
222        .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
223    if let Some(previous_resolve) = previous_resolve {
224        print_lockfile_sync(ws, previous_resolve, resolve, registry)
225    } else {
226        print_lockfile_generation(ws, resolve, registry)
227    }
228}
229pub fn upgrade_manifests(
230    ws: &mut Workspace<'_>,
231    to_update: &Vec<String>,
232) -> CargoResult<UpgradeMap> {
233    let gctx = ws.gctx();
234    let mut upgrades = HashMap::default();
235    let mut upgrade_messages = HashSet::default();
236
237    let to_update = to_update
238        .iter()
239        .map(|spec| {
240            PackageIdSpec::parse(spec)
241                .with_context(|| format!("invalid package ID specification: `{spec}`"))
242        })
243        .collect::<Result<Vec<_>, _>>()?;
244
245    // Updates often require a lot of modifications to the registry, so ensure
246    // that we're synchronized against other Cargos.
247    let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
248
249    let mut registry = ws.package_registry()?;
250    registry.lock_patches();
251
252    let mut remaining_specs: IndexSet<_> = to_update.iter().cloned().collect();
253
254    for member in ws.members_mut().sorted() {
255        debug!("upgrading manifest for `{}`", member.name());
256
257        *member.manifest_mut().summary_mut() = member
258            .manifest()
259            .summary()
260            .clone()
261            .try_map_dependencies(|d| {
262                upgrade_dependency(
263                    &gctx,
264                    &to_update,
265                    &mut registry,
266                    &mut upgrades,
267                    &mut upgrade_messages,
268                    &mut remaining_specs,
269                    d,
270                )
271            })?;
272    }
273
274    if !remaining_specs.is_empty() {
275        let previous_resolve = ops::load_pkg_lockfile(ws)?;
276        let plural = if remaining_specs.len() == 1 { "" } else { "s" };
277
278        let mut error_msg = format!(
279            "package ID specification{plural} did not match any direct dependencies that could be upgraded"
280        );
281
282        let mut transitive_specs = Vec::new();
283        for spec in &remaining_specs {
284            error_msg.push_str(&format!("\n  {spec}"));
285
286            // Check if spec is in the lockfile (could be transitive)
287            let in_lockfile = if let Some(ref resolve) = previous_resolve {
288                spec.query(resolve.iter()).is_ok()
289            } else {
290                false
291            };
292
293            // Check if spec matches any direct dependency in the workspace
294            let matches_direct_dep = ws.members().any(|member| {
295                member.dependencies().iter().any(|dep| {
296                    spec.name() == dep.package_name().as_str()
297                        && dep.source_id().is_registry()
298                        && spec.url().map_or(true, |url| url == dep.source_id().url())
299                        && spec
300                            .version()
301                            .map_or(true, |v| dep.version_req().matches(&v))
302                })
303            });
304
305            // Track transitive specs for notes at the end
306            if in_lockfile && !matches_direct_dep {
307                transitive_specs.push(spec);
308            }
309        }
310
311        for spec in transitive_specs {
312            error_msg.push_str(&format!(
313                "\nnote: `{spec}` exists as a transitive dependency but those are not available for upgrading through `--breaking`"
314            ));
315        }
316
317        anyhow::bail!("{error_msg}");
318    }
319
320    Ok(upgrades)
321}
322
323fn upgrade_dependency(
324    gctx: &GlobalContext,
325    to_update: &Vec<PackageIdSpec>,
326    registry: &mut PackageRegistry<'_>,
327    upgrades: &mut UpgradeMap,
328    upgrade_messages: &mut HashSet<String>,
329    remaining_specs: &mut IndexSet<PackageIdSpec>,
330    dependency: Dependency,
331) -> CargoResult<Dependency> {
332    let name = dependency.package_name();
333    let renamed_to = dependency.name_in_toml();
334
335    if name != renamed_to {
336        trace!("skipping dependency renamed from `{name}` to `{renamed_to}`");
337        return Ok(dependency);
338    }
339
340    if !to_update.is_empty()
341        && !to_update.iter().any(|spec| {
342            spec.name() == name.as_str()
343                && dependency.source_id().is_registry()
344                && spec
345                    .url()
346                    .map_or(true, |url| url == dependency.source_id().url())
347                && spec
348                    .version()
349                    .map_or(true, |v| dependency.version_req().matches(&v))
350        })
351    {
352        trace!("skipping dependency `{name}` not selected for upgrading");
353        return Ok(dependency);
354    }
355
356    if !dependency.source_id().is_registry() {
357        trace!("skipping non-registry dependency: {name}");
358        return Ok(dependency);
359    }
360
361    let version_req = dependency.version_req();
362
363    let OptVersionReq::Req(current) = version_req else {
364        trace!("skipping dependency `{name}` without a simple version requirement: {version_req}");
365        return Ok(dependency);
366    };
367
368    let [comparator] = &current.comparators[..] else {
369        trace!(
370            "skipping dependency `{name}` with multiple version comparators: {:?}",
371            &current.comparators
372        );
373        return Ok(dependency);
374    };
375
376    if comparator.op != Op::Caret {
377        trace!("skipping non-caret dependency `{name}`: {comparator}");
378        return Ok(dependency);
379    }
380
381    let query = crate::workspace::dependency::Dependency::parse(
382        name,
383        None,
384        dependency.source_id().clone(),
385    )?;
386
387    let possibilities = crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?;
388
389    let latest = if !possibilities.is_empty() {
390        possibilities
391            .iter()
392            .filter_map(|s| match s {
393                IndexSummary::Candidate(s) => Some(s),
394                _ => None,
395            })
396            .map(|s| s.version())
397            .filter(|v| !v.is_prerelease())
398            .max()
399    } else {
400        None
401    };
402
403    let Some(latest) = latest else {
404        trace!("skipping dependency `{name}` without any published versions");
405        return Ok(dependency);
406    };
407
408    if current.matches(&latest) {
409        trace!("skipping dependency `{name}` without a breaking update available");
410        return Ok(dependency);
411    }
412
413    let Some((new_req_string, _)) = upgrade_requirement(&current.to_string(), latest)? else {
414        trace!("skipping dependency `{name}` because the version requirement didn't change");
415        return Ok(dependency);
416    };
417
418    let upgrade_message = format!("{name} {current} -> {new_req_string}");
419    trace!(upgrade_message);
420
421    if upgrade_messages.insert(upgrade_message.clone()) {
422        gctx.shell()
423            .status_with_color("Upgrading", &upgrade_message, &style::GOOD)?;
424    }
425
426    upgrades.insert((name.to_string(), dependency.source_id()), latest.clone());
427
428    // Remove this spec from remaining_specs since we successfully upgraded it
429    remaining_specs
430        .retain(|spec| !(spec.name() == name.as_str() && dependency.source_id().is_registry()));
431
432    let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
433    let mut dep = dependency.clone();
434    dep.set_version_req(req);
435    Ok(dep)
436}
437
438/// Update manifests with upgraded versions, and write to disk. Based on
439/// cargo-edit. Returns true if any file has changed.
440///
441/// Some of the checks here are duplicating checks already done in
442/// `upgrade_manifests/upgrade_dependency`. Why? Let's say `upgrade_dependency` has
443/// found that dependency foo was eligible for an upgrade. But foo can occur in
444/// multiple manifest files, and even multiple times in the same manifest file,
445/// and may be pinned, renamed, etc. in some of the instances. So we still need
446/// to check here which dependencies to actually modify. So why not drop the
447/// upgrade map and redo all checks here? Because then we'd have to query the
448/// registries again to find the latest versions.
449pub fn write_manifest_upgrades(
450    ws: &Workspace<'_>,
451    upgrades: &UpgradeMap,
452    dry_run: bool,
453) -> CargoResult<bool> {
454    if upgrades.is_empty() {
455        return Ok(false);
456    }
457
458    let mut any_file_has_changed = false;
459
460    let items = std::iter::once((ws.root_manifest(), ws.unstable_features()))
461        .chain(ws.members().map(|member| {
462            (
463                member.manifest_path(),
464                member.manifest().unstable_features(),
465            )
466        }))
467        .collect::<Vec<_>>();
468
469    for (manifest_path, unstable_features) in items {
470        trace!("updating TOML manifest at `{manifest_path:?}` with upgraded dependencies");
471
472        let crate_root = manifest_path
473            .parent()
474            .expect("manifest path is absolute")
475            .to_owned();
476
477        let mut local_manifest = LocalManifest::try_new(&manifest_path)?;
478        let mut manifest_has_changed = false;
479
480        for dep_table in local_manifest.get_dependency_tables_mut() {
481            for (mut dep_key, dep_item) in dep_table.iter_mut() {
482                let dep_key_str = dep_key.get();
483                let dependency = crate::workspace::editor::dependency::Dependency::from_toml(
484                    ws.gctx(),
485                    ws.root(),
486                    &manifest_path,
487                    unstable_features,
488                    dep_key_str,
489                    dep_item,
490                )?;
491                let name = &dependency.name;
492
493                if let Some(renamed_to) = dependency.rename {
494                    trace!("skipping dependency renamed from `{name}` to `{renamed_to}`");
495                    continue;
496                }
497
498                let Some(current) = dependency.version() else {
499                    trace!("skipping dependency without a version: {name}");
500                    continue;
501                };
502
503                let (MaybeWorkspace::Other(source_id), Some(Source::Registry(source))) =
504                    (dependency.source_id(ws.gctx())?, dependency.source())
505                else {
506                    trace!("skipping non-registry dependency: {name}");
507                    continue;
508                };
509
510                let Some(latest) = upgrades.get(&(name.to_owned(), source_id)) else {
511                    trace!("skipping dependency without an upgrade: {name}");
512                    continue;
513                };
514
515                let Some((new_req_string, new_req)) = upgrade_requirement(current, latest)? else {
516                    trace!(
517                        "skipping dependency `{name}` because the version requirement didn't change"
518                    );
519                    continue;
520                };
521
522                let [comparator] = &new_req.comparators[..] else {
523                    trace!(
524                        "skipping dependency `{}` with multiple version comparators: {:?}",
525                        name, new_req.comparators
526                    );
527                    continue;
528                };
529
530                if comparator.op != Op::Caret {
531                    trace!("skipping non-caret dependency `{}`: {}", name, comparator);
532                    continue;
533                }
534
535                let mut dep = dependency.clone();
536                let mut source = source.clone();
537                source.version = new_req_string;
538                dep.source = Some(Source::Registry(source));
539
540                trace!("upgrading dependency {name}");
541                dep.update_toml(
542                    ws.gctx(),
543                    ws.root(),
544                    &crate_root,
545                    unstable_features,
546                    &mut dep_key,
547                    dep_item,
548                )?;
549                manifest_has_changed = true;
550                any_file_has_changed = true;
551            }
552        }
553
554        if manifest_has_changed && !dry_run {
555            debug!("writing upgraded manifest to {}", manifest_path.display());
556            local_manifest.write()?;
557        }
558    }
559
560    Ok(any_file_has_changed)
561}
562
563fn print_lockfile_generation(
564    ws: &Workspace<'_>,
565    resolve: &Resolve,
566    registry: &mut PackageRegistry<'_>,
567) -> CargoResult<()> {
568    let mut changes = PackageChange::new(ws, resolve);
569    let num_pkgs: usize = changes
570        .values()
571        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
572        .count();
573    if num_pkgs == 0 {
574        // nothing worth reporting
575        return Ok(());
576    }
577    annotate_required_rust_version(ws, resolve, &mut changes);
578    let publish_age = publish_age_policy_for_report(ws);
579
580    status_locking(ws, publish_age.as_ref(), num_pkgs)?;
581    for change in changes.values() {
582        if change.is_member.unwrap_or(false) {
583            continue;
584        };
585        match change.kind {
586            PackageChangeKind::Added => {
587                let possibilities = if let Some(query) = change.alternatives_query() {
588                    crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
589                } else {
590                    vec![]
591                };
592
593                let required_rust_version = report_required_rust_version(resolve, change);
594                let too_new = report_too_new(resolve, change, publish_age.as_ref());
595                let latest = report_latest(&possibilities, change, publish_age.as_ref());
596                let note = required_rust_version.or(too_new).or(latest);
597
598                if let Some(note) = note {
599                    ws.gctx().shell().status_with_color(
600                        change.kind.status(),
601                        format!("{change}{note}"),
602                        &change.kind.style(),
603                    )?;
604                }
605            }
606            PackageChangeKind::Upgraded
607            | PackageChangeKind::Downgraded
608            | PackageChangeKind::Removed
609            | PackageChangeKind::Unchanged => {
610                unreachable!("without a previous resolve, everything should be added")
611            }
612        }
613    }
614
615    Ok(())
616}
617
618fn print_lockfile_sync(
619    ws: &Workspace<'_>,
620    previous_resolve: &Resolve,
621    resolve: &Resolve,
622    registry: &mut PackageRegistry<'_>,
623) -> CargoResult<()> {
624    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
625    let num_pkgs: usize = changes
626        .values()
627        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
628        .count();
629    if num_pkgs == 0 {
630        // nothing worth reporting
631        return Ok(());
632    }
633    annotate_required_rust_version(ws, resolve, &mut changes);
634    let publish_age = publish_age_policy_for_report(ws);
635
636    status_locking(ws, publish_age.as_ref(), num_pkgs)?;
637    for change in changes.values() {
638        if change.is_member.unwrap_or(false) {
639            continue;
640        };
641        match change.kind {
642            PackageChangeKind::Added
643            | PackageChangeKind::Upgraded
644            | PackageChangeKind::Downgraded => {
645                let possibilities = if let Some(query) = change.alternatives_query() {
646                    crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
647                } else {
648                    vec![]
649                };
650
651                let required_rust_version = report_required_rust_version(resolve, change);
652                let too_new = report_too_new(resolve, change, publish_age.as_ref());
653                let latest = report_latest(&possibilities, change, publish_age.as_ref());
654                let note = required_rust_version
655                    .or(too_new)
656                    .or(latest)
657                    .unwrap_or_default();
658
659                ws.gctx().shell().status_with_color(
660                    change.kind.status(),
661                    format!("{change}{note}"),
662                    &change.kind.style(),
663                )?;
664            }
665            PackageChangeKind::Removed | PackageChangeKind::Unchanged => {}
666        }
667    }
668
669    Ok(())
670}
671
672fn print_lockfile_updates(
673    ws: &Workspace<'_>,
674    previous_resolve: &Resolve,
675    resolve: &Resolve,
676    precise: bool,
677    registry: &mut PackageRegistry<'_>,
678) -> CargoResult<()> {
679    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
680    let num_pkgs: usize = changes
681        .values()
682        .filter(|change| change.kind.is_new())
683        .count();
684    annotate_required_rust_version(ws, resolve, &mut changes);
685    let publish_age = publish_age_policy_for_report(ws);
686
687    if !precise {
688        status_locking(ws, publish_age.as_ref(), num_pkgs)?;
689    }
690    let mut unchanged_behind = 0;
691    for change in changes.values() {
692        let possibilities = if let Some(query) = change.alternatives_query() {
693            crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
694        } else {
695            vec![]
696        };
697
698        match change.kind {
699            PackageChangeKind::Added
700            | PackageChangeKind::Upgraded
701            | PackageChangeKind::Downgraded => {
702                let required_rust_version = report_required_rust_version(resolve, change);
703                let too_new = report_too_new(resolve, change, publish_age.as_ref());
704                let latest = report_latest(&possibilities, change, publish_age.as_ref());
705                let note = required_rust_version
706                    .or(too_new)
707                    .or(latest)
708                    .unwrap_or_default();
709
710                ws.gctx().shell().status_with_color(
711                    change.kind.status(),
712                    format!("{change}{note}"),
713                    &change.kind.style(),
714                )?;
715            }
716            PackageChangeKind::Removed => {
717                ws.gctx().shell().status_with_color(
718                    change.kind.status(),
719                    format!("{change}"),
720                    &change.kind.style(),
721                )?;
722            }
723            PackageChangeKind::Unchanged => {
724                let required_rust_version = report_required_rust_version(resolve, change);
725                let too_new = report_too_new(resolve, change, publish_age.as_ref());
726                let latest = report_latest(&possibilities, change, publish_age.as_ref());
727                let note = required_rust_version
728                    .as_deref()
729                    .or(too_new.as_deref())
730                    .or(latest.as_deref());
731
732                if let Some(note) = note {
733                    if latest.is_some() {
734                        unchanged_behind += 1;
735                    }
736                    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
737                        ws.gctx().shell().status_with_color(
738                            change.kind.status(),
739                            format!("{change}{note}"),
740                            &change.kind.style(),
741                        )?;
742                    }
743                }
744            }
745        }
746    }
747
748    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
749        ws.gctx()
750            .shell()
751            .note("to see how you depend on a package, run `cargo tree --invert <dep>@<ver>`")?;
752    } else {
753        if 0 < unchanged_behind {
754            ws.gctx().shell().note(format!(
755                "pass `--verbose` to see {unchanged_behind} unchanged dependencies behind latest"
756            ))?;
757        }
758    }
759
760    Ok(())
761}
762
763fn status_locking(
764    ws: &Workspace<'_>,
765    publish_age: Option<&PublishAgePolicy>,
766    num_pkgs: usize,
767) -> CargoResult<()> {
768    use std::fmt::Write as _;
769
770    let resolver_config = ws.gctx().get::<Option<CargoResolverConfig>>("resolver")?;
771    let deny_min_publish_age = resolver_config
772        .and_then(|c| c.incompatible_publish_age)
773        .is_none_or(|v| v == IncompatiblePublishAge::Deny);
774    let publish_age = publish_age.filter(|_| deny_min_publish_age);
775    let publish_time = ws.resolve_publish_time();
776
777    let plural = if num_pkgs == 1 { "" } else { "s" };
778
779    let mut cfg = String::new();
780    // Don't have a good way to describe `direct_minimal_versions` atm
781    if !ws.gctx().cli_unstable().direct_minimal_versions {
782        write!(&mut cfg, " to")?;
783        if ws.gctx().cli_unstable().minimal_versions {
784            write!(&mut cfg, " lowest")?;
785        } else {
786            write!(&mut cfg, " highest")?;
787        }
788
789        if let Some(rust_version) = required_rust_version(ws) {
790            write!(&mut cfg, " Rust {rust_version}")?;
791        }
792        write!(&mut cfg, " compatible version{plural}")?;
793        match (publish_age, publish_time) {
794            (Some(publish_age), Some(publish_time)) => {
795                write!(
796                    &mut cfg,
797                    " as of {} before {publish_time}",
798                    publish_age
799                        .common_min_publish_age()
800                        .map(|a| a.age_label())
801                        .unwrap_or_else(|| "min-publish-age".to_owned())
802                )?;
803            }
804            (Some(publish_age), None) => {
805                write!(
806                    &mut cfg,
807                    " as of {}",
808                    publish_age
809                        .common_min_publish_age()
810                        .map(|a| format!("{} ago", a.age_label()))
811                        .unwrap_or_else(|| "min-publish-age".to_owned())
812                )?;
813            }
814            (None, Some(publish_time)) => {
815                write!(&mut cfg, " as of {publish_time}")?;
816            }
817            (None, None) => {}
818        }
819    }
820
821    ws.gctx()
822        .shell()
823        .status("Locking", format!("{num_pkgs} package{plural}{cfg}"))?;
824    Ok(())
825}
826
827fn required_rust_version(ws: &Workspace<'_>) -> Option<PartialVersion> {
828    if !ws.resolve_honors_rust_version() {
829        return None;
830    }
831
832    if let Some(ver) = ws.lowest_rust_version() {
833        Some(ver.to_partial())
834    } else {
835        let rustc = ws.gctx().load_global_rustc(Some(ws)).ok()?;
836        let rustc_version = rustc.version.clone().into();
837        Some(rustc_version)
838    }
839}
840
841fn publish_age_policy_for_report(ws: &Workspace<'_>) -> Option<PublishAgePolicy> {
842    if !ws.resolve_honors_publish_age() {
843        return None;
844    }
845    PublishAgePolicy::for_report(ws.resolve_publish_time(), ws.gctx())
846        .ok()
847        .flatten()
848}
849
850fn report_required_rust_version(resolve: &Resolve, change: &PackageChange) -> Option<String> {
851    if change.package_id.source_id().is_path() {
852        return None;
853    }
854    let summary = resolve.summary(change.package_id);
855    let package_rust_version = summary.rust_version()?;
856    let required_rust_version = change.required_rust_version.as_ref()?;
857    if package_rust_version.is_compatible_with(required_rust_version) {
858        return None;
859    }
860
861    let error = style::ERROR;
862    Some(format!(
863        " {error}(requires Rust {package_rust_version}){error:#}"
864    ))
865}
866
867/// Reports when the selected version is too new and violates `min-publish-age` config.
868fn report_too_new(
869    resolve: &Resolve,
870    change: &PackageChange,
871    publish_age: Option<&PublishAgePolicy>,
872) -> Option<String> {
873    let summary = resolve.summary(change.package_id);
874    let note = publish_age?.too_new(summary)?.note();
875
876    let warn = style::WARN;
877    Some(format!(" {warn}({note}){warn:#}"))
878}
879
880fn report_latest(
881    possibilities: &[IndexSummary],
882    change: &PackageChange,
883    publish_age: Option<&PublishAgePolicy>,
884) -> Option<String> {
885    let package_id = change.package_id;
886    if !package_id.source_id().is_registry() {
887        return None;
888    }
889
890    let version_req = package_id.version().to_caret_req();
891    let required_rust_version = change.required_rust_version.as_ref();
892
893    let publish_note = |summary| {
894        let age = publish_age?.too_new(summary)?.age_label();
895        Some(format!(", published {age} ago"))
896    };
897
898    let compat_ver_compat_msrv_summary = possibilities
899        .iter()
900        .filter_map(|s| match s {
901            IndexSummary::Candidate(s) => Some(s),
902            _ => None,
903        })
904        .filter(|s| {
905            if let (Some(summary_rust_version), Some(required_rust_version)) =
906                (s.rust_version(), required_rust_version)
907            {
908                summary_rust_version.is_compatible_with(required_rust_version)
909            } else {
910                true
911            }
912        })
913        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
914        .max_by_key(|s| s.version());
915    if let Some(summary) = compat_ver_compat_msrv_summary {
916        let warn = style::WARN;
917        let version = summary.version();
918        let publish_note = publish_note(summary).unwrap_or_default();
919        let report = format!(" {warn}(available: v{version}{publish_note}){warn:#}");
920        return Some(report);
921    }
922
923    if !change.is_transitive.unwrap_or(true) {
924        let incompat_ver_compat_msrv_summary = possibilities
925            .iter()
926            .filter_map(|s| match s {
927                IndexSummary::Candidate(s) => Some(s),
928                _ => None,
929            })
930            .filter(|s| {
931                if let (Some(summary_rust_version), Some(required_rust_version)) =
932                    (s.rust_version(), required_rust_version)
933                {
934                    summary_rust_version.is_compatible_with(required_rust_version)
935                } else {
936                    true
937                }
938            })
939            .filter(|s| is_latest(s.version(), package_id.version()))
940            .max_by_key(|s| s.version());
941        if let Some(summary) = incompat_ver_compat_msrv_summary {
942            let warn = style::WARN;
943            let version = summary.version();
944            let publish_note = publish_note(summary).unwrap_or_default();
945            let report = format!(" {warn}(available: v{version}{publish_note}){warn:#}");
946            return Some(report);
947        }
948    }
949
950    let compat_ver_summary = possibilities
951        .iter()
952        .filter_map(|s| match s {
953            IndexSummary::Candidate(s) => Some(s),
954            _ => None,
955        })
956        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
957        .max_by_key(|s| s.version());
958    if let Some(summary) = compat_ver_summary {
959        let msrv_note = summary
960            .rust_version()
961            .map(|rv| format!(", requires Rust {rv}"))
962            .unwrap_or_default();
963        let warn = style::NOP;
964        let version = summary.version();
965        let publish_note = publish_note(summary).unwrap_or_default();
966        let report = format!(" {warn}(available: v{version}{msrv_note}{publish_note}){warn:#}");
967        return Some(report);
968    }
969
970    if !change.is_transitive.unwrap_or(true) {
971        let incompat_ver_summary = possibilities
972            .iter()
973            .filter_map(|s| match s {
974                IndexSummary::Candidate(s) => Some(s),
975                _ => None,
976            })
977            .filter(|s| is_latest(s.version(), package_id.version()))
978            .max_by_key(|s| s.version());
979        if let Some(summary) = incompat_ver_summary {
980            let msrv_note = summary
981                .rust_version()
982                .map(|rv| format!(", requires Rust {rv}"))
983                .unwrap_or_default();
984            let warn = style::NOP;
985            let version = summary.version();
986            let publish_note = publish_note(summary).unwrap_or_default();
987            let report = format!(" {warn}(available: v{version}{msrv_note}{publish_note}){warn:#}");
988            return Some(report);
989        }
990    }
991
992    None
993}
994
995fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
996    current < candidate
997                // Only match pre-release if major.minor.patch are the same
998                && (candidate.pre.is_empty()
999                    || (candidate.major == current.major
1000                        && candidate.minor == current.minor
1001                        && candidate.patch == current.patch))
1002}
1003
1004fn fill_with_deps<'a>(
1005    resolve: &'a Resolve,
1006    dep: PackageId,
1007    set: &mut HashSet<PackageId>,
1008    visited: &mut HashSet<PackageId>,
1009) {
1010    if !visited.insert(dep) {
1011        return;
1012    }
1013    set.insert(dep);
1014    for (dep, _) in resolve.deps_not_replaced(dep) {
1015        fill_with_deps(resolve, dep, set, visited);
1016    }
1017}
1018
1019#[derive(Clone, Debug)]
1020struct PackageChange {
1021    package_id: PackageId,
1022    previous_id: Option<PackageId>,
1023    kind: PackageChangeKind,
1024    is_member: Option<bool>,
1025    is_transitive: Option<bool>,
1026    required_rust_version: Option<PartialVersion>,
1027}
1028
1029impl PackageChange {
1030    pub fn new(ws: &Workspace<'_>, resolve: &Resolve) -> IndexMap<PackageId, Self> {
1031        let diff = PackageDiff::new(resolve);
1032        Self::with_diff(diff, ws, resolve)
1033    }
1034
1035    pub fn diff(
1036        ws: &Workspace<'_>,
1037        previous_resolve: &Resolve,
1038        resolve: &Resolve,
1039    ) -> IndexMap<PackageId, Self> {
1040        let diff = PackageDiff::diff(previous_resolve, resolve);
1041        Self::with_diff(diff, ws, resolve)
1042    }
1043
1044    fn with_diff(
1045        diff: impl Iterator<Item = PackageDiff>,
1046        ws: &Workspace<'_>,
1047        resolve: &Resolve,
1048    ) -> IndexMap<PackageId, Self> {
1049        let member_ids: HashSet<_> = ws.members().map(|p| p.package_id()).collect();
1050
1051        let mut changes = IndexMap::default();
1052        for diff in diff {
1053            if let Some((previous_id, package_id)) = diff.change() {
1054                // If versions differ only in build metadata, we call it an "update"
1055                // regardless of whether the build metadata has gone up or down.
1056                // This metadata is often stuff like git commit hashes, which are
1057                // not meaningfully ordered.
1058                let kind = if previous_id.version().cmp_precedence(package_id.version())
1059                    == Ordering::Greater
1060                {
1061                    PackageChangeKind::Downgraded
1062                } else {
1063                    PackageChangeKind::Upgraded
1064                };
1065                let is_member = Some(member_ids.contains(&package_id));
1066                let is_transitive = Some(true);
1067                let change = Self {
1068                    package_id,
1069                    previous_id: Some(previous_id),
1070                    kind,
1071                    is_member,
1072                    is_transitive,
1073                    required_rust_version: None,
1074                };
1075                changes.insert(change.package_id, change);
1076            } else {
1077                for package_id in diff.removed {
1078                    let kind = PackageChangeKind::Removed;
1079                    let is_member = None;
1080                    let is_transitive = None;
1081                    let change = Self {
1082                        package_id,
1083                        previous_id: None,
1084                        kind,
1085                        is_member,
1086                        is_transitive,
1087                        required_rust_version: None,
1088                    };
1089                    changes.insert(change.package_id, change);
1090                }
1091                for package_id in diff.added {
1092                    let kind = PackageChangeKind::Added;
1093                    let is_member = Some(member_ids.contains(&package_id));
1094                    let is_transitive = Some(true);
1095                    let change = Self {
1096                        package_id,
1097                        previous_id: None,
1098                        kind,
1099                        is_member,
1100                        is_transitive,
1101                        required_rust_version: None,
1102                    };
1103                    changes.insert(change.package_id, change);
1104                }
1105            }
1106            for package_id in diff.unchanged {
1107                let kind = PackageChangeKind::Unchanged;
1108                let is_member = Some(member_ids.contains(&package_id));
1109                let is_transitive = Some(true);
1110                let change = Self {
1111                    package_id,
1112                    previous_id: None,
1113                    kind,
1114                    is_member,
1115                    is_transitive,
1116                    required_rust_version: None,
1117                };
1118                changes.insert(change.package_id, change);
1119            }
1120        }
1121
1122        for member_id in &member_ids {
1123            let Some(change) = changes.get_mut(member_id) else {
1124                continue;
1125            };
1126            change.is_transitive = Some(false);
1127            for (direct_dep_id, _) in resolve.deps(*member_id) {
1128                let Some(change) = changes.get_mut(&direct_dep_id) else {
1129                    continue;
1130                };
1131                change.is_transitive = Some(false);
1132            }
1133        }
1134
1135        changes
1136    }
1137
1138    /// For querying [`PackageRegistry`] for alternative versions to report to the user
1139    fn alternatives_query(&self) -> Option<crate::workspace::dependency::Dependency> {
1140        if !self.package_id.source_id().is_registry() {
1141            return None;
1142        }
1143
1144        let query = crate::workspace::dependency::Dependency::parse(
1145            self.package_id.name(),
1146            None,
1147            self.package_id.source_id(),
1148        )
1149        .expect("already a valid dependency");
1150        Some(query)
1151    }
1152}
1153
1154impl std::fmt::Display for PackageChange {
1155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1156        let package_id = self.package_id;
1157        if let Some(previous_id) = self.previous_id {
1158            if package_id.source_id().is_git() {
1159                write!(
1160                    f,
1161                    "{previous_id} -> #{}",
1162                    &package_id.source_id().precise_git_fragment().unwrap()[..8],
1163                )
1164            } else {
1165                write!(f, "{previous_id} -> v{}", package_id.version())
1166            }
1167        } else {
1168            write!(f, "{package_id}")
1169        }
1170    }
1171}
1172
1173#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1174enum PackageChangeKind {
1175    Added,
1176    Removed,
1177    Upgraded,
1178    Downgraded,
1179    Unchanged,
1180}
1181
1182impl PackageChangeKind {
1183    pub fn is_new(&self) -> bool {
1184        match self {
1185            Self::Added | Self::Upgraded | Self::Downgraded => true,
1186            Self::Removed | Self::Unchanged => false,
1187        }
1188    }
1189
1190    pub fn status(&self) -> &'static str {
1191        match self {
1192            Self::Added => "Adding",
1193            Self::Removed => "Removing",
1194            Self::Upgraded => "Updating",
1195            Self::Downgraded => "Downgrading",
1196            Self::Unchanged => "Unchanged",
1197        }
1198    }
1199
1200    pub fn style(&self) -> anstyle::Style {
1201        match self {
1202            Self::Added => style::UPDATE_ADDED,
1203            Self::Removed => style::UPDATE_REMOVED,
1204            Self::Upgraded => style::UPDATE_UPGRADED,
1205            Self::Downgraded => style::UPDATE_DOWNGRADED,
1206            Self::Unchanged => style::UPDATE_UNCHANGED,
1207        }
1208    }
1209}
1210
1211/// All resolved versions of a package name within a [`SourceId`]
1212#[derive(Default, Clone, Debug)]
1213pub struct PackageDiff {
1214    removed: Vec<PackageId>,
1215    added: Vec<PackageId>,
1216    unchanged: Vec<PackageId>,
1217}
1218
1219impl PackageDiff {
1220    pub fn new(resolve: &Resolve) -> impl Iterator<Item = Self> {
1221        let mut changes = BTreeMap::new();
1222        let empty = Self::default();
1223        for dep in resolve.iter() {
1224            changes
1225                .entry(Self::key(dep))
1226                .or_insert_with(|| empty.clone())
1227                .added
1228                .push(dep);
1229        }
1230
1231        changes.into_iter().map(|(_, v)| v)
1232    }
1233
1234    pub fn diff(previous_resolve: &Resolve, resolve: &Resolve) -> impl Iterator<Item = Self> {
1235        fn vec_subset(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1236            a.iter().filter(|a| !contains_id(b, a)).cloned().collect()
1237        }
1238
1239        fn vec_intersection(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1240            a.iter().filter(|a| contains_id(b, a)).cloned().collect()
1241        }
1242
1243        // Check if a PackageId is present `b` from `a`.
1244        //
1245        // Note that this is somewhat more complicated because the equality for source IDs does not
1246        // take precise versions into account (e.g., git shas), but we want to take that into
1247        // account here.
1248        fn contains_id(haystack: &[PackageId], needle: &PackageId) -> bool {
1249            let Ok(i) = haystack.binary_search(needle) else {
1250                return false;
1251            };
1252
1253            // If we've found `a` in `b`, then we iterate over all instances
1254            // (we know `b` is sorted) and see if they all have different
1255            // precise versions. If so, then `a` isn't actually in `b` so
1256            // we'll let it through.
1257            //
1258            // Note that we only check this for non-registry sources,
1259            // however, as registries contain enough version information in
1260            // the package ID to disambiguate.
1261            if needle.source_id().is_registry() {
1262                return true;
1263            }
1264            haystack[i..]
1265                .iter()
1266                .take_while(|b| &needle == b)
1267                .any(|b| needle.source_id().has_same_precise_as(b.source_id()))
1268        }
1269
1270        // Map `(package name, package source)` to `(removed versions, added versions)`.
1271        let mut changes = BTreeMap::new();
1272        let empty = Self::default();
1273        for dep in previous_resolve.iter() {
1274            changes
1275                .entry(Self::key(dep))
1276                .or_insert_with(|| empty.clone())
1277                .removed
1278                .push(dep);
1279        }
1280        for dep in resolve.iter() {
1281            changes
1282                .entry(Self::key(dep))
1283                .or_insert_with(|| empty.clone())
1284                .added
1285                .push(dep);
1286        }
1287
1288        for v in changes.values_mut() {
1289            let Self {
1290                removed: ref mut old,
1291                added: ref mut new,
1292                unchanged: ref mut other,
1293            } = *v;
1294            old.sort();
1295            new.sort();
1296            let removed = vec_subset(old, new);
1297            let added = vec_subset(new, old);
1298            let unchanged = vec_intersection(new, old);
1299            *old = removed;
1300            *new = added;
1301            *other = unchanged;
1302        }
1303        debug!("{:#?}", changes);
1304
1305        changes.into_iter().map(|(_, v)| v)
1306    }
1307
1308    fn key(dep: PackageId) -> (&'static str, SourceId) {
1309        (dep.name().as_str(), dep.source_id())
1310    }
1311
1312    /// Guess if a package upgraded/downgraded
1313    ///
1314    /// All `PackageDiff` knows is that entries were added/removed within [`Resolve`].
1315    /// A package could be added or removed because of dependencies from other packages
1316    /// which makes it hard to definitively say "X was upgrade to N".
1317    pub fn change(&self) -> Option<(PackageId, PackageId)> {
1318        if self.removed.len() == 1 && self.added.len() == 1 {
1319            Some((self.removed[0], self.added[0]))
1320        } else {
1321            None
1322        }
1323    }
1324}
1325
1326fn annotate_required_rust_version(
1327    ws: &Workspace<'_>,
1328    resolve: &Resolve,
1329    changes: &mut IndexMap<PackageId, PackageChange>,
1330) {
1331    let rustc = ws.gctx().load_global_rustc(Some(ws)).ok();
1332    let rustc_version: Option<PartialVersion> =
1333        rustc.as_ref().map(|rustc| rustc.version.clone().into());
1334
1335    if ws.resolve_honors_rust_version() {
1336        let mut queue: std::collections::VecDeque<_> = ws
1337            .members()
1338            .map(|p| {
1339                (
1340                    p.rust_version()
1341                        .map(|r| r.to_partial())
1342                        .or_else(|| rustc_version.clone()),
1343                    p.package_id(),
1344                )
1345            })
1346            .collect();
1347        while let Some((required_rust_version, current_id)) = queue.pop_front() {
1348            let Some(required_rust_version) = required_rust_version else {
1349                continue;
1350            };
1351            if let Some(change) = changes.get_mut(&current_id) {
1352                if let Some(existing) = change.required_rust_version.as_ref() {
1353                    if *existing <= required_rust_version {
1354                        // Stop early; we already walked down this path with a better match
1355                        continue;
1356                    }
1357                }
1358                change.required_rust_version = Some(required_rust_version.clone());
1359            }
1360            queue.extend(
1361                resolve
1362                    .deps(current_id)
1363                    .map(|(dep, _)| (Some(required_rust_version.clone()), dep)),
1364            );
1365        }
1366    } else {
1367        for change in changes.values_mut() {
1368            change.required_rust_version = rustc_version.clone();
1369        }
1370    }
1371}