cargo/ops/
cargo_update.rs

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