#
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}