use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::path::PathBuf;
use anyhow::Context;
use log::info;
use crate::model::changeset::{ChangeType, Changeset};
use crate::model::config::DependencyBump;
use crate::package_manager::Project;
use super::version::effective_new_version;
use super::{PropagationMap, PropagationResult};
pub(crate) fn build_reverse_dep_graph(projects: &[Project]) -> BTreeMap<String, Vec<String>> {
let project_names: BTreeSet<String> = projects.iter().map(|p| p.name().to_string()).collect();
let mut reverse_deps: BTreeMap<String, Vec<String>> = BTreeMap::new();
for project in projects {
for dep_name in project.dependency_names() {
if project_names.contains(dep_name.as_str()) {
reverse_deps
.entry(dep_name.clone())
.or_default()
.push(project.name().to_string());
}
}
}
reverse_deps
}
pub(crate) fn mark_propagation_bumps(
aggregated: &BTreeMap<String, ChangeType>,
version_overrides: &BTreeMap<String, semver::Version>,
reverse_deps: &BTreeMap<String, Vec<String>>,
dep_bump: DependencyBump,
) -> PropagationMap {
let mut queue: VecDeque<(String, ChangeType)> = aggregated
.iter()
.map(|(name, &ct)| (name.clone(), ct))
.collect();
let mut propagation_map: PropagationMap = BTreeMap::new();
while let Some((bumped_name, upstream_ct)) = queue.pop_front() {
let effective_ct = dep_bump.to_change_type(upstream_ct);
let Some(dependents) = reverse_deps.get(&bumped_name) else {
continue;
};
for dependent_name in dependents {
if version_overrides.contains_key(dependent_name.as_str()) {
continue; }
let current_ct = aggregated
.get(dependent_name.as_str())
.copied()
.or_else(|| {
propagation_map
.get(dependent_name.as_str())
.map(|(ct, _)| *ct)
});
if current_ct.is_some_and(|c| c >= effective_ct) {
continue; }
let entry = propagation_map
.entry(dependent_name.clone())
.or_insert_with(|| (effective_ct, BTreeSet::new()));
entry.0 = effective_ct;
entry.1.insert(bumped_name.clone());
queue.push_back((dependent_name.clone(), effective_ct));
}
}
propagation_map
}
pub(super) async fn write_out_of_scope_changeset(
pkg_name: &str,
effective_ct: ChangeType,
dep_msgs: &[String],
env: &crate::Env,
dry_run: bool,
) -> anyhow::Result<Option<PathBuf>> {
let message = format!("Dependency updates: {}", dep_msgs.join(", "));
let mut packages = BTreeMap::new();
packages.insert(pkg_name.to_string(), effective_ct);
let changeset = Changeset::new(packages, Some(message));
if dry_run {
info!(
"Would write dependency propagation changeset for \
out-of-scope package '{pkg_name}' ({effective_ct})"
);
return Ok(None);
}
let path = changeset
.write(env.git(), env.fs())
.await
.with_context(|| format!("Failed to write propagation changeset for '{pkg_name}'"))?;
info!(
"Wrote dependency propagation changeset for '{pkg_name}': {}",
path.display()
);
Ok(Some(path))
}
pub(super) async fn apply_dependency_propagation(
projects: &[Project],
aggregated: &mut BTreeMap<String, ChangeType>,
version_overrides: &BTreeMap<String, semver::Version>,
package_filter: &[String],
dep_bump: DependencyBump,
env: &crate::Env,
dry_run: bool,
) -> anyhow::Result<PropagationResult> {
let reverse_deps = build_reverse_dep_graph(projects);
let propagation_map =
mark_propagation_bumps(aggregated, version_overrides, &reverse_deps, dep_bump);
if propagation_map.is_empty() {
return Ok((BTreeMap::new(), Vec::new()));
}
let mut dep_entries: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut new_changeset_paths: Vec<PathBuf> = Vec::new();
for (pkg_name, (effective_ct, upstream_names)) in &propagation_map {
let dep_msgs: Vec<String> = upstream_names
.iter()
.map(|up| {
match effective_new_version(
up,
projects,
aggregated,
version_overrides,
&propagation_map,
) {
Some(v) => format!("`{up}` bumped to {v}"),
None => format!("`{up}` bumped"),
}
})
.collect();
if package_filter.is_empty() || package_filter.contains(pkg_name) {
let existing_ct = aggregated.get(pkg_name.as_str()).copied();
if existing_ct.is_none_or(|c| c < *effective_ct) {
aggregated.insert(pkg_name.clone(), *effective_ct);
dep_entries.insert(pkg_name.clone(), dep_msgs);
info!(
"{pkg_name}: dependency propagation bump ({effective_ct}) from {}",
upstream_names
.iter()
.cloned()
.collect::<Vec<_>>()
.join(", ")
);
}
} else if let Some(path) =
write_out_of_scope_changeset(pkg_name, *effective_ct, &dep_msgs, env, dry_run).await?
{
new_changeset_paths.push(path);
}
}
Ok((dep_entries, new_changeset_paths))
}