use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use anyhow::Result;
use cargo_metadata::{CargoOpt, Metadata, MetadataCommand};
use indexmap::IndexSet;
use crate::{error::AppError, vcs::ChangedFile};
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub struct Crates(HashSet<CrateInfo>);
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct CrateInfo {
pub name: String,
pub path: PathBuf,
}
pub fn get_workspace_metadata(workspace_root: &Path) -> Result<Metadata, AppError> {
let metadata = MetadataCommand::new()
.manifest_path(workspace_root.join("Cargo.toml"))
.features(CargoOpt::AllFeatures)
.no_deps()
.exec()
.map_err(|e| AppError::MetadataFailed {
reason: e.to_string(),
})?;
Ok(metadata)
}
pub fn get_workspace_crates(metadata: &Metadata) -> Result<Crates, AppError> {
let mut crates = HashSet::new();
for package in &metadata.packages {
let manifest_dir =
Path::new(&package.manifest_path)
.parent()
.ok_or_else(|| AppError::MetadataFailed {
reason: "Failed to get parent directory".to_string(),
})?;
crates.insert(CrateInfo {
name: package.name.clone(),
path: manifest_dir.to_path_buf(),
});
}
Ok(Crates(crates))
}
fn find_crate_for_file<'a>(file_path: &Path, crates: &'a Crates) -> Option<&'a CrateInfo> {
let mut best_match: Option<&CrateInfo> = None;
let mut best_match_components = 0;
for crate_info in &crates.0 {
if file_path.starts_with(&crate_info.path) {
let component_count = crate_info.path.components().count();
if best_match.is_none() || component_count > best_match_components {
best_match = Some(crate_info);
best_match_components = component_count;
}
}
}
best_match
}
pub fn find_changed_crates<'a>(
changed_files: &[ChangedFile],
crates: &'a Crates,
) -> Result<IndexSet<&'a String>, AppError> {
let mut changed_crates = IndexSet::new();
for change in changed_files {
if let Some(crate_info) = find_crate_for_file(&change.current_path, crates) {
changed_crates.insert(&crate_info.name);
}
if let Some(old_path) = &change.old_path {
if let Some(crate_info) = find_crate_for_file(old_path, crates) {
changed_crates.insert(&crate_info.name);
}
}
}
Ok(changed_crates)
}
pub fn find_dependent_crates<'a>(
changed_crates: &IndexSet<&String>,
metadata: &'a cargo_metadata::Metadata,
) -> Result<IndexSet<&'a String>> {
let mut dependent_crates = IndexSet::new();
for package in &metadata.packages {
for dep in &package.dependencies {
if changed_crates.contains(&dep.name) {
dependent_crates.insert(&package.name);
}
}
}
Ok(dependent_crates)
}
pub fn verify_crates_exist(
metadata: &cargo_metadata::Metadata,
crates: &[String],
) -> Result<(), AppError> {
for crate_name in crates {
if !metadata.packages.iter().any(|p| p.name == *crate_name) {
return Err(AppError::UnknownCrate {
crate_name: crate_name.clone(),
});
}
}
Ok(())
}