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 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 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 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 to_avoid.retain(|id| {
141 for package in ws.members() {
142 let member_id = package.package_id();
143 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 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
200pub 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 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 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 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
523fn 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 && (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 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 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#[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 fn contains_id(haystack: &[PackageId], needle: &PackageId) -> bool {
905 let Ok(i) = haystack.binary_search(needle) else {
906 return false;
907 };
908
909 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 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 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(¤t_id) {
1008 if let Some(existing) = change.required_rust_version.as_ref() {
1009 if *existing <= required_rust_version {
1010 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}