Skip to main content

cargo/ops/
cargo_update.rs

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