cargo/ops/
cargo_update.rs

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