From 3a6142fb7cae90b619b9d1961ee0ab08c0bbc0e0 Mon Sep 17 00:00:00 2001 From: Tor Hovland <55164+torhovland@users.noreply.github.com> Date: Mon, 27 May 2024 12:08:32 +0200 Subject: [PATCH] feat: Implement `cargo update --breaking`. --- crates/cargo-test-support/src/compare.rs | 1 + src/bin/cargo/commands/update.rs | 31 ++- src/cargo/core/summary.rs | 16 +- src/cargo/ops/cargo_update.rs | 257 +++++++++++++++++- src/cargo/ops/mod.rs | 2 + src/cargo/util/toml_mut/manifest.rs | 66 +++-- src/cargo/util/toml_mut/mod.rs | 1 + src/cargo/util/toml_mut/upgrade.rs | 219 +++++++++++++++ src/doc/src/reference/unstable.md | 20 ++ .../cargo_update/help/stdout.term.svg | 44 +-- tests/testsuite/update.rs | 130 ++++++--- 11 files changed, 701 insertions(+), 86 deletions(-) create mode 100644 src/cargo/util/toml_mut/upgrade.rs diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index 53b752621de8..a6a9d30e7af9 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -176,6 +176,7 @@ static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[ ("[DIRTY]", " Dirty"), ("[LOCKING]", " Locking"), ("[UPDATING]", " Updating"), + ("[UPGRADING]", " Upgrading"), ("[ADDING]", " Adding"), ("[REMOVING]", " Removing"), ("[REMOVED]", " Removed"), diff --git a/src/bin/cargo/commands/update.rs b/src/bin/cargo/commands/update.rs index fb394e4aa336..4586098302e1 100644 --- a/src/bin/cargo/commands/update.rs +++ b/src/bin/cargo/commands/update.rs @@ -35,6 +35,13 @@ pub fn cli() -> Command { .value_name("PRECISE") .requires("package-group"), ) + .arg( + flag( + "breaking", + "Upgrade [SPEC] to latest breaking versions, unless pinned (unstable)", + ) + .short('b'), + ) .arg_silent_suggestion() .arg( flag("workspace", "Only update the workspace packages") @@ -59,7 +66,8 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { gctx.cli_unstable().msrv_policy, )?; } - let ws = args.workspace(gctx)?; + + let mut ws = args.workspace(gctx)?; if args.is_present_with_zero_values("package") { print_available_packages(&ws)?; @@ -84,11 +92,30 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { let update_opts = UpdateOptions { recursive: args.flag("recursive"), precise: args.get_one::("precise").map(String::as_str), + breaking: args.flag("breaking"), to_update, dry_run: args.dry_run(), workspace: args.flag("workspace"), gctx, }; - ops::update_lockfile(&ws, &update_opts)?; + + if update_opts.breaking { + gctx.cli_unstable() + .fail_if_stable_opt("--breaking", 12425)?; + + let upgrades = ops::upgrade_manifests(&mut ws, &update_opts)?; + ops::resolve_ws(&ws, update_opts.dry_run)?; + ops::write_manifest_upgrades(&ws, &update_opts, &upgrades)?; + + if update_opts.dry_run { + update_opts + .gctx + .shell() + .warn("aborting update due to dry run")?; + } + } else { + ops::update_lockfile(&ws, &update_opts)?; + } + Ok(()) } diff --git a/src/cargo/core/summary.rs b/src/cargo/core/summary.rs index ec0197cf40d0..d7744e24ef73 100644 --- a/src/cargo/core/summary.rs +++ b/src/cargo/core/summary.rs @@ -103,15 +103,25 @@ impl Summary { Rc::make_mut(&mut self.inner).checksum = Some(cksum); } - pub fn map_dependencies(mut self, f: F) -> Summary + pub fn map_dependencies(self, mut f: F) -> Summary where F: FnMut(Dependency) -> Dependency, + { + self.try_map_dependencies(|dep| Ok(f(dep))).unwrap() + } + + pub fn try_map_dependencies(mut self, f: F) -> CargoResult + where + F: FnMut(Dependency) -> CargoResult, { { let slot = &mut Rc::make_mut(&mut self.inner).dependencies; - *slot = mem::take(slot).into_iter().map(f).collect(); + *slot = mem::take(slot) + .into_iter() + .map(f) + .collect::>()?; } - self + Ok(self) } pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Summary { diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs index fc22608e8f23..1f0a4dca613d 100644 --- a/src/cargo/ops/cargo_update.rs +++ b/src/cargo/ops/cargo_update.rs @@ -1,3 +1,4 @@ +use crate::core::dependency::Dependency; use crate::core::registry::PackageRegistry; use crate::core::resolver::features::{CliFeatures, HasDevUnits}; use crate::core::shell::Verbosity; @@ -8,17 +9,25 @@ use crate::ops; use crate::sources::source::QueryKind; use crate::util::cache_lock::CacheLockMode; use crate::util::context::GlobalContext; -use crate::util::style; -use crate::util::CargoResult; +use crate::util::toml_mut::dependency::{MaybeWorkspace, Source}; +use crate::util::toml_mut::manifest::LocalManifest; +use crate::util::toml_mut::upgrade::upgrade_requirement; +use crate::util::{style, OptVersionReq}; +use crate::util::{CargoResult, VersionExt}; +use itertools::Itertools; +use semver::{Op, Version, VersionReq}; use std::cmp::Ordering; -use std::collections::{BTreeMap, HashSet}; -use tracing::debug; +use std::collections::{BTreeMap, HashMap, HashSet}; +use tracing::{debug, trace}; + +pub type UpgradeMap = HashMap<(String, SourceId), Version>; pub struct UpdateOptions<'a> { pub gctx: &'a GlobalContext, pub to_update: Vec, pub precise: Option<&'a str>, pub recursive: bool, + pub breaking: bool, pub dry_run: bool, pub workspace: bool, } @@ -207,6 +216,246 @@ pub fn print_lockfile_changes( } } +pub fn upgrade_manifests( + ws: &mut Workspace<'_>, + opts: &UpdateOptions<'_>, +) -> CargoResult { + let mut upgrades = HashMap::new(); + let mut upgrade_messages = HashSet::new(); + + // Updates often require a lot of modifications to the registry, so ensure + // that we're synchronized against other Cargos. + let _lock = ws + .gctx() + .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?; + + let mut registry = PackageRegistry::new(opts.gctx)?; + registry.lock_patches(); + + for member in ws.members_mut().sorted() { + debug!("upgrading manifest for `{}`", member.name()); + + *member.manifest_mut().summary_mut() = member + .manifest() + .summary() + .clone() + .try_map_dependencies(|d| { + upgrade_dependency(&mut registry, &mut upgrades, &mut upgrade_messages, opts, d) + })?; + } + + Ok(upgrades) +} + +fn upgrade_dependency( + registry: &mut PackageRegistry<'_>, + upgrades: &mut UpgradeMap, + upgrade_messages: &mut HashSet, + opts: &UpdateOptions<'_>, + dependency: Dependency, +) -> CargoResult { + let name = dependency.package_name(); + let renamed_to = dependency.name_in_toml(); + + if name != renamed_to { + trace!( + "skipping dependency renamed from `{}` to `{}`", + name, + renamed_to + ); + return Ok(dependency); + } + + if !opts.to_update.is_empty() && !opts.to_update.contains(&name.to_string()) { + trace!("skipping dependency `{}` not selected for upgrading", name); + return Ok(dependency); + } + + if !dependency.source_id().is_registry() { + trace!("skipping non-registry dependency: {}", name); + return Ok(dependency); + } + + let version_req = dependency.version_req(); + + let OptVersionReq::Req(current) = version_req else { + trace!( + "skipping dependency `{}` without a simple version requirement: {}", + name, + version_req + ); + return Ok(dependency); + }; + + let [comparator] = ¤t.comparators[..] else { + trace!( + "skipping dependency `{}` with multiple version comparators: {:?}", + name, + ¤t.comparators + ); + return Ok(dependency); + }; + + if comparator.op != Op::Caret { + trace!("skipping non-caret dependency `{}`: {}", name, comparator); + return Ok(dependency); + } + + let query = + crate::core::dependency::Dependency::parse(name, None, dependency.source_id().clone())?; + + let possibilities = { + loop { + match registry.query_vec(&query, QueryKind::Exact) { + std::task::Poll::Ready(res) => { + break res?; + } + std::task::Poll::Pending => registry.block_until_ready()?, + } + } + }; + + let latest = if !possibilities.is_empty() { + possibilities + .iter() + .map(|s| s.as_summary()) + .map(|s| s.version()) + .filter(|v| !v.is_prerelease()) + .max() + } else { + None + }; + + let Some(latest) = latest else { + trace!( + "skipping dependency `{}` without any published versions", + name + ); + return Ok(dependency); + }; + + if current.matches(&latest) { + trace!( + "skipping dependency `{}` without a breaking update available", + name + ); + return Ok(dependency); + } + + let Some(new_req_string) = upgrade_requirement(¤t.to_string(), latest)? else { + trace!( + "skipping dependency `{}` because the version requirement didn't change", + name + ); + return Ok(dependency); + }; + + let upgrade_message = format!("{} {} -> {}", name, current, new_req_string); + trace!(upgrade_message); + + if upgrade_messages.insert(upgrade_message.clone()) { + opts.gctx + .shell() + .status_with_color("Upgrading", &upgrade_message, &style::GOOD)?; + } + + upgrades.insert((name.to_string(), dependency.source_id()), latest.clone()); + + let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?); + let mut dep = dependency.clone(); + dep.set_version_req(req); + Ok(dep) +} + +/// Update manifests with upgraded versions, and write to disk. Based on cargo-edit. +/// Returns true if any file has changed. +pub fn write_manifest_upgrades( + ws: &Workspace<'_>, + opts: &UpdateOptions<'_>, + upgrades: &UpgradeMap, +) -> CargoResult { + if upgrades.is_empty() { + return Ok(false); + } + + let mut any_file_has_changed = false; + + let manifest_paths = std::iter::once(ws.root_manifest()) + .chain(ws.members().map(|member| member.manifest_path())) + .collect::>(); + + for manifest_path in manifest_paths { + trace!( + "updating TOML manifest at `{:?}` with upgraded dependencies", + manifest_path + ); + + let crate_root = manifest_path + .parent() + .expect("manifest path is absolute") + .to_owned(); + + let mut local_manifest = LocalManifest::try_new(&manifest_path)?; + let mut manifest_has_changed = false; + + for dep_table in local_manifest.get_dependency_tables_mut() { + for (mut dep_key, dep_item) in dep_table.iter_mut() { + let dep_key_str = dep_key.get(); + let dependency = crate::util::toml_mut::dependency::Dependency::from_toml( + &manifest_path, + dep_key_str, + dep_item, + )?; + + let Some(current) = dependency.version() else { + trace!("skipping dependency without a version: {}", dependency.name); + continue; + }; + + let (MaybeWorkspace::Other(source_id), Some(Source::Registry(source))) = + (dependency.source_id(opts.gctx)?, dependency.source()) + else { + trace!("skipping non-registry dependency: {}", dependency.name); + continue; + }; + + let Some(latest) = upgrades.get(&(dependency.name.to_owned(), source_id)) else { + trace!( + "skipping dependency without an upgrade: {}", + dependency.name + ); + continue; + }; + + let Some(new_req_string) = upgrade_requirement(current, latest)? else { + trace!( + "skipping dependency `{}` because the version requirement didn't change", + dependency.name + ); + continue; + }; + + let mut dep = dependency.clone(); + let mut source = source.clone(); + source.version = new_req_string; + dep.source = Some(Source::Registry(source)); + + trace!("upgrading dependency {}", dependency.name); + dep.update_toml(&crate_root, &mut dep_key, dep_item); + manifest_has_changed = true; + any_file_has_changed = true; + } + } + + if manifest_has_changed && !opts.dry_run { + debug!("writing upgraded manifest to {}", manifest_path.display()); + local_manifest.write()?; + } + } + + Ok(any_file_has_changed) +} + fn print_lockfile_generation( ws: &Workspace<'_>, resolve: &Resolve, diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 88b422e5b301..49dfaf53fc87 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -19,6 +19,8 @@ pub use self::cargo_uninstall::uninstall; pub use self::cargo_update::generate_lockfile; pub use self::cargo_update::print_lockfile_changes; pub use self::cargo_update::update_lockfile; +pub use self::cargo_update::upgrade_manifests; +pub use self::cargo_update::write_manifest_upgrades; pub use self::cargo_update::UpdateOptions; pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions}; pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile}; diff --git a/src/cargo/util/toml_mut/manifest.rs b/src/cargo/util/toml_mut/manifest.rs index 1fbfb9deccab..d7ed18440d29 100644 --- a/src/cargo/util/toml_mut/manifest.rs +++ b/src/cargo/util/toml_mut/manifest.rs @@ -276,23 +276,6 @@ impl LocalManifest { /// Write changes back to the file. pub fn write(&self) -> CargoResult<()> { - if !self.manifest.data.contains_key("package") - && !self.manifest.data.contains_key("project") - { - if self.manifest.data.contains_key("workspace") { - anyhow::bail!( - "found virtual manifest at {}, but this command requires running against an \ - actual package in this workspace.", - self.path.display() - ); - } else { - anyhow::bail!( - "missing expected `package` or `project` fields in {}", - self.path.display() - ); - } - } - let s = self.manifest.data.to_string(); let new_contents_bytes = s.as_bytes(); @@ -397,6 +380,55 @@ impl LocalManifest { Ok(()) } + /// Allow mutating depedencies, wherever they live. + /// Copied from cargo-edit. + pub fn get_dependency_tables_mut( + &mut self, + ) -> impl Iterator + '_ { + let root = self.data.as_table_mut(); + root.iter_mut().flat_map(|(k, v)| { + if DepTable::KINDS + .iter() + .any(|dt| dt.kind.kind_table() == k.get()) + { + v.as_table_like_mut().into_iter().collect::>() + } else if k == "workspace" { + v.as_table_like_mut() + .unwrap() + .iter_mut() + .filter_map(|(k, v)| { + if k.get() == "dependencies" { + v.as_table_like_mut() + } else { + None + } + }) + .collect::>() + } else if k == "target" { + v.as_table_like_mut() + .unwrap() + .iter_mut() + .flat_map(|(_, v)| { + v.as_table_like_mut().into_iter().flat_map(|v| { + v.iter_mut().filter_map(|(k, v)| { + if DepTable::KINDS + .iter() + .any(|dt| dt.kind.kind_table() == k.get()) + { + v.as_table_like_mut() + } else { + None + } + }) + }) + }) + .collect::>() + } else { + Vec::new() + } + }) + } + /// Remove references to `dep_key` if its no longer present. pub fn gc_dep(&mut self, dep_key: &str) { let explicit_dep_activation = self.is_explicit_dep_activation(dep_key); diff --git a/src/cargo/util/toml_mut/mod.rs b/src/cargo/util/toml_mut/mod.rs index cb5d3aaf2920..1da707ea192d 100644 --- a/src/cargo/util/toml_mut/mod.rs +++ b/src/cargo/util/toml_mut/mod.rs @@ -11,6 +11,7 @@ pub mod dependency; pub mod manifest; +pub mod upgrade; // Based on Iterator::is_sorted from nightly std; remove in favor of that when stabilized. pub fn is_sorted(mut it: impl Iterator) -> bool { diff --git a/src/cargo/util/toml_mut/upgrade.rs b/src/cargo/util/toml_mut/upgrade.rs new file mode 100644 index 000000000000..5bf7dacee4bd --- /dev/null +++ b/src/cargo/util/toml_mut/upgrade.rs @@ -0,0 +1,219 @@ +use std::fmt::Display; + +use crate::CargoResult; + +/// Upgrade an existing requirement to a new version. +/// Copied from cargo-edit. +pub(crate) fn upgrade_requirement( + req: &str, + version: &semver::Version, +) -> CargoResult> { + let req_text = req.to_string(); + let raw_req = semver::VersionReq::parse(&req_text) + .expect("semver to generate valid version requirements"); + if raw_req.comparators.is_empty() { + // Empty matches everything, no-change. + Ok(None) + } else { + let comparators: CargoResult> = raw_req + .comparators + .into_iter() + .map(|p| set_comparator(p, version)) + .collect(); + let comparators = comparators?; + let new_req = semver::VersionReq { comparators }; + let mut new_req_text = new_req.to_string(); + if new_req_text.starts_with('^') && !req.starts_with('^') { + new_req_text.remove(0); + } + // Validate contract + #[cfg(debug_assertions)] + { + assert!( + new_req.matches(version), + "New req {} is invalid, because {} does not match {}", + new_req_text, + new_req, + version + ) + } + if new_req_text == req_text { + Ok(None) + } else { + Ok(Some(new_req_text)) + } + } +} + +fn set_comparator( + mut pred: semver::Comparator, + version: &semver::Version, +) -> CargoResult { + match pred.op { + semver::Op::Wildcard => { + pred.major = version.major; + if pred.minor.is_some() { + pred.minor = Some(version.minor); + } + if pred.patch.is_some() { + pred.patch = Some(version.patch); + } + Ok(pred) + } + semver::Op::Exact => Ok(assign_partial_req(version, pred)), + semver::Op::Greater | semver::Op::GreaterEq | semver::Op::Less | semver::Op::LessEq => { + let user_pred = pred.to_string(); + Err(unsupported_version_req(user_pred)) + } + semver::Op::Tilde => Ok(assign_partial_req(version, pred)), + semver::Op::Caret => Ok(assign_partial_req(version, pred)), + _ => { + let user_pred = pred.to_string(); + Err(unsupported_version_req(user_pred)) + } + } +} + +fn assign_partial_req( + version: &semver::Version, + mut pred: semver::Comparator, +) -> semver::Comparator { + pred.major = version.major; + if pred.minor.is_some() { + pred.minor = Some(version.minor); + } + if pred.patch.is_some() { + pred.patch = Some(version.patch); + } + pred.pre = version.pre.clone(); + pred +} + +fn unsupported_version_req(req: impl Display) -> anyhow::Error { + anyhow::format_err!("Support for modifying {} is currently unsupported", req) +} + +#[cfg(test)] +mod test { + use super::*; + + mod upgrade_requirement { + use super::*; + + #[track_caller] + fn assert_req_bump<'a, O: Into>>(version: &str, req: &str, expected: O) { + let version = semver::Version::parse(version).unwrap(); + let actual = upgrade_requirement(req, &version).unwrap(); + let expected = expected.into(); + assert_eq!(actual.as_deref(), expected); + } + + #[test] + fn wildcard_major() { + assert_req_bump("1.0.0", "*", None); + } + + #[test] + fn wildcard_minor() { + assert_req_bump("1.0.0", "1.*", None); + assert_req_bump("1.1.0", "1.*", None); + assert_req_bump("2.0.0", "1.*", "2.*"); + } + + #[test] + fn wildcard_patch() { + assert_req_bump("1.0.0", "1.0.*", None); + assert_req_bump("1.1.0", "1.0.*", "1.1.*"); + assert_req_bump("1.1.1", "1.0.*", "1.1.*"); + assert_req_bump("2.0.0", "1.0.*", "2.0.*"); + } + + #[test] + fn caret_major() { + assert_req_bump("1.0.0", "1", None); + assert_req_bump("1.0.0", "^1", None); + + assert_req_bump("1.1.0", "1", None); + assert_req_bump("1.1.0", "^1", None); + + assert_req_bump("2.0.0", "1", "2"); + assert_req_bump("2.0.0", "^1", "^2"); + } + + #[test] + fn caret_minor() { + assert_req_bump("1.0.0", "1.0", None); + assert_req_bump("1.0.0", "^1.0", None); + + assert_req_bump("1.1.0", "1.0", "1.1"); + assert_req_bump("1.1.0", "^1.0", "^1.1"); + + assert_req_bump("1.1.1", "1.0", "1.1"); + assert_req_bump("1.1.1", "^1.0", "^1.1"); + + assert_req_bump("2.0.0", "1.0", "2.0"); + assert_req_bump("2.0.0", "^1.0", "^2.0"); + } + + #[test] + fn caret_patch() { + assert_req_bump("1.0.0", "1.0.0", None); + assert_req_bump("1.0.0", "^1.0.0", None); + + assert_req_bump("1.1.0", "1.0.0", "1.1.0"); + assert_req_bump("1.1.0", "^1.0.0", "^1.1.0"); + + assert_req_bump("1.1.1", "1.0.0", "1.1.1"); + assert_req_bump("1.1.1", "^1.0.0", "^1.1.1"); + + assert_req_bump("2.0.0", "1.0.0", "2.0.0"); + assert_req_bump("2.0.0", "^1.0.0", "^2.0.0"); + } + + #[test] + fn tilde_major() { + assert_req_bump("1.0.0", "~1", None); + assert_req_bump("1.1.0", "~1", None); + assert_req_bump("2.0.0", "~1", "~2"); + } + + #[test] + fn tilde_minor() { + assert_req_bump("1.0.0", "~1.0", None); + assert_req_bump("1.1.0", "~1.0", "~1.1"); + assert_req_bump("1.1.1", "~1.0", "~1.1"); + assert_req_bump("2.0.0", "~1.0", "~2.0"); + } + + #[test] + fn tilde_patch() { + assert_req_bump("1.0.0", "~1.0.0", None); + assert_req_bump("1.1.0", "~1.0.0", "~1.1.0"); + assert_req_bump("1.1.1", "~1.0.0", "~1.1.1"); + assert_req_bump("2.0.0", "~1.0.0", "~2.0.0"); + } + + #[test] + fn equal_major() { + assert_req_bump("1.0.0", "=1", None); + assert_req_bump("1.1.0", "=1", None); + assert_req_bump("2.0.0", "=1", "=2"); + } + + #[test] + fn equal_minor() { + assert_req_bump("1.0.0", "=1.0", None); + assert_req_bump("1.1.0", "=1.0", "=1.1"); + assert_req_bump("1.1.1", "=1.0", "=1.1"); + assert_req_bump("2.0.0", "=1.0", "=2.0"); + } + + #[test] + fn equal_patch() { + assert_req_bump("1.0.0", "=1.0.0", None); + assert_req_bump("1.1.0", "=1.0.0", "=1.1.0"); + assert_req_bump("1.1.1", "=1.0.0", "=1.1.1"); + assert_req_bump("2.0.0", "=1.0.0", "=2.0.0"); + } + } +} diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index fce26e146c54..060d6ffb6449 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -73,6 +73,7 @@ For the latest nightly, see the [nightly version] of this page. * [public-dependency](#public-dependency) --- Allows dependencies to be classified as either public or private. * [msrv-policy](#msrv-policy) --- MSRV-aware resolver and version selection * [precise-pre-release](#precise-pre-release) --- Allows pre-release versions to be selected with `update --precise` + * [update-breaking](#update-breaking) --- Allows upgrading to breaking versions with `update --breaking` * Output behavior * [out-dir](#out-dir) --- Adds a directory where artifacts are copied to. * [Different binary name](#different-binary-name) --- Assign a name to the built binary that is separate from the crate name. @@ -378,6 +379,25 @@ It's possible to update `my-dependency` to a pre-release with `update -Zunstable This is because `0.1.2-pre.0` is considered compatible with `0.1.1`. It would not be possible to upgrade to `0.2.0-pre.0` from `0.1.1` in the same way. +## update-breaking + +* Tracking Issue: [#12425](https://github.com/rust-lang/cargo/issues/12425) + +This feature allows upgrading dependencies to breaking versions with +`update --breaking`. + +This is essentially migrating `cargo upgrade` from `cargo-edit` into Cargo itself, +and involves making changes to the `Cargo.toml` manifests, not just the lock file. + +When doing a breaking update, Cargo will keep all non-breaking dependencies +unchanged. It will also not change any dependencies that use a different version +operator than the default caret. Also, it will not upgrade any renamed package +dependencies. Example: + +```sh +cargo +nightly update --breaking -Z unstable-options +``` + ## build-std * Tracking Repository: diff --git a/tests/testsuite/cargo_update/help/stdout.term.svg b/tests/testsuite/cargo_update/help/stdout.term.svg index 9642c49d2a2d..565032d86996 100644 --- a/tests/testsuite/cargo_update/help/stdout.term.svg +++ b/tests/testsuite/cargo_update/help/stdout.term.svg @@ -1,4 +1,4 @@ - +