use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::path::Path;
use cargo_metadata::{Dependency, Target};
use tracing::{debug, trace};
use crate::diff::DiffItem;
use crate::error::Error;
use crate::format::{make_diff, path_from_dep, path_from_target, value_from_dep, value_from_target};
use crate::krate::Crate;
use crate::metadata::Metadata;
use crate::utils::file_mode;
use super::common::dependency_partial_eq;
use super::version_req::compare_version_req;
pub struct CrateComparator<'a> {
old: &'a Crate,
new: &'a Crate,
}
impl<'a> CrateComparator<'a> {
pub const fn new(old: &'a Crate, new: &'a Crate) -> Self {
Self { old, new }
}
pub fn compare(&self) -> Result<Vec<DiffItem>, Error> {
let mut items = Vec::new();
let old_metadata = &self.old.metadata;
let new_metadata = &self.new.metadata;
let old_contents = self.old.file_contents()?;
let new_contents = self.new.file_contents()?;
debug!("Comparing crate contents");
items.extend(compare_metadata(old_metadata, new_metadata));
for file in &new_contents.files {
if !old_contents.files.contains(file) {
items.push(DiffItem::FileAdded {
path: file.to_string_lossy().to_string(),
});
continue;
}
trace!("Comparing files at path: {}", file.to_string_lossy());
items.extend(self.compare_contents(&file, &file)?);
items.extend(self.compare_modes(&file, &file)?);
}
for file in &old_contents.files {
if !new_contents.files.contains(file) {
items.push(DiffItem::FileRemoved {
path: file.to_string_lossy().to_string(),
});
}
}
Ok(items)
}
fn path_in_crate_old(&self, path: &str) -> String {
let name = self.old.metadata.inner.name.as_str();
let version = self.old.metadata.inner.version.to_string();
format!("{name}-{version}/{path}")
}
fn path_in_crate_new(&self, path: &str) -> String {
let name = self.new.metadata.inner.name.as_str();
let version = self.new.metadata.inner.version.to_string();
format!("{name}-{version}/{path}")
}
fn compare_contents<P: AsRef<Path>>(&self, old_path: P, new_path: P) -> Result<Vec<DiffItem>, Error> {
let mut items = Vec::new();
let old_file_content = self.old.read_entry_to_bytes(&old_path)?;
let new_file_content = self.new.read_entry_to_bytes(&new_path)?;
if old_file_content != new_file_content {
let old_fc_lf: Vec<_> = old_file_content.iter().filter(|c| **c != b'\r').collect();
let new_fc_lf: Vec<_> = new_file_content.iter().filter(|c| **c != b'\r').collect();
if old_fc_lf == new_fc_lf {
items.push(DiffItem::LineEndingsChange {
path: old_path.as_ref().to_string_lossy().to_string(),
});
} else {
let diff = if let (Ok(old_file_utf8), Ok(new_file_utf8)) = (
self.old.read_entry_to_string(&old_path),
self.new.read_entry_to_string(&new_path),
) {
Some(make_diff(
&old_file_utf8,
&new_file_utf8,
Some((
&self.path_in_crate_old(&old_path.as_ref().to_string_lossy()),
&self.path_in_crate_new(&new_path.as_ref().to_string_lossy()),
)),
))
} else {
None
};
items.push(DiffItem::FileChanged {
path: old_path.as_ref().to_string_lossy().to_string(),
diff,
});
}
}
Ok(items)
}
fn compare_modes<P: AsRef<Path>>(&self, old_path: P, new_path: P) -> Result<Vec<DiffItem>, Error> {
let mut items = Vec::new();
let old_file_mode = file_mode(self.old.root.join(&old_path))?;
let new_file_mode = file_mode(self.new.root.join(&new_path))?;
if old_file_mode != new_file_mode {
items.push(DiffItem::PermissionChange {
path: old_path.as_ref().to_string_lossy().to_string(),
old: old_file_mode,
new: new_file_mode,
});
}
Ok(items)
}
}
#[expect(clippy::too_many_lines)]
fn compare_metadata(old_metadata: &Metadata, new_metadata: &Metadata) -> Vec<DiffItem> {
let mut items = Vec::new();
let old_md = &old_metadata.inner;
let new_md = &new_metadata.inner;
if old_md.name != new_md.name {
items.push(DiffItem::NameChange {
old: old_md.name.to_string(),
new: new_md.name.to_string(),
});
}
if old_md.version != new_md.version {
items.push(DiffItem::VersionChange {
old: old_md.version.to_string(),
new: new_md.version.to_string(),
});
}
if old_md.edition != new_md.edition {
items.push(DiffItem::EditionChange {
old: old_md.edition.to_string(),
new: new_md.edition.to_string(),
});
}
if old_md.rust_version != new_md.rust_version {
items.push(DiffItem::RustVersionChange {
old: old_md.rust_version.as_ref().map(ToString::to_string),
new: new_md.rust_version.as_ref().map(ToString::to_string),
});
}
if old_md.authors != new_md.authors {
let (added, removed) = compare_str_list(&old_md.authors, &new_md.authors);
items.push(DiffItem::AuthorsChange { added, removed });
}
if old_md.description != new_md.description {
items.push(DiffItem::DescriptionChange {
old: old_md.description.clone(),
new: new_md.description.clone(),
});
}
if old_md.license != new_md.license {
items.push(DiffItem::LicenseChange {
old: old_md.license.clone(),
new: new_md.license.clone(),
});
}
if old_md.license_file != new_md.license_file {
items.push(DiffItem::LicenseFileChange {
old: old_md.license_file.as_ref().map(ToString::to_string),
new: new_md.license_file.as_ref().map(ToString::to_string),
});
}
if old_md.readme != new_md.readme {
items.push(DiffItem::ReadmeChange {
old: old_md.readme.as_ref().map(ToString::to_string),
new: new_md.readme.as_ref().map(ToString::to_string),
});
}
if old_md.categories != new_md.categories {
let (added, removed) = compare_str_list(&old_md.categories, &new_md.categories);
items.push(DiffItem::CategoriesChange { added, removed });
}
if old_md.keywords != new_md.keywords {
let (added, removed) = compare_str_list(&old_md.keywords, &new_md.keywords);
items.push(DiffItem::KeywordsChange { added, removed });
}
if old_md.repository != new_md.repository {
items.push(DiffItem::RepositoryChange {
old: old_md.repository.clone(),
new: new_md.repository.clone(),
});
}
if old_md.homepage != new_md.homepage {
items.push(DiffItem::HomepageChange {
old: old_md.homepage.clone(),
new: new_md.homepage.clone(),
});
}
if old_md.documentation != new_md.documentation {
items.push(DiffItem::DocumentationChange {
old: old_md.documentation.clone(),
new: new_md.documentation.clone(),
});
}
if old_md.links != new_md.links {
items.push(DiffItem::LinksChange {
old: old_md.links.clone(),
new: new_md.links.clone(),
});
}
if old_md.default_run != new_md.default_run {
items.push(DiffItem::DefaultRunChange {
old: old_md.default_run.clone(),
new: new_md.default_run.clone(),
});
}
if old_md.metadata != new_md.metadata {
items.push(DiffItem::MetadataChange {
old: old_md.metadata.clone(),
new: new_md.metadata.clone(),
});
}
items.extend(compare_dependencies(&old_md.dependencies, &new_md.dependencies));
items.extend(compare_targets(&old_md.targets, &new_md.targets));
items.extend(compare_features(&old_md.features, &new_md.features));
items
}
fn compare_dependencies(old_deps: &[Dependency], new_deps: &[Dependency]) -> Vec<DiffItem> {
if old_deps.len() == new_deps.len() && old_deps.iter().zip(new_deps).all(|(k, u)| dependency_partial_eq(k, u)) {
return Vec::new();
}
let mut items = Vec::new();
let old_deps_map: BTreeMap<String, &Dependency> = old_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect();
let new_deps_map: BTreeMap<String, &Dependency> = new_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect();
for (path, old_dep) in &old_deps_map {
if let Some(new_dep) = new_deps_map.get(path) {
let (lower_bound, upper_bound) = compare_version_req(&old_dep.req, &new_dep.req);
if lower_bound == Ordering::Greater || upper_bound == Ordering::Greater {
items.push(DiffItem::DependencyUpgraded {
path: path_from_dep(new_dep),
old: old_dep.req.to_string(),
new: new_dep.req.to_string(),
});
}
if lower_bound == Ordering::Less || upper_bound == Ordering::Less {
items.push(DiffItem::DependencyDowngraded {
path: path_from_dep(new_dep),
old: old_dep.req.to_string(),
new: new_dep.req.to_string(),
});
}
let (added_features, removed_features) = compare_str_list(&old_dep.features, &new_dep.features);
if !added_features.is_empty() || !removed_features.is_empty() {
items.push(DiffItem::DependencyFeatures {
path: path_from_dep(new_dep),
added: added_features,
removed: removed_features,
});
}
if old_dep.optional != new_dep.optional {
items.push(DiffItem::DependencyOptionality {
path: path_from_dep(new_dep),
old: old_dep.optional,
new: new_dep.optional,
});
}
} else {
items.push(DiffItem::DependencyRemoved {
path: path_from_dep(old_dep),
value: value_from_dep(old_dep),
});
}
}
for (path, new_dep) in &new_deps_map {
if !old_deps_map.contains_key(path) {
items.push(DiffItem::DependencyAdded {
path: path_from_dep(new_dep),
value: value_from_dep(new_dep),
});
}
}
items
}
fn compare_targets(old_targets: &[Target], new_targets: &[Target]) -> Vec<DiffItem> {
if old_targets == new_targets {
return Vec::new();
}
let mut items = Vec::new();
let old_target_map: BTreeMap<String, &Target> = old_targets
.iter()
.map(|target| (path_from_target(target), target))
.collect();
let new_target_map: BTreeMap<String, &Target> = new_targets
.iter()
.map(|target| (path_from_target(target), target))
.collect();
for (path, old_target) in &old_target_map {
if let Some(new_target) = new_target_map.get(path) {
if old_target != new_target {
items.push(DiffItem::TargetChanged {
path: path.clone(),
old: value_from_target(old_target),
new: value_from_target(new_target),
});
}
} else {
items.push(DiffItem::TargetRemoved {
path: path.clone(),
target: value_from_target(old_target),
});
}
}
for (path, new_target) in &new_target_map {
if !old_target_map.contains_key(path) {
items.push(DiffItem::TargetAdded {
path: path.clone(),
target: value_from_target(new_target),
});
}
}
items
}
type Features = BTreeMap<String, Vec<String>>;
fn compare_features(old_features: &Features, new_features: &Features) -> Vec<DiffItem> {
if old_features == new_features {
return Vec::new();
}
let mut items = Vec::new();
for (old_key, old_values) in old_features {
match new_features.get(old_key) {
Some(new_values) => {
if old_values == new_values {
continue;
}
let (added, removed) = compare_str_list(old_values, new_values);
items.push(DiffItem::FeatureChanged {
name: String::from(old_key),
added,
removed,
});
},
None => {
items.push(DiffItem::FeatureRemoved {
name: String::from(old_key),
});
},
}
}
for new_key in new_features.keys() {
if old_features.get(new_key).is_none() {
items.push(DiffItem::FeatureAdded {
name: String::from(new_key),
});
}
}
items
}
fn compare_str_list(old: &[String], new: &[String]) -> (Vec<String>, Vec<String>) {
let mut added = Vec::new();
let mut removed = Vec::new();
for new_str in new {
if !old.contains(new_str) {
added.push(new_str.clone());
}
}
for old_str in old {
if !new.contains(old_str) {
removed.push(old_str.clone());
}
}
(added, removed)
}