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.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,
464                        new_req.comparators
465                    );
466                    continue;
467                };
468
469                if comparator.op != Op::Caret {
470                    trace!("skipping non-caret dependency `{}`: {}", name, comparator);
471                    continue;
472                }
473
474                let mut dep = dependency.clone();
475                let mut source = source.clone();
476                source.version = new_req_string;
477                dep.source = Some(Source::Registry(source));
478
479                trace!("upgrading dependency {name}");
480                dep.update_toml(
481                    ws.gctx(),
482                    ws.root(),
483                    &crate_root,
484                    unstable_features,
485                    &mut dep_key,
486                    dep_item,
487                )?;
488                manifest_has_changed = true;
489                any_file_has_changed = true;
490            }
491        }
492
493        if manifest_has_changed && !dry_run {
494            debug!("writing upgraded manifest to {}", manifest_path.display());
495            local_manifest.write()?;
496        }
497    }
498
499    Ok(any_file_has_changed)
500}
501
502fn print_lockfile_generation(
503    ws: &Workspace<'_>,
504    resolve: &Resolve,
505    registry: &mut PackageRegistry<'_>,
506) -> CargoResult<()> {
507    let mut changes = PackageChange::new(ws, resolve);
508    let num_pkgs: usize = changes
509        .values()
510        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
511        .count();
512    if num_pkgs == 0 {
513        // nothing worth reporting
514        return Ok(());
515    }
516    annotate_required_rust_version(ws, resolve, &mut changes);
517
518    status_locking(ws, num_pkgs)?;
519    for change in changes.values() {
520        if change.is_member.unwrap_or(false) {
521            continue;
522        };
523        match change.kind {
524            PackageChangeKind::Added => {
525                let possibilities = if let Some(query) = change.alternatives_query() {
526                    loop {
527                        match registry.query_vec(&query, QueryKind::Exact) {
528                            std::task::Poll::Ready(res) => {
529                                break res?;
530                            }
531                            std::task::Poll::Pending => registry.block_until_ready()?,
532                        }
533                    }
534                } else {
535                    vec![]
536                };
537
538                let required_rust_version = report_required_rust_version(resolve, change);
539                let latest = report_latest(&possibilities, change);
540                let note = required_rust_version.or(latest);
541
542                if let Some(note) = note {
543                    ws.gctx().shell().status_with_color(
544                        change.kind.status(),
545                        format!("{change}{note}"),
546                        &change.kind.style(),
547                    )?;
548                }
549            }
550            PackageChangeKind::Upgraded
551            | PackageChangeKind::Downgraded
552            | PackageChangeKind::Removed
553            | PackageChangeKind::Unchanged => {
554                unreachable!("without a previous resolve, everything should be added")
555            }
556        }
557    }
558
559    Ok(())
560}
561
562fn print_lockfile_sync(
563    ws: &Workspace<'_>,
564    previous_resolve: &Resolve,
565    resolve: &Resolve,
566    registry: &mut PackageRegistry<'_>,
567) -> CargoResult<()> {
568    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
569    let num_pkgs: usize = changes
570        .values()
571        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
572        .count();
573    if num_pkgs == 0 {
574        // nothing worth reporting
575        return Ok(());
576    }
577    annotate_required_rust_version(ws, resolve, &mut changes);
578
579    status_locking(ws, num_pkgs)?;
580    for change in changes.values() {
581        if change.is_member.unwrap_or(false) {
582            continue;
583        };
584        match change.kind {
585            PackageChangeKind::Added
586            | PackageChangeKind::Upgraded
587            | PackageChangeKind::Downgraded => {
588                let possibilities = if let Some(query) = change.alternatives_query() {
589                    loop {
590                        match registry.query_vec(&query, QueryKind::Exact) {
591                            std::task::Poll::Ready(res) => {
592                                break res?;
593                            }
594                            std::task::Poll::Pending => registry.block_until_ready()?,
595                        }
596                    }
597                } else {
598                    vec![]
599                };
600
601                let required_rust_version = report_required_rust_version(resolve, change);
602                let latest = report_latest(&possibilities, change);
603                let note = required_rust_version.or(latest).unwrap_or_default();
604
605                ws.gctx().shell().status_with_color(
606                    change.kind.status(),
607                    format!("{change}{note}"),
608                    &change.kind.style(),
609                )?;
610            }
611            PackageChangeKind::Removed | PackageChangeKind::Unchanged => {}
612        }
613    }
614
615    Ok(())
616}
617
618fn print_lockfile_updates(
619    ws: &Workspace<'_>,
620    previous_resolve: &Resolve,
621    resolve: &Resolve,
622    precise: bool,
623    registry: &mut PackageRegistry<'_>,
624) -> CargoResult<()> {
625    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
626    let num_pkgs: usize = changes
627        .values()
628        .filter(|change| change.kind.is_new())
629        .count();
630    annotate_required_rust_version(ws, resolve, &mut changes);
631
632    if !precise {
633        status_locking(ws, num_pkgs)?;
634    }
635    let mut unchanged_behind = 0;
636    for change in changes.values() {
637        let possibilities = if let Some(query) = change.alternatives_query() {
638            loop {
639                match registry.query_vec(&query, QueryKind::Exact) {
640                    std::task::Poll::Ready(res) => {
641                        break res?;
642                    }
643                    std::task::Poll::Pending => registry.block_until_ready()?,
644                }
645            }
646        } else {
647            vec![]
648        };
649
650        match change.kind {
651            PackageChangeKind::Added
652            | PackageChangeKind::Upgraded
653            | PackageChangeKind::Downgraded => {
654                let required_rust_version = report_required_rust_version(resolve, change);
655                let latest = report_latest(&possibilities, change);
656                let note = required_rust_version.or(latest).unwrap_or_default();
657
658                ws.gctx().shell().status_with_color(
659                    change.kind.status(),
660                    format!("{change}{note}"),
661                    &change.kind.style(),
662                )?;
663            }
664            PackageChangeKind::Removed => {
665                ws.gctx().shell().status_with_color(
666                    change.kind.status(),
667                    format!("{change}"),
668                    &change.kind.style(),
669                )?;
670            }
671            PackageChangeKind::Unchanged => {
672                let required_rust_version = report_required_rust_version(resolve, change);
673                let latest = report_latest(&possibilities, change);
674                let note = required_rust_version.as_deref().or(latest.as_deref());
675
676                if let Some(note) = note {
677                    if latest.is_some() {
678                        unchanged_behind += 1;
679                    }
680                    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
681                        ws.gctx().shell().status_with_color(
682                            change.kind.status(),
683                            format!("{change}{note}"),
684                            &change.kind.style(),
685                        )?;
686                    }
687                }
688            }
689        }
690    }
691
692    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
693        ws.gctx().shell().note(
694            "to see how you depend on a package, run `cargo tree --invert --package <dep>@<ver>`",
695        )?;
696    } else {
697        if 0 < unchanged_behind {
698            ws.gctx().shell().note(format!(
699                "pass `--verbose` to see {unchanged_behind} unchanged dependencies behind latest"
700            ))?;
701        }
702    }
703
704    Ok(())
705}
706
707fn status_locking(ws: &Workspace<'_>, num_pkgs: usize) -> CargoResult<()> {
708    use std::fmt::Write as _;
709
710    let plural = if num_pkgs == 1 { "" } else { "s" };
711
712    let mut cfg = String::new();
713    // Don't have a good way to describe `direct_minimal_versions` atm
714    if !ws.gctx().cli_unstable().direct_minimal_versions {
715        write!(&mut cfg, " to")?;
716        if ws.gctx().cli_unstable().minimal_versions {
717            write!(&mut cfg, " earliest")?;
718        } else {
719            write!(&mut cfg, " latest")?;
720        }
721
722        if let Some(rust_version) = required_rust_version(ws) {
723            write!(&mut cfg, " Rust {rust_version}")?;
724        }
725        write!(&mut cfg, " compatible version{plural}")?;
726    }
727
728    ws.gctx()
729        .shell()
730        .status("Locking", format!("{num_pkgs} package{plural}{cfg}"))?;
731    Ok(())
732}
733
734fn required_rust_version(ws: &Workspace<'_>) -> Option<PartialVersion> {
735    if !ws.resolve_honors_rust_version() {
736        return None;
737    }
738
739    if let Some(ver) = ws.lowest_rust_version() {
740        Some(ver.clone().into_partial())
741    } else {
742        let rustc = ws.gctx().load_global_rustc(Some(ws)).ok()?;
743        let rustc_version = rustc.version.clone().into();
744        Some(rustc_version)
745    }
746}
747
748fn report_required_rust_version(resolve: &Resolve, change: &PackageChange) -> Option<String> {
749    if change.package_id.source_id().is_path() {
750        return None;
751    }
752    let summary = resolve.summary(change.package_id);
753    let package_rust_version = summary.rust_version()?;
754    let required_rust_version = change.required_rust_version.as_ref()?;
755    if package_rust_version.is_compatible_with(required_rust_version) {
756        return None;
757    }
758
759    let error = style::ERROR;
760    Some(format!(
761        " {error}(requires Rust {package_rust_version}){error:#}"
762    ))
763}
764
765fn report_latest(possibilities: &[IndexSummary], change: &PackageChange) -> Option<String> {
766    let package_id = change.package_id;
767    if !package_id.source_id().is_registry() {
768        return None;
769    }
770
771    let version_req = package_id.version().to_caret_req();
772    let required_rust_version = change.required_rust_version.as_ref();
773
774    let compat_ver_compat_msrv_summary = possibilities
775        .iter()
776        .map(|s| s.as_summary())
777        .filter(|s| {
778            if let (Some(summary_rust_version), Some(required_rust_version)) =
779                (s.rust_version(), required_rust_version)
780            {
781                summary_rust_version.is_compatible_with(required_rust_version)
782            } else {
783                true
784            }
785        })
786        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
787        .max_by_key(|s| s.version());
788    if let Some(summary) = compat_ver_compat_msrv_summary {
789        let warn = style::WARN;
790        let version = summary.version();
791        let report = format!(" {warn}(available: v{version}){warn:#}");
792        return Some(report);
793    }
794
795    if !change.is_transitive.unwrap_or(true) {
796        let incompat_ver_compat_msrv_summary = possibilities
797            .iter()
798            .map(|s| s.as_summary())
799            .filter(|s| {
800                if let (Some(summary_rust_version), Some(required_rust_version)) =
801                    (s.rust_version(), required_rust_version)
802                {
803                    summary_rust_version.is_compatible_with(required_rust_version)
804                } else {
805                    true
806                }
807            })
808            .filter(|s| is_latest(s.version(), package_id.version()))
809            .max_by_key(|s| s.version());
810        if let Some(summary) = incompat_ver_compat_msrv_summary {
811            let warn = style::WARN;
812            let version = summary.version();
813            let report = format!(" {warn}(available: v{version}){warn:#}");
814            return Some(report);
815        }
816    }
817
818    let compat_ver_summary = possibilities
819        .iter()
820        .map(|s| s.as_summary())
821        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
822        .max_by_key(|s| s.version());
823    if let Some(summary) = compat_ver_summary {
824        let msrv_note = summary
825            .rust_version()
826            .map(|rv| format!(", requires Rust {rv}"))
827            .unwrap_or_default();
828        let warn = style::NOP;
829        let version = summary.version();
830        let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
831        return Some(report);
832    }
833
834    if !change.is_transitive.unwrap_or(true) {
835        let incompat_ver_summary = possibilities
836            .iter()
837            .map(|s| s.as_summary())
838            .filter(|s| is_latest(s.version(), package_id.version()))
839            .max_by_key(|s| s.version());
840        if let Some(summary) = incompat_ver_summary {
841            let msrv_note = summary
842                .rust_version()
843                .map(|rv| format!(", requires Rust {rv}"))
844                .unwrap_or_default();
845            let warn = style::NOP;
846            let version = summary.version();
847            let report = format!(" {warn}(available: v{version}{msrv_note}){warn:#}");
848            return Some(report);
849        }
850    }
851
852    None
853}
854
855fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
856    current < candidate
857                // Only match pre-release if major.minor.patch are the same
858                && (candidate.pre.is_empty()
859                    || (candidate.major == current.major
860                        && candidate.minor == current.minor
861                        && candidate.patch == current.patch))
862}
863
864fn fill_with_deps<'a>(
865    resolve: &'a Resolve,
866    dep: PackageId,
867    set: &mut HashSet<PackageId>,
868    visited: &mut HashSet<PackageId>,
869) {
870    if !visited.insert(dep) {
871        return;
872    }
873    set.insert(dep);
874    for (dep, _) in resolve.deps_not_replaced(dep) {
875        fill_with_deps(resolve, dep, set, visited);
876    }
877}
878
879#[derive(Clone, Debug)]
880struct PackageChange {
881    package_id: PackageId,
882    previous_id: Option<PackageId>,
883    kind: PackageChangeKind,
884    is_member: Option<bool>,
885    is_transitive: Option<bool>,
886    required_rust_version: Option<PartialVersion>,
887}
888
889impl PackageChange {
890    pub fn new(ws: &Workspace<'_>, resolve: &Resolve) -> IndexMap<PackageId, Self> {
891        let diff = PackageDiff::new(resolve);
892        Self::with_diff(diff, ws, resolve)
893    }
894
895    pub fn diff(
896        ws: &Workspace<'_>,
897        previous_resolve: &Resolve,
898        resolve: &Resolve,
899    ) -> IndexMap<PackageId, Self> {
900        let diff = PackageDiff::diff(previous_resolve, resolve);
901        Self::with_diff(diff, ws, resolve)
902    }
903
904    fn with_diff(
905        diff: impl Iterator<Item = PackageDiff>,
906        ws: &Workspace<'_>,
907        resolve: &Resolve,
908    ) -> IndexMap<PackageId, Self> {
909        let member_ids: HashSet<_> = ws.members().map(|p| p.package_id()).collect();
910
911        let mut changes = IndexMap::new();
912        for diff in diff {
913            if let Some((previous_id, package_id)) = diff.change() {
914                // If versions differ only in build metadata, we call it an "update"
915                // regardless of whether the build metadata has gone up or down.
916                // This metadata is often stuff like git commit hashes, which are
917                // not meaningfully ordered.
918                let kind = if previous_id.version().cmp_precedence(package_id.version())
919                    == Ordering::Greater
920                {
921                    PackageChangeKind::Downgraded
922                } else {
923                    PackageChangeKind::Upgraded
924                };
925                let is_member = Some(member_ids.contains(&package_id));
926                let is_transitive = Some(true);
927                let change = Self {
928                    package_id,
929                    previous_id: Some(previous_id),
930                    kind,
931                    is_member,
932                    is_transitive,
933                    required_rust_version: None,
934                };
935                changes.insert(change.package_id, change);
936            } else {
937                for package_id in diff.removed {
938                    let kind = PackageChangeKind::Removed;
939                    let is_member = None;
940                    let is_transitive = None;
941                    let change = Self {
942                        package_id,
943                        previous_id: None,
944                        kind,
945                        is_member,
946                        is_transitive,
947                        required_rust_version: None,
948                    };
949                    changes.insert(change.package_id, change);
950                }
951                for package_id in diff.added {
952                    let kind = PackageChangeKind::Added;
953                    let is_member = Some(member_ids.contains(&package_id));
954                    let is_transitive = Some(true);
955                    let change = Self {
956                        package_id,
957                        previous_id: None,
958                        kind,
959                        is_member,
960                        is_transitive,
961                        required_rust_version: None,
962                    };
963                    changes.insert(change.package_id, change);
964                }
965            }
966            for package_id in diff.unchanged {
967                let kind = PackageChangeKind::Unchanged;
968                let is_member = Some(member_ids.contains(&package_id));
969                let is_transitive = Some(true);
970                let change = Self {
971                    package_id,
972                    previous_id: None,
973                    kind,
974                    is_member,
975                    is_transitive,
976                    required_rust_version: None,
977                };
978                changes.insert(change.package_id, change);
979            }
980        }
981
982        for member_id in &member_ids {
983            let Some(change) = changes.get_mut(member_id) else {
984                continue;
985            };
986            change.is_transitive = Some(false);
987            for (direct_dep_id, _) in resolve.deps(*member_id) {
988                let Some(change) = changes.get_mut(&direct_dep_id) else {
989                    continue;
990                };
991                change.is_transitive = Some(false);
992            }
993        }
994
995        changes
996    }
997
998    /// For querying [`PackageRegistry`] for alternative versions to report to the user
999    fn alternatives_query(&self) -> Option<crate::core::dependency::Dependency> {
1000        if !self.package_id.source_id().is_registry() {
1001            return None;
1002        }
1003
1004        let query = crate::core::dependency::Dependency::parse(
1005            self.package_id.name(),
1006            None,
1007            self.package_id.source_id(),
1008        )
1009        .expect("already a valid dependency");
1010        Some(query)
1011    }
1012}
1013
1014impl std::fmt::Display for PackageChange {
1015    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1016        let package_id = self.package_id;
1017        if let Some(previous_id) = self.previous_id {
1018            if package_id.source_id().is_git() {
1019                write!(
1020                    f,
1021                    "{previous_id} -> #{}",
1022                    &package_id.source_id().precise_git_fragment().unwrap()[..8],
1023                )
1024            } else {
1025                write!(f, "{previous_id} -> v{}", package_id.version())
1026            }
1027        } else {
1028            write!(f, "{package_id}")
1029        }
1030    }
1031}
1032
1033#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1034enum PackageChangeKind {
1035    Added,
1036    Removed,
1037    Upgraded,
1038    Downgraded,
1039    Unchanged,
1040}
1041
1042impl PackageChangeKind {
1043    pub fn is_new(&self) -> bool {
1044        match self {
1045            Self::Added | Self::Upgraded | Self::Downgraded => true,
1046            Self::Removed | Self::Unchanged => false,
1047        }
1048    }
1049
1050    pub fn status(&self) -> &'static str {
1051        match self {
1052            Self::Added => "Adding",
1053            Self::Removed => "Removing",
1054            Self::Upgraded => "Updating",
1055            Self::Downgraded => "Downgrading",
1056            Self::Unchanged => "Unchanged",
1057        }
1058    }
1059
1060    pub fn style(&self) -> anstyle::Style {
1061        match self {
1062            Self::Added => style::NOTE,
1063            Self::Removed => style::ERROR,
1064            Self::Upgraded => style::GOOD,
1065            Self::Downgraded => style::WARN,
1066            Self::Unchanged => anstyle::Style::new().bold(),
1067        }
1068    }
1069}
1070
1071/// All resolved versions of a package name within a [`SourceId`]
1072#[derive(Default, Clone, Debug)]
1073pub struct PackageDiff {
1074    removed: Vec<PackageId>,
1075    added: Vec<PackageId>,
1076    unchanged: Vec<PackageId>,
1077}
1078
1079impl PackageDiff {
1080    pub fn new(resolve: &Resolve) -> impl Iterator<Item = Self> {
1081        let mut changes = BTreeMap::new();
1082        let empty = Self::default();
1083        for dep in resolve.iter() {
1084            changes
1085                .entry(Self::key(dep))
1086                .or_insert_with(|| empty.clone())
1087                .added
1088                .push(dep);
1089        }
1090
1091        changes.into_iter().map(|(_, v)| v)
1092    }
1093
1094    pub fn diff(previous_resolve: &Resolve, resolve: &Resolve) -> impl Iterator<Item = Self> {
1095        fn vec_subset(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1096            a.iter().filter(|a| !contains_id(b, a)).cloned().collect()
1097        }
1098
1099        fn vec_intersection(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
1100            a.iter().filter(|a| contains_id(b, a)).cloned().collect()
1101        }
1102
1103        // Check if a PackageId is present `b` from `a`.
1104        //
1105        // Note that this is somewhat more complicated because the equality for source IDs does not
1106        // take precise versions into account (e.g., git shas), but we want to take that into
1107        // account here.
1108        fn contains_id(haystack: &[PackageId], needle: &PackageId) -> bool {
1109            let Ok(i) = haystack.binary_search(needle) else {
1110                return false;
1111            };
1112
1113            // If we've found `a` in `b`, then we iterate over all instances
1114            // (we know `b` is sorted) and see if they all have different
1115            // precise versions. If so, then `a` isn't actually in `b` so
1116            // we'll let it through.
1117            //
1118            // Note that we only check this for non-registry sources,
1119            // however, as registries contain enough version information in
1120            // the package ID to disambiguate.
1121            if needle.source_id().is_registry() {
1122                return true;
1123            }
1124            haystack[i..]
1125                .iter()
1126                .take_while(|b| &needle == b)
1127                .any(|b| needle.source_id().has_same_precise_as(b.source_id()))
1128        }
1129
1130        // Map `(package name, package source)` to `(removed versions, added versions)`.
1131        let mut changes = BTreeMap::new();
1132        let empty = Self::default();
1133        for dep in previous_resolve.iter() {
1134            changes
1135                .entry(Self::key(dep))
1136                .or_insert_with(|| empty.clone())
1137                .removed
1138                .push(dep);
1139        }
1140        for dep in resolve.iter() {
1141            changes
1142                .entry(Self::key(dep))
1143                .or_insert_with(|| empty.clone())
1144                .added
1145                .push(dep);
1146        }
1147
1148        for v in changes.values_mut() {
1149            let Self {
1150                removed: ref mut old,
1151                added: ref mut new,
1152                unchanged: ref mut other,
1153            } = *v;
1154            old.sort();
1155            new.sort();
1156            let removed = vec_subset(old, new);
1157            let added = vec_subset(new, old);
1158            let unchanged = vec_intersection(new, old);
1159            *old = removed;
1160            *new = added;
1161            *other = unchanged;
1162        }
1163        debug!("{:#?}", changes);
1164
1165        changes.into_iter().map(|(_, v)| v)
1166    }
1167
1168    fn key(dep: PackageId) -> (&'static str, SourceId) {
1169        (dep.name().as_str(), dep.source_id())
1170    }
1171
1172    /// Guess if a package upgraded/downgraded
1173    ///
1174    /// All `PackageDiff` knows is that entries were added/removed within [`Resolve`].
1175    /// A package could be added or removed because of dependencies from other packages
1176    /// which makes it hard to definitively say "X was upgrade to N".
1177    pub fn change(&self) -> Option<(PackageId, PackageId)> {
1178        if self.removed.len() == 1 && self.added.len() == 1 {
1179            Some((self.removed[0], self.added[0]))
1180        } else {
1181            None
1182        }
1183    }
1184}
1185
1186fn annotate_required_rust_version(
1187    ws: &Workspace<'_>,
1188    resolve: &Resolve,
1189    changes: &mut IndexMap<PackageId, PackageChange>,
1190) {
1191    let rustc = ws.gctx().load_global_rustc(Some(ws)).ok();
1192    let rustc_version: Option<PartialVersion> =
1193        rustc.as_ref().map(|rustc| rustc.version.clone().into());
1194
1195    if ws.resolve_honors_rust_version() {
1196        let mut queue: std::collections::VecDeque<_> = ws
1197            .members()
1198            .map(|p| {
1199                (
1200                    p.rust_version()
1201                        .map(|r| r.clone().into_partial())
1202                        .or_else(|| rustc_version.clone()),
1203                    p.package_id(),
1204                )
1205            })
1206            .collect();
1207        while let Some((required_rust_version, current_id)) = queue.pop_front() {
1208            let Some(required_rust_version) = required_rust_version else {
1209                continue;
1210            };
1211            if let Some(change) = changes.get_mut(&current_id) {
1212                if let Some(existing) = change.required_rust_version.as_ref() {
1213                    if *existing <= required_rust_version {
1214                        // Stop early; we already walked down this path with a better match
1215                        continue;
1216                    }
1217                }
1218                change.required_rust_version = Some(required_rust_version.clone());
1219            }
1220            queue.extend(
1221                resolve
1222                    .deps(current_id)
1223                    .map(|(dep, _)| (Some(required_rust_version.clone()), dep)),
1224            );
1225        }
1226    } else {
1227        for change in changes.values_mut() {
1228            change.required_rust_version = rustc_version.clone();
1229        }
1230    }
1231}