Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Report some dependency changes on any command #13561

Merged
merged 4 commits into from Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/cargo-test-support/src/compare.rs
Expand Up @@ -204,6 +204,7 @@ fn substitute_macros(input: &str) -> String {
("[SCRAPING]", " Scraping"),
("[FRESH]", " Fresh"),
("[DIRTY]", " Dirty"),
("[LOCKING]", " Locking"),
("[UPDATING]", " Updating"),
("[ADDING]", " Adding"),
("[REMOVING]", " Removing"),
Expand Down
231 changes: 201 additions & 30 deletions src/cargo/ops/cargo_generate_lockfile.rs
Expand Up @@ -10,7 +10,6 @@ use crate::util::cache_lock::CacheLockMode;
use crate::util::context::GlobalContext;
use crate::util::style;
use crate::util::CargoResult;
use anstyle::Style;
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashSet};
use tracing::debug;
Expand All @@ -26,17 +25,19 @@ pub struct UpdateOptions<'a> {

pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> {
let mut registry = PackageRegistry::new(ws.gctx())?;
let previous_resolve = None;
let mut resolve = ops::resolve_with_previous(
&mut registry,
ws,
&CliFeatures::new_all(true),
HasDevUnits::Yes,
None,
previous_resolve,
None,
&[],
true,
)?;
ops::write_pkg_lockfile(ws, &mut resolve)?;
print_lockfile_changes(ws.gctx(), previous_resolve, &resolve, &mut registry)?;
Ok(())
}

Expand Down Expand Up @@ -154,7 +155,7 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
true,
)?;

print_lockfile_update(opts.gctx, &previous_resolve, &resolve, &mut registry)?;
print_lockfile_updates(opts.gctx, &previous_resolve, &resolve, &mut registry)?;
if opts.dry_run {
opts.gctx
.shell()
Expand All @@ -165,30 +166,177 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
Ok(())
}

fn print_lockfile_update(
pub fn print_lockfile_changes(
gctx: &GlobalContext,
previous_resolve: Option<&Resolve>,
resolve: &Resolve,
registry: &mut PackageRegistry<'_>,
) -> CargoResult<()> {
if let Some(previous_resolve) = previous_resolve {
print_lockfile_sync(gctx, previous_resolve, resolve, registry)
} else {
print_lockfile_generation(gctx, resolve, registry)
}
}

fn print_lockfile_generation(
gctx: &GlobalContext,
resolve: &Resolve,
registry: &mut PackageRegistry<'_>,
) -> CargoResult<()> {
let mut shell = gctx.shell();

let diff = PackageDiff::new(&resolve);
let num_pkgs: usize = diff.iter().map(|d| d.added.len()).sum();
if num_pkgs <= 1 {
// just ourself, nothing worth reporting
return Ok(());
}
shell.status("Locking", format!("{num_pkgs} packages"))?;

for diff in diff {
fn format_latest(version: semver::Version) -> String {
weihanglo marked this conversation as resolved.
Show resolved Hide resolved
let warn = style::WARN;
format!(" {warn}(latest: v{version}){warn:#}")
}
let possibilities = if let Some(query) = diff.alternatives_query() {
loop {
match registry.query_vec(&query, QueryKind::Exact) {
std::task::Poll::Ready(res) => {
break res?;
}
std::task::Poll::Pending => registry.block_until_ready()?,
}
}
} else {
vec![]
};

for package in diff.added.iter() {
let latest = if !possibilities.is_empty() {
possibilities
.iter()
.map(|s| s.as_summary())
.filter(|s| is_latest(s.version(), package.version()))
.map(|s| s.version().clone())
.max()
.map(format_latest)
} else {
None
};

if let Some(latest) = latest {
shell.status_with_color("Adding", format!("{package}{latest}"), &style::NOTE)?;
}
}
}

Ok(())
}

fn print_lockfile_sync(
gctx: &GlobalContext,
previous_resolve: &Resolve,
resolve: &Resolve,
registry: &mut PackageRegistry<'_>,
) -> CargoResult<()> {
// Summarize what is changing for the user.
let print_change = |status: &str, msg: String, color: &Style| {
gctx.shell().status_with_color(status, msg, color)
};
let mut shell = gctx.shell();

let diff = PackageDiff::diff(&previous_resolve, &resolve);
let num_pkgs: usize = diff.iter().map(|d| d.added.len()).sum();
if num_pkgs == 0 {
return Ok(());
}
let plural = if num_pkgs == 1 { "" } else { "s" };
shell.status("Locking", format!("{num_pkgs} package{plural}"))?;

for diff in diff {
fn format_latest(version: semver::Version) -> String {
let warn = style::WARN;
format!(" {warn}(latest: v{version}){warn:#}")
}
let possibilities = if let Some(query) = diff.alternatives_query() {
loop {
match registry.query_vec(&query, QueryKind::Exact) {
std::task::Poll::Ready(res) => {
break res?;
}
std::task::Poll::Pending => registry.block_until_ready()?,
}
}
} else {
vec![]
};

if let Some((removed, added)) = diff.change() {
let latest = if !possibilities.is_empty() {
possibilities
.iter()
.map(|s| s.as_summary())
.filter(|s| is_latest(s.version(), added.version()))
.map(|s| s.version().clone())
.max()
.map(format_latest)
} else {
None
}
.unwrap_or_default();

let msg = if removed.source_id().is_git() {
format!(
"{removed} -> #{}",
&added.source_id().precise_git_fragment().unwrap()[..8],
)
} else {
format!("{removed} -> v{}{latest}", added.version())
};

// If versions differ only in build metadata, we call it an "update"
// regardless of whether the build metadata has gone up or down.
// This metadata is often stuff like git commit hashes, which are
// not meaningfully ordered.
if removed.version().cmp_precedence(added.version()) == Ordering::Greater {
shell.status_with_color("Downgrading", msg, &style::WARN)?;
} else {
shell.status_with_color("Updating", msg, &style::GOOD)?;
}
} else {
for package in diff.added.iter() {
let latest = if !possibilities.is_empty() {
possibilities
.iter()
.map(|s| s.as_summary())
.filter(|s| is_latest(s.version(), package.version()))
.map(|s| s.version().clone())
.max()
.map(format_latest)
} else {
None
}
.unwrap_or_default();

shell.status_with_color("Adding", format!("{package}{latest}"), &style::NOTE)?;
}
}
}

Ok(())
}

pub fn print_lockfile_updates(
weihanglo marked this conversation as resolved.
Show resolved Hide resolved
gctx: &GlobalContext,
previous_resolve: &Resolve,
resolve: &Resolve,
registry: &mut PackageRegistry<'_>,
) -> CargoResult<()> {
let mut shell = gctx.shell();

let mut unchanged_behind = 0;
for diff in PackageDiff::diff(&previous_resolve, &resolve) {
fn format_latest(version: semver::Version) -> String {
let warn = style::WARN;
format!(" {warn}(latest: v{version}){warn:#}")
}
fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
current < candidate
// Only match pre-release if major.minor.patch are the same
&& (candidate.pre.is_empty()
|| (candidate.major == current.major
&& candidate.minor == current.minor
&& candidate.patch == current.patch))
}
let possibilities = if let Some(query) = diff.alternatives_query() {
loop {
match registry.query_vec(&query, QueryKind::Exact) {
Expand Down Expand Up @@ -230,13 +378,13 @@ fn print_lockfile_update(
// This metadata is often stuff like git commit hashes, which are
// not meaningfully ordered.
if removed.version().cmp_precedence(added.version()) == Ordering::Greater {
print_change("Downgrading", msg, &style::WARN)?;
shell.status_with_color("Downgrading", msg, &style::WARN)?;
} else {
print_change("Updating", msg, &style::GOOD)?;
shell.status_with_color("Updating", msg, &style::GOOD)?;
}
} else {
for package in diff.removed.iter() {
print_change("Removing", format!("{package}"), &style::ERROR)?;
shell.status_with_color("Removing", format!("{package}"), &style::ERROR)?;
}
for package in diff.added.iter() {
let latest = if !possibilities.is_empty() {
Expand All @@ -252,7 +400,7 @@ fn print_lockfile_update(
}
.unwrap_or_default();

print_change("Adding", format!("{package}{latest}"), &style::NOTE)?;
shell.status_with_color("Adding", format!("{package}{latest}"), &style::NOTE)?;
}
}
for package in &diff.unchanged {
Expand All @@ -270,8 +418,8 @@ fn print_lockfile_update(

if let Some(latest) = latest {
unchanged_behind += 1;
if gctx.shell().verbosity() == Verbosity::Verbose {
gctx.shell().status_with_color(
if shell.verbosity() == Verbosity::Verbose {
shell.status_with_color(
"Unchanged",
format!("{package}{latest}"),
&anstyle::Style::new().bold(),
Expand All @@ -280,13 +428,13 @@ fn print_lockfile_update(
}
}
}
if gctx.shell().verbosity() == Verbosity::Verbose {
gctx.shell().note(
if shell.verbosity() == Verbosity::Verbose {
shell.note(
"to see how you depend on a package, run `cargo tree --invert --package <dep>@<ver>`",
)?;
} else {
if 0 < unchanged_behind {
gctx.shell().note(format!(
shell.note(format!(
"pass `--verbose` to see {unchanged_behind} unchanged dependencies behind latest"
))?;
}
Expand All @@ -295,6 +443,15 @@ fn print_lockfile_update(
Ok(())
}

fn is_latest(candidate: &semver::Version, current: &semver::Version) -> bool {
current < candidate
// Only match pre-release if major.minor.patch are the same
&& (candidate.pre.is_empty()
|| (candidate.major == current.major
&& candidate.minor == current.minor
&& candidate.patch == current.patch))
}

fn fill_with_deps<'a>(
resolve: &'a Resolve,
dep: PackageId,
Expand All @@ -319,11 +476,21 @@ pub struct PackageDiff {
}

impl PackageDiff {
pub fn diff(previous_resolve: &Resolve, resolve: &Resolve) -> Vec<Self> {
fn key(dep: PackageId) -> (&'static str, SourceId) {
(dep.name().as_str(), dep.source_id())
pub fn new(resolve: &Resolve) -> Vec<Self> {
let mut changes = BTreeMap::new();
let empty = Self::default();
for dep in resolve.iter() {
changes
.entry(Self::key(dep))
.or_insert_with(|| empty.clone())
.added
.push(dep);
}

changes.into_iter().map(|(_, v)| v).collect()
}

pub fn diff(previous_resolve: &Resolve, resolve: &Resolve) -> Vec<Self> {
fn vec_subset(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
a.iter().filter(|a| !contains_id(b, a)).cloned().collect()
}
Expand Down Expand Up @@ -364,14 +531,14 @@ impl PackageDiff {
let empty = Self::default();
for dep in previous_resolve.iter() {
changes
.entry(key(dep))
.entry(Self::key(dep))
.or_insert_with(|| empty.clone())
.removed
.push(dep);
}
for dep in resolve.iter() {
changes
.entry(key(dep))
.entry(Self::key(dep))
.or_insert_with(|| empty.clone())
.added
.push(dep);
Expand All @@ -397,6 +564,10 @@ impl PackageDiff {
changes.into_iter().map(|(_, v)| v).collect()
}

fn key(dep: PackageId) -> (&'static str, SourceId) {
(dep.name().as_str(), dep.source_id())
}

/// Guess if a package upgraded/downgraded
///
/// All `PackageDiff` knows is that entries were added/removed within [`Resolve`].
Expand Down