Skip to content

verbose update of dependencies #5634

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

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 137 additions & 75 deletions src/cargo/ops/cargo_generate_lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,31 +97,39 @@ pub fn update_lockfile(ws: &Workspace, opts: &UpdateOptions) -> CargoResult<()>
let print_change = |status: &str, msg: String, color: Color| {
opts.config.shell().status_with_color(status, msg, color)
};
for (removed, added) in compare_dependency_graphs(&previous_resolve, &resolve) {
if removed.len() == 1 && added.len() == 1 {
let msg = if removed[0].source_id().is_git() {
format!(
"{} -> #{}",
removed[0],
&added[0].source_id().precise().unwrap()[..8]
)
} else {
format!("{} -> v{}", removed[0], added[0].version())
};
print_change("Updating", msg, Green)?;
} else {
for package in removed.iter() {
print_change("Removing", format!("{}", package), Red)?;
}
for package in added.iter() {
print_change("Adding", format!("{}", package), Cyan)?;
}

let diffs = compare_dependency_graphs(&previous_resolve, &resolve);

for diff in diffs {
for ((old, new), _) in diff.updated {
let msg = format_package_for_update(old, new);
print_change("Updating", format!("{}", msg), Green)?;
}

for (package, _) in diff.removed {
print_change("Removing", format!("{}", package), Red)?;
}

for (package, _) in diff.added {
print_change("Adding", format!("{}", package), Cyan)?;
}
}

ops::write_pkg_lockfile(ws, &resolve)?;
return Ok(());

fn format_package_for_update(removed: &PackageId, added: &PackageId) -> String {
if removed.source_id().is_git() {
format!(
"{} -> #{}",
removed,
&added.source_id().precise().unwrap()[..8]
)
} else {
format!("{} -> v{}", removed, added.version())
}
}

fn fill_with_deps<'a>(
resolve: &'a Resolve,
dep: &'a PackageId,
Expand All @@ -137,77 +145,131 @@ pub fn update_lockfile(ws: &Workspace, opts: &UpdateOptions) -> CargoResult<()>
}
}

fn equal_packages(a: &PackageId, b: &PackageId) -> bool {
if a.source_id().is_registry() && b.source_id().is_registry() {
a.version() == b.version()
} else {
a.source_id().precise() == b.source_id().precise()
}
}

struct DependencyGraphDiff<'a> {
/// dependencies that are not new
added: BTreeMap<&'a PackageId, Vec<&'a PackageId>>,
/// dependencies that are updated
updated: BTreeMap<(&'a PackageId, &'a PackageId), Vec<&'a PackageId>>,
/// dependencies that are no longer used
removed: BTreeMap<&'a PackageId, Vec<&'a PackageId>>,
}

fn compare_dependency_graphs<'a>(
previous_resolve: &'a Resolve,
resolve: &'a Resolve,
) -> Vec<(Vec<&'a PackageId>, Vec<&'a PackageId>)> {
) -> Vec<DependencyGraphDiff<'a>> {
fn key(dep: &PackageId) -> (&str, &SourceId) {
(dep.name().as_str(), dep.source_id())
}

// Removes all package ids in `b` from `a`. Note that this is somewhat
// more complicated because the equality for source ids does not take
// precise versions into account (e.g. git shas), but we want to take
// that into account here.
fn vec_subtract<'a>(a: &[&'a PackageId], b: &[&'a PackageId]) -> Vec<&'a PackageId> {
a.iter()
.filter(|a| {
// If this package id is not found in `b`, then it's definitely
// in the subtracted set
let i = match b.binary_search(a) {
Ok(i) => i,
Err(..) => return true,
};

// If we've found `a` in `b`, then we iterate over all instances
// (we know `b` is sorted) and see if they all have different
// precise versions. If so, then `a` isn't actually in `b` so
// we'll let it through.
//
// Note that we only check this for non-registry sources,
// however, as registries contain enough version information in
// the package id to disambiguate
if a.source_id().is_registry() {
return false;
}
b[i..]
.iter()
.take_while(|b| a == b)
.all(|b| a.source_id().precise() != b.source_id().precise())
})
.cloned()
.collect()
}

// Map (package name, package source) to (removed versions, added versions).
let mut changes = BTreeMap::new();
let empty = (Vec::new(), Vec::new());
for dep in previous_resolve.iter() {
changes
.entry(key(dep))
.or_insert_with(|| empty.clone())
.0
.push(dep);
for (id, _) in previous_resolve.deps(dep) {
changes
.entry(key(id))
.or_insert_with(BTreeMap::new)
.entry(dep)
.or_insert_with(|| (None, None))
.0 = Some(id);
}
}
for dep in resolve.iter() {
changes
.entry(key(dep))
.or_insert_with(|| empty.clone())
.1
.push(dep);
for (id, _) in resolve.deps(dep) {
changes
.entry(key(id))
.or_insert_with(BTreeMap::new)
.entry(dep)
.or_insert_with(|| (None, None))
.1 = Some(id);
}
}

for v in changes.values_mut() {
let (ref mut old, ref mut new) = *v;
old.sort();
new.sort();
let removed = vec_subtract(old, new);
let added = vec_subtract(new, old);
*old = removed;
*new = added;
}
debug!("{:#?}", changes);

changes.into_iter().map(|(_, v)| v).collect()
let mut diffs = Vec::new();

for (_, dependents) in changes {
let mut added = BTreeMap::new();
let mut updated = BTreeMap::new();
let mut removed = BTreeMap::new();
let mut unchanged = HashSet::new();

for (dependent, versions) in dependents {
match versions {
(None, Some(new)) => {
added.entry(new)
.or_insert_with(Vec::new)
.push(dependent);
},
(Some(old), Some(new)) if !equal_packages(old, new) => {
updated.entry((old, new))
.or_insert_with(Vec::new)
.push(dependent);
},
(Some(old), None) => {
removed.entry(old)
.or_insert_with(Vec::new)
.push(dependent);
},
(Some(_), Some(new)) => {
unchanged.insert(new);
},
_ => (),
}
}

// if it is still one of our dependencies,
// there is no reason to print the message
for dep in unchanged {
added.remove(dep);
removed.remove(dep);
}

// if it is removed or added, but also updated,
// there is no reason to print the message
for ((old, new), _) in updated.iter() {
added.remove(new);
removed.remove(old);
}

// handle cases when crate and it's dependency has been updated at the same time
// e.g.
//
// foo 0.1.0
// -> bar 0.1.0
//
// foo 0.1.1
// -> bar 0.1.1
if removed.len() == 1 && added.len() == 1 {
for ((old, _), (new, dependents)) in removed.iter().zip(added.iter()) {
if !equal_packages(old, new) {
updated.entry((*old, *new))
.or_insert_with(Vec::new)
.extend(dependents);
}
}
added.clear();
removed.clear();
}

let diff = DependencyGraphDiff {
added,
updated,
removed,
};

diffs.push(diff);
}

diffs
}
}