Skip to main content

cargo/ops/
cargo_update.rs

1use crate::context::CargoResolverConfig;
2use crate::context::GlobalContext;
3use crate::context::IncompatiblePublishAge;
4use crate::ops;
5use crate::resolver::PublishAgePolicy;
6use crate::resolver::Resolve;
7use crate::resolver::features::{CliFeatures, HasDevUnits};
8use crate::sources::IndexSummary;
9use crate::sources::source::QueryKind;
10use crate::util::cache_lock::CacheLockMode;
11use crate::util::style;
12use crate::util::{CargoResult, VersionExt};
13use crate::workspace::Registry as _;
14use crate::workspace::registry::PackageRegistry;
15use crate::workspace::{PackageId, PackageIdSpec, PackageIdSpecQuery};
16use crate::workspace::{SourceId, Workspace};
17
18use crate::util::data_structures::{HashSet, IndexMap};
19use cargo_util_schemas::core::PartialVersion;
20use cargo_util_terminal::Verbosity;
21use std::cmp::Ordering;
22use std::collections::BTreeMap;
23use tracing::debug;
24
25pub struct UpdateOptions<'a> {
26    pub gctx: &'a GlobalContext,
27    pub to_update: Vec<String>,
28    pub precise: Option<&'a str>,
29    pub recursive: bool,
30    pub dry_run: bool,
31    pub workspace: bool,
32}
33
34pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> {
35    let mut registry = ws.package_registry()?;
36    let previous_resolve = None;
37    let mut resolve = ops::resolve_with_previous(
38        &mut registry,
39        ws,
40        &CliFeatures::new_all(true),
41        HasDevUnits::Yes,
42        previous_resolve,
43        None,
44        &[],
45        true,
46    )?;
47    ops::write_pkg_lockfile(ws, &mut resolve)?;
48    print_lockfile_changes(ws, previous_resolve, &resolve, &mut registry)?;
49    Ok(())
50}
51
52pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoResult<()> {
53    if opts.recursive && opts.precise.is_some() {
54        anyhow::bail!("cannot specify both recursive and precise simultaneously")
55    }
56
57    if ws.members().count() == 0 {
58        anyhow::bail!("you can't generate a lockfile for an empty workspace.")
59    }
60
61    // Updates often require a lot of modifications to the registry, so ensure
62    // that we're synchronized against other Cargos.
63    let _lock = ws
64        .gctx()
65        .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
66
67    let previous_resolve = match ops::load_pkg_lockfile(ws)? {
68        Some(resolve) => resolve,
69        None => {
70            match opts.precise {
71                None => return generate_lockfile(ws),
72
73                // Precise option specified, so calculate a previous_resolve required
74                // by precise package update later.
75                Some(_) => {
76                    let mut registry = ws.package_registry()?;
77                    ops::resolve_with_previous(
78                        &mut registry,
79                        ws,
80                        &CliFeatures::new_all(true),
81                        HasDevUnits::Yes,
82                        None,
83                        None,
84                        &[],
85                        true,
86                    )?
87                }
88            }
89        }
90    };
91    let mut registry = ws.package_registry()?;
92    let mut to_avoid = HashSet::default();
93
94    if opts.to_update.is_empty() {
95        if !opts.workspace {
96            to_avoid.extend(previous_resolve.iter());
97            to_avoid.extend(previous_resolve.unused_patches());
98        }
99    } else {
100        let mut sources = Vec::new();
101        for name in opts.to_update.iter() {
102            let pid = previous_resolve.query(name)?;
103            if opts.recursive {
104                fill_with_deps(
105                    &previous_resolve,
106                    pid,
107                    &mut to_avoid,
108                    &mut HashSet::default(),
109                );
110            } else {
111                to_avoid.insert(pid);
112                sources.push(match opts.precise {
113                    Some(precise) => {
114                        // TODO: see comment in `resolve.rs` as well, but this
115                        //       seems like a pretty hokey reason to single out
116                        //       the registry as well.
117                        if pid.source_id().is_registry() {
118                            pid.source_id().with_precise_registry_version(
119                                pid.name(),
120                                pid.version().clone(),
121                                precise,
122                            )?
123                        } else {
124                            pid.source_id().with_git_precise(Some(precise.to_string()))
125                        }
126                    }
127                    None => pid.source_id().without_precise(),
128                });
129            }
130            if let Ok(unused_id) =
131                PackageIdSpec::query_str(name, previous_resolve.unused_patches().iter().cloned())
132            {
133                to_avoid.insert(unused_id);
134            }
135        }
136
137        // Mirror `--workspace` and never avoid workspace members.
138        // Filtering them out here so the above processes them normally
139        // so their dependencies can be updated as requested
140        to_avoid.retain(|id| {
141            for package in ws.members() {
142                let member_id = package.package_id();
143                // Skip checking the `version` because `previous_resolve` might have a stale
144                // value.
145                // When dealing with workspace members, the other fields should be a
146                // sufficiently unique match.
147                if id.name() == member_id.name() && id.source_id() == member_id.source_id() {
148                    return false;
149                }
150            }
151            true
152        });
153
154        registry.add_sources(sources)?;
155    }
156
157    // Here we place an artificial limitation that all non-registry sources
158    // cannot be locked at more than one revision. This means that if a Git
159    // repository provides more than one package, they must all be updated in
160    // step when any of them are updated.
161    //
162    // TODO: this seems like a hokey reason to single out the registry as being
163    // different.
164    let to_avoid_sources: HashSet<_> = to_avoid
165        .iter()
166        .map(|p| p.source_id())
167        .filter(|s| !s.is_registry())
168        .collect();
169
170    let keep = |p: &PackageId| !to_avoid_sources.contains(&p.source_id()) && !to_avoid.contains(p);
171
172    let mut resolve = ops::resolve_with_previous(
173        &mut registry,
174        ws,
175        &CliFeatures::new_all(true),
176        HasDevUnits::Yes,
177        Some(&previous_resolve),
178        Some(&keep),
179        &[],
180        true,
181    )?;
182
183    print_lockfile_updates(
184        ws,
185        &previous_resolve,
186        &resolve,
187        opts.precise.is_some(),
188        &mut registry,
189    )?;
190    if opts.dry_run {
191        opts.gctx
192            .shell()
193            .warn("not updating lockfile due to dry run")?;
194    } else {
195        ops::write_pkg_lockfile(ws, &mut resolve)?;
196    }
197    Ok(())
198}
199
200/// Prints lockfile change statuses.
201///
202/// This would acquire the package-cache lock, as it may update the index to
203/// show users latest available versions.
204pub fn print_lockfile_changes(
205    ws: &Workspace<'_>,
206    previous_resolve: Option<&Resolve>,
207    resolve: &Resolve,
208    registry: &mut PackageRegistry<'_>,
209) -> CargoResult<()> {
210    let _lock = ws
211        .gctx()
212        .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
213    if let Some(previous_resolve) = previous_resolve {
214        print_lockfile_sync(ws, previous_resolve, resolve, registry)
215    } else {
216        print_lockfile_generation(ws, resolve, registry)
217    }
218}
219fn print_lockfile_generation(
220    ws: &Workspace<'_>,
221    resolve: &Resolve,
222    registry: &mut PackageRegistry<'_>,
223) -> CargoResult<()> {
224    let mut changes = PackageChange::new(ws, resolve);
225    let num_pkgs: usize = changes
226        .values()
227        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
228        .count();
229    if num_pkgs == 0 {
230        // nothing worth reporting
231        return Ok(());
232    }
233    annotate_required_rust_version(ws, resolve, &mut changes);
234    let publish_age = publish_age_policy_for_report(ws);
235
236    status_locking(ws, publish_age.as_ref(), num_pkgs)?;
237    for change in changes.values() {
238        if change.is_member.unwrap_or(false) {
239            continue;
240        };
241        match change.kind {
242            PackageChangeKind::Added => {
243                let possibilities = if let Some(query) = change.alternatives_query() {
244                    crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
245                } else {
246                    vec![]
247                };
248
249                let required_rust_version = report_required_rust_version(resolve, change);
250                let too_new = report_too_new(resolve, change, publish_age.as_ref());
251                let latest = report_latest(&possibilities, change, publish_age.as_ref());
252                let note = required_rust_version.or(too_new).or(latest);
253
254                if let Some(note) = note {
255                    ws.gctx().shell().status_with_color(
256                        change.kind.status(),
257                        format!("{change}{note}"),
258                        &change.kind.style(),
259                    )?;
260                }
261            }
262            PackageChangeKind::Upgraded
263            | PackageChangeKind::Downgraded
264            | PackageChangeKind::Removed
265            | PackageChangeKind::Unchanged => {
266                unreachable!("without a previous resolve, everything should be added")
267            }
268        }
269    }
270
271    Ok(())
272}
273
274fn print_lockfile_sync(
275    ws: &Workspace<'_>,
276    previous_resolve: &Resolve,
277    resolve: &Resolve,
278    registry: &mut PackageRegistry<'_>,
279) -> CargoResult<()> {
280    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
281    let num_pkgs: usize = changes
282        .values()
283        .filter(|change| change.kind.is_new() && !change.is_member.unwrap_or(false))
284        .count();
285    if num_pkgs == 0 {
286        // nothing worth reporting
287        return Ok(());
288    }
289    annotate_required_rust_version(ws, resolve, &mut changes);
290    let publish_age = publish_age_policy_for_report(ws);
291
292    status_locking(ws, publish_age.as_ref(), num_pkgs)?;
293    for change in changes.values() {
294        if change.is_member.unwrap_or(false) {
295            continue;
296        };
297        match change.kind {
298            PackageChangeKind::Added
299            | PackageChangeKind::Upgraded
300            | PackageChangeKind::Downgraded => {
301                let possibilities = if let Some(query) = change.alternatives_query() {
302                    crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
303                } else {
304                    vec![]
305                };
306
307                let required_rust_version = report_required_rust_version(resolve, change);
308                let too_new = report_too_new(resolve, change, publish_age.as_ref());
309                let latest = report_latest(&possibilities, change, publish_age.as_ref());
310                let note = required_rust_version
311                    .or(too_new)
312                    .or(latest)
313                    .unwrap_or_default();
314
315                ws.gctx().shell().status_with_color(
316                    change.kind.status(),
317                    format!("{change}{note}"),
318                    &change.kind.style(),
319                )?;
320            }
321            PackageChangeKind::Removed | PackageChangeKind::Unchanged => {}
322        }
323    }
324
325    Ok(())
326}
327
328fn print_lockfile_updates(
329    ws: &Workspace<'_>,
330    previous_resolve: &Resolve,
331    resolve: &Resolve,
332    precise: bool,
333    registry: &mut PackageRegistry<'_>,
334) -> CargoResult<()> {
335    let mut changes = PackageChange::diff(ws, previous_resolve, resolve);
336    let num_pkgs: usize = changes
337        .values()
338        .filter(|change| change.kind.is_new())
339        .count();
340    annotate_required_rust_version(ws, resolve, &mut changes);
341    let publish_age = publish_age_policy_for_report(ws);
342
343    if !precise {
344        status_locking(ws, publish_age.as_ref(), num_pkgs)?;
345    }
346    let mut unchanged_behind = 0;
347    for change in changes.values() {
348        let possibilities = if let Some(query) = change.alternatives_query() {
349            crate::util::block_on(registry.query_vec(&query, QueryKind::Exact))?
350        } else {
351            vec![]
352        };
353
354        match change.kind {
355            PackageChangeKind::Added
356            | PackageChangeKind::Upgraded
357            | PackageChangeKind::Downgraded => {
358                let required_rust_version = report_required_rust_version(resolve, change);
359                let too_new = report_too_new(resolve, change, publish_age.as_ref());
360                let latest = report_latest(&possibilities, change, publish_age.as_ref());
361                let note = required_rust_version
362                    .or(too_new)
363                    .or(latest)
364                    .unwrap_or_default();
365
366                ws.gctx().shell().status_with_color(
367                    change.kind.status(),
368                    format!("{change}{note}"),
369                    &change.kind.style(),
370                )?;
371            }
372            PackageChangeKind::Removed => {
373                ws.gctx().shell().status_with_color(
374                    change.kind.status(),
375                    format!("{change}"),
376                    &change.kind.style(),
377                )?;
378            }
379            PackageChangeKind::Unchanged => {
380                let required_rust_version = report_required_rust_version(resolve, change);
381                let too_new = report_too_new(resolve, change, publish_age.as_ref());
382                let latest = report_latest(&possibilities, change, publish_age.as_ref());
383                let note = required_rust_version
384                    .as_deref()
385                    .or(too_new.as_deref())
386                    .or(latest.as_deref());
387
388                if let Some(note) = note {
389                    if latest.is_some() {
390                        unchanged_behind += 1;
391                    }
392                    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
393                        ws.gctx().shell().status_with_color(
394                            change.kind.status(),
395                            format!("{change}{note}"),
396                            &change.kind.style(),
397                        )?;
398                    }
399                }
400            }
401        }
402    }
403
404    if ws.gctx().shell().verbosity() == Verbosity::Verbose {
405        ws.gctx()
406            .shell()
407            .note("to see how you depend on a package, run `cargo tree --invert <dep>@<ver>`")?;
408    } else {
409        if 0 < unchanged_behind {
410            ws.gctx().shell().note(format!(
411                "pass `--verbose` to see {unchanged_behind} unchanged dependencies behind latest"
412            ))?;
413        }
414    }
415
416    Ok(())
417}
418
419fn status_locking(
420    ws: &Workspace<'_>,
421    publish_age: Option<&PublishAgePolicy>,
422    num_pkgs: usize,
423) -> CargoResult<()> {
424    use std::fmt::Write as _;
425
426    let resolver_config = ws.gctx().get::<Option<CargoResolverConfig>>("resolver")?;
427    let deny_min_publish_age = resolver_config
428        .and_then(|c| c.incompatible_publish_age)
429        .is_none_or(|v| v == IncompatiblePublishAge::Deny);
430    let publish_age = publish_age.filter(|_| deny_min_publish_age);
431    let publish_time = ws.resolve_publish_time();
432
433    let plural = if num_pkgs == 1 { "" } else { "s" };
434
435    let mut cfg = String::new();
436    // Don't have a good way to describe `direct_minimal_versions` atm
437    if !ws.gctx().cli_unstable().direct_minimal_versions {
438        write!(&mut cfg, " to")?;
439        if ws.gctx().cli_unstable().minimal_versions {
440            write!(&mut cfg, " lowest")?;
441        } else {
442            write!(&mut cfg, " highest")?;
443        }
444
445        if let Some(rust_version) = required_rust_version(ws) {
446            write!(&mut cfg, " Rust {rust_version}")?;
447        }
448        write!(&mut cfg, " compatible version{plural}")?;
449        match (publish_age, publish_time) {
450            (Some(publish_age), Some(publish_time)) => {
451                write!(
452                    &mut cfg,
453                    " as of {} before {publish_time}",
454                    publish_age
455                        .common_min_publish_age()
456                        .map(|a| a.age_label())
457                        .unwrap_or_else(|| "min-publish-age".to_owned())
458                )?;
459            }
460            (Some(publish_age), None) => {
461                write!(
462                    &mut cfg,
463                    " as of {}",
464                    publish_age
465                        .common_min_publish_age()
466                        .map(|a| format!("{} ago", a.age_label()))
467                        .unwrap_or_else(|| "min-publish-age".to_owned())
468                )?;
469            }
470            (None, Some(publish_time)) => {
471                write!(&mut cfg, " as of {publish_time}")?;
472            }
473            (None, None) => {}
474        }
475    }
476
477    ws.gctx()
478        .shell()
479        .status("Locking", format!("{num_pkgs} package{plural}{cfg}"))?;
480    Ok(())
481}
482
483fn required_rust_version(ws: &Workspace<'_>) -> Option<PartialVersion> {
484    if !ws.resolve_honors_rust_version() {
485        return None;
486    }
487
488    if let Some(ver) = ws.lowest_rust_version() {
489        Some(ver.to_partial())
490    } else {
491        let rustc = ws.gctx().load_global_rustc(Some(ws)).ok()?;
492        let rustc_version = rustc.version.clone().into();
493        Some(rustc_version)
494    }
495}
496
497fn publish_age_policy_for_report(ws: &Workspace<'_>) -> Option<PublishAgePolicy> {
498    if !ws.resolve_honors_publish_age() {
499        return None;
500    }
501    PublishAgePolicy::for_report(ws.resolve_publish_time(), ws.gctx())
502        .ok()
503        .flatten()
504}
505
506fn report_required_rust_version(resolve: &Resolve, change: &PackageChange) -> Option<String> {
507    if change.package_id.source_id().is_path() {
508        return None;
509    }
510    let summary = resolve.summary(change.package_id);
511    let package_rust_version = summary.rust_version()?;
512    let required_rust_version = change.required_rust_version.as_ref()?;
513    if package_rust_version.is_compatible_with(required_rust_version) {
514        return None;
515    }
516
517    let error = style::ERROR;
518    Some(format!(
519        " {error}(requires Rust {package_rust_version}){error:#}"
520    ))
521}
522
523/// Reports when the selected version is too new and violates `min-publish-age` config.
524fn report_too_new(
525    resolve: &Resolve,
526    change: &PackageChange,
527    publish_age: Option<&PublishAgePolicy>,
528) -> Option<String> {
529    let summary = resolve.summary(change.package_id);
530    let note = publish_age?.too_new(summary)?.note();
531
532    let warn = style::WARN;
533    Some(format!(" {warn}({note}){warn:#}"))
534}
535
536fn report_latest(
537    possibilities: &[IndexSummary],
538    change: &PackageChange,
539    publish_age: Option<&PublishAgePolicy>,
540) -> Option<String> {
541    let package_id = change.package_id;
542    if !package_id.source_id().is_registry() {
543        return None;
544    }
545
546    let version_req = package_id.version().to_caret_req();
547    let required_rust_version = change.required_rust_version.as_ref();
548
549    let publish_note = |summary| {
550        let age = publish_age?.too_new(summary)?.age_label();
551        Some(format!(", published {age} ago"))
552    };
553
554    let compat_ver_compat_msrv_summary = possibilities
555        .iter()
556        .filter_map(|s| match s {
557            IndexSummary::Candidate(s) => Some(s),
558            _ => None,
559        })
560        .filter(|s| {
561            if let (Some(summary_rust_version), Some(required_rust_version)) =
562                (s.rust_version(), required_rust_version)
563            {
564                summary_rust_version.is_compatible_with(required_rust_version)
565            } else {
566                true
567            }
568        })
569        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
570        .max_by_key(|s| s.version());
571    if let Some(summary) = compat_ver_compat_msrv_summary {
572        let warn = style::WARN;
573        let version = summary.version();
574        let publish_note = publish_note(summary).unwrap_or_default();
575        let report = format!(" {warn}(available: v{version}{publish_note}){warn:#}");
576        return Some(report);
577    }
578
579    if !change.is_transitive.unwrap_or(true) {
580        let incompat_ver_compat_msrv_summary = possibilities
581            .iter()
582            .filter_map(|s| match s {
583                IndexSummary::Candidate(s) => Some(s),
584                _ => None,
585            })
586            .filter(|s| {
587                if let (Some(summary_rust_version), Some(required_rust_version)) =
588                    (s.rust_version(), required_rust_version)
589                {
590                    summary_rust_version.is_compatible_with(required_rust_version)
591                } else {
592                    true
593                }
594            })
595            .filter(|s| is_latest(s.version(), package_id.version()))
596            .max_by_key(|s| s.version());
597        if let Some(summary) = incompat_ver_compat_msrv_summary {
598            let warn = style::WARN;
599            let version = summary.version();
600            let publish_note = publish_note(summary).unwrap_or_default();
601            let report = format!(" {warn}(available: v{version}{publish_note}){warn:#}");
602            return Some(report);
603        }
604    }
605
606    let compat_ver_summary = possibilities
607        .iter()
608        .filter_map(|s| match s {
609            IndexSummary::Candidate(s) => Some(s),
610            _ => None,
611        })
612        .filter(|s| package_id.version() != s.version() && version_req.matches(s.version()))
613        .max_by_key(|s| s.version());
614    if let Some(summary) = compat_ver_summary {
615        let msrv_note = summary
616            .rust_version()
617            .map(|rv| format!(", requires Rust {rv}"))
618            .unwrap_or_default();
619        let warn = style::NOP;
620        let version = summary.version();
621        let publish_note = publish_note(summary).unwrap_or_default();
622        let report = format!(" {warn}(available: v{version}{msrv_note}{publish_note}){warn:#}");
623        return Some(report);
624    }
625
626    if !change.is_transitive.unwrap_or(true) {
627        let incompat_ver_summary = possibilities
628            .iter()
629            .filter_map(|s| match s {
630                IndexSummary::Candidate(s) => Some(s),
631                _ => None,
632            })
633            .filter(|s| is_latest(s.version(), package_id.version()))
634            .max_by_key(|s| s.version());
635        if let Some(summary) = incompat_ver_summary {
636            let msrv_note = summary
637                .rust_version()
638                .map(|rv| format!(", requires Rust {rv}"))
639                .unwrap_or_default();
640            let warn = style::NOP;
641            let version = summary.version();
642            let publish_note = publish_note(summary).unwrap_or_default();
643            let report = format!(" {warn}(available: v{version}{msrv_note}{publish_note}){warn:#}");
644            return Some(report);
645        }
646    }
647
648    None
649}
650
651fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
652    current < candidate
653                // Only match pre-release if major.minor.patch are the same
654                && (candidate.pre.is_empty()
655                    || (candidate.major == current.major
656                        && candidate.minor == current.minor
657                        && candidate.patch == current.patch))
658}
659
660fn fill_with_deps<'a>(
661    resolve: &'a Resolve,
662    dep: PackageId,
663    set: &mut HashSet<PackageId>,
664    visited: &mut HashSet<PackageId>,
665) {
666    if !visited.insert(dep) {
667        return;
668    }
669    set.insert(dep);
670    for (dep, _) in resolve.deps_not_replaced(dep) {
671        fill_with_deps(resolve, dep, set, visited);
672    }
673}
674
675#[derive(Clone, Debug)]
676struct PackageChange {
677    package_id: PackageId,
678    previous_id: Option<PackageId>,
679    kind: PackageChangeKind,
680    is_member: Option<bool>,
681    is_transitive: Option<bool>,
682    required_rust_version: Option<PartialVersion>,
683}
684
685impl PackageChange {
686    pub fn new(ws: &Workspace<'_>, resolve: &Resolve) -> IndexMap<PackageId, Self> {
687        let diff = PackageDiff::new(resolve);
688        Self::with_diff(diff, ws, resolve)
689    }
690
691    pub fn diff(
692        ws: &Workspace<'_>,
693        previous_resolve: &Resolve,
694        resolve: &Resolve,
695    ) -> IndexMap<PackageId, Self> {
696        let diff = PackageDiff::diff(previous_resolve, resolve);
697        Self::with_diff(diff, ws, resolve)
698    }
699
700    fn with_diff(
701        diff: impl Iterator<Item = PackageDiff>,
702        ws: &Workspace<'_>,
703        resolve: &Resolve,
704    ) -> IndexMap<PackageId, Self> {
705        let member_ids: HashSet<_> = ws.members().map(|p| p.package_id()).collect();
706
707        let mut changes = IndexMap::default();
708        for diff in diff {
709            if let Some((previous_id, package_id)) = diff.change() {
710                // If versions differ only in build metadata, we call it an "update"
711                // regardless of whether the build metadata has gone up or down.
712                // This metadata is often stuff like git commit hashes, which are
713                // not meaningfully ordered.
714                let kind = if previous_id.version().cmp_precedence(package_id.version())
715                    == Ordering::Greater
716                {
717                    PackageChangeKind::Downgraded
718                } else {
719                    PackageChangeKind::Upgraded
720                };
721                let is_member = Some(member_ids.contains(&package_id));
722                let is_transitive = Some(true);
723                let change = Self {
724                    package_id,
725                    previous_id: Some(previous_id),
726                    kind,
727                    is_member,
728                    is_transitive,
729                    required_rust_version: None,
730                };
731                changes.insert(change.package_id, change);
732            } else {
733                for package_id in diff.removed {
734                    let kind = PackageChangeKind::Removed;
735                    let is_member = None;
736                    let is_transitive = None;
737                    let change = Self {
738                        package_id,
739                        previous_id: None,
740                        kind,
741                        is_member,
742                        is_transitive,
743                        required_rust_version: None,
744                    };
745                    changes.insert(change.package_id, change);
746                }
747                for package_id in diff.added {
748                    let kind = PackageChangeKind::Added;
749                    let is_member = Some(member_ids.contains(&package_id));
750                    let is_transitive = Some(true);
751                    let change = Self {
752                        package_id,
753                        previous_id: None,
754                        kind,
755                        is_member,
756                        is_transitive,
757                        required_rust_version: None,
758                    };
759                    changes.insert(change.package_id, change);
760                }
761            }
762            for package_id in diff.unchanged {
763                let kind = PackageChangeKind::Unchanged;
764                let is_member = Some(member_ids.contains(&package_id));
765                let is_transitive = Some(true);
766                let change = Self {
767                    package_id,
768                    previous_id: None,
769                    kind,
770                    is_member,
771                    is_transitive,
772                    required_rust_version: None,
773                };
774                changes.insert(change.package_id, change);
775            }
776        }
777
778        for member_id in &member_ids {
779            let Some(change) = changes.get_mut(member_id) else {
780                continue;
781            };
782            change.is_transitive = Some(false);
783            for (direct_dep_id, _) in resolve.deps(*member_id) {
784                let Some(change) = changes.get_mut(&direct_dep_id) else {
785                    continue;
786                };
787                change.is_transitive = Some(false);
788            }
789        }
790
791        changes
792    }
793
794    /// For querying [`PackageRegistry`] for alternative versions to report to the user
795    fn alternatives_query(&self) -> Option<crate::workspace::dependency::Dependency> {
796        if !self.package_id.source_id().is_registry() {
797            return None;
798        }
799
800        let query = crate::workspace::dependency::Dependency::parse(
801            self.package_id.name(),
802            None,
803            self.package_id.source_id(),
804        )
805        .expect("already a valid dependency");
806        Some(query)
807    }
808}
809
810impl std::fmt::Display for PackageChange {
811    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812        let package_id = self.package_id;
813        if let Some(previous_id) = self.previous_id {
814            if package_id.source_id().is_git() {
815                write!(
816                    f,
817                    "{previous_id} -> #{}",
818                    &package_id.source_id().precise_git_fragment().unwrap()[..8],
819                )
820            } else {
821                write!(f, "{previous_id} -> v{}", package_id.version())
822            }
823        } else {
824            write!(f, "{package_id}")
825        }
826    }
827}
828
829#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
830enum PackageChangeKind {
831    Added,
832    Removed,
833    Upgraded,
834    Downgraded,
835    Unchanged,
836}
837
838impl PackageChangeKind {
839    pub fn is_new(&self) -> bool {
840        match self {
841            Self::Added | Self::Upgraded | Self::Downgraded => true,
842            Self::Removed | Self::Unchanged => false,
843        }
844    }
845
846    pub fn status(&self) -> &'static str {
847        match self {
848            Self::Added => "Adding",
849            Self::Removed => "Removing",
850            Self::Upgraded => "Updating",
851            Self::Downgraded => "Downgrading",
852            Self::Unchanged => "Unchanged",
853        }
854    }
855
856    pub fn style(&self) -> anstyle::Style {
857        match self {
858            Self::Added => style::UPDATE_ADDED,
859            Self::Removed => style::UPDATE_REMOVED,
860            Self::Upgraded => style::UPDATE_UPGRADED,
861            Self::Downgraded => style::UPDATE_DOWNGRADED,
862            Self::Unchanged => style::UPDATE_UNCHANGED,
863        }
864    }
865}
866
867/// All resolved versions of a package name within a [`SourceId`]
868#[derive(Default, Clone, Debug)]
869pub struct PackageDiff {
870    removed: Vec<PackageId>,
871    added: Vec<PackageId>,
872    unchanged: Vec<PackageId>,
873}
874
875impl PackageDiff {
876    pub fn new(resolve: &Resolve) -> impl Iterator<Item = Self> {
877        let mut changes = BTreeMap::new();
878        let empty = Self::default();
879        for dep in resolve.iter() {
880            changes
881                .entry(Self::key(dep))
882                .or_insert_with(|| empty.clone())
883                .added
884                .push(dep);
885        }
886
887        changes.into_iter().map(|(_, v)| v)
888    }
889
890    pub fn diff(previous_resolve: &Resolve, resolve: &Resolve) -> impl Iterator<Item = Self> {
891        fn vec_subset(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
892            a.iter().filter(|a| !contains_id(b, a)).cloned().collect()
893        }
894
895        fn vec_intersection(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
896            a.iter().filter(|a| contains_id(b, a)).cloned().collect()
897        }
898
899        // Check if a PackageId is present `b` from `a`.
900        //
901        // Note that this is somewhat more complicated because the equality for source IDs does not
902        // take precise versions into account (e.g., git shas), but we want to take that into
903        // account here.
904        fn contains_id(haystack: &[PackageId], needle: &PackageId) -> bool {
905            let Ok(i) = haystack.binary_search(needle) else {
906                return false;
907            };
908
909            // If we've found `a` in `b`, then we iterate over all instances
910            // (we know `b` is sorted) and see if they all have different
911            // precise versions. If so, then `a` isn't actually in `b` so
912            // we'll let it through.
913            //
914            // Note that we only check this for non-registry sources,
915            // however, as registries contain enough version information in
916            // the package ID to disambiguate.
917            if needle.source_id().is_registry() {
918                return true;
919            }
920            haystack[i..]
921                .iter()
922                .take_while(|b| &needle == b)
923                .any(|b| needle.source_id().has_same_precise_as(b.source_id()))
924        }
925
926        // Map `(package name, package source)` to `(removed versions, added versions)`.
927        let mut changes = BTreeMap::new();
928        let empty = Self::default();
929        for dep in previous_resolve.iter() {
930            changes
931                .entry(Self::key(dep))
932                .or_insert_with(|| empty.clone())
933                .removed
934                .push(dep);
935        }
936        for dep in resolve.iter() {
937            changes
938                .entry(Self::key(dep))
939                .or_insert_with(|| empty.clone())
940                .added
941                .push(dep);
942        }
943
944        for v in changes.values_mut() {
945            let Self {
946                removed: ref mut old,
947                added: ref mut new,
948                unchanged: ref mut other,
949            } = *v;
950            old.sort();
951            new.sort();
952            let removed = vec_subset(old, new);
953            let added = vec_subset(new, old);
954            let unchanged = vec_intersection(new, old);
955            *old = removed;
956            *new = added;
957            *other = unchanged;
958        }
959        debug!("{:#?}", changes);
960
961        changes.into_iter().map(|(_, v)| v)
962    }
963
964    fn key(dep: PackageId) -> (&'static str, SourceId) {
965        (dep.name().as_str(), dep.source_id())
966    }
967
968    /// Guess if a package upgraded/downgraded
969    ///
970    /// All `PackageDiff` knows is that entries were added/removed within [`Resolve`].
971    /// A package could be added or removed because of dependencies from other packages
972    /// which makes it hard to definitively say "X was upgrade to N".
973    pub fn change(&self) -> Option<(PackageId, PackageId)> {
974        if self.removed.len() == 1 && self.added.len() == 1 {
975            Some((self.removed[0], self.added[0]))
976        } else {
977            None
978        }
979    }
980}
981
982fn annotate_required_rust_version(
983    ws: &Workspace<'_>,
984    resolve: &Resolve,
985    changes: &mut IndexMap<PackageId, PackageChange>,
986) {
987    let rustc = ws.gctx().load_global_rustc(Some(ws)).ok();
988    let rustc_version: Option<PartialVersion> =
989        rustc.as_ref().map(|rustc| rustc.version.clone().into());
990
991    if ws.resolve_honors_rust_version() {
992        let mut queue: std::collections::VecDeque<_> = ws
993            .members()
994            .map(|p| {
995                (
996                    p.rust_version()
997                        .map(|r| r.to_partial())
998                        .or_else(|| rustc_version.clone()),
999                    p.package_id(),
1000                )
1001            })
1002            .collect();
1003        while let Some((required_rust_version, current_id)) = queue.pop_front() {
1004            let Some(required_rust_version) = required_rust_version else {
1005                continue;
1006            };
1007            if let Some(change) = changes.get_mut(&current_id) {
1008                if let Some(existing) = change.required_rust_version.as_ref() {
1009                    if *existing <= required_rust_version {
1010                        // Stop early; we already walked down this path with a better match
1011                        continue;
1012                    }
1013                }
1014                change.required_rust_version = Some(required_rust_version.clone());
1015            }
1016            queue.extend(
1017                resolve
1018                    .deps(current_id)
1019                    .map(|(dep, _)| (Some(required_rust_version.clone()), dep)),
1020            );
1021        }
1022    } else {
1023        for change in changes.values_mut() {
1024            change.required_rust_version = rustc_version.clone();
1025        }
1026    }
1027}