Skip to main content

cargo/ops/
cargo_update.rs

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