use std::collections::{BTreeSet, HashMap, HashSet};
use xshell::Shell;
use crate::environment::{get_workspace_packages, Package, ProgressGuard};
use crate::git;
pub fn run(
sh: &Shell,
roots: &[String],
baseline: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let _progress = ProgressGuard::new();
let packages = internal_packages(sh, roots)?;
if packages.is_empty() {
return Err("No publishable packages found".into());
}
print_release_waves(sh, &packages, baseline)
}
fn internal_packages(
sh: &Shell,
roots: &[String],
) -> Result<Vec<Package>, Box<dyn std::error::Error>> {
let mut packages: Vec<Package> =
get_workspace_packages(sh, &[])?.into_iter().filter(|p| p.publish).collect();
let names: HashSet<String> = packages.iter().map(|p| p.name.clone()).collect();
for package in &mut packages {
package.deps.retain(|d| names.contains(d));
}
if !roots.is_empty() {
let selected = get_workspace_packages(sh, roots)?;
let by_name: HashMap<&str, &Package> =
packages.iter().map(|p| (p.name.as_str(), p)).collect();
let mut keep: HashSet<String> = HashSet::new();
let mut stack: Vec<String> = selected.into_iter().map(|p| p.name).collect();
while let Some(name) = stack.pop() {
if keep.insert(name.clone()) {
if let Some(package) = by_name.get(name.as_str()) {
stack.extend(package.deps.iter().cloned());
}
}
}
packages.retain(|p| keep.contains(&p.name));
}
Ok(packages)
}
fn print_release_waves(
sh: &Shell,
packages: &[Package],
baseline: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
for package in packages {
for dep in &package.deps {
dependents.entry(dep.as_str()).or_default().push(package.name.as_str());
}
}
let mut memo: HashMap<&str, usize> = HashMap::new();
let mut visible: Vec<(&Package, usize)> = Vec::new();
let mut depths: BTreeSet<usize> = BTreeSet::new();
for package in packages {
let show = match baseline {
Some(baseline) => git::has_changes_since(sh, baseline, &package.dir)?,
None => true,
};
if show {
let depth = package_depth(&package.name, &dependents, &mut memo);
depths.insert(depth);
visible.push((package, depth));
}
}
if let Some(baseline) = baseline {
if visible.is_empty() {
println!("No packages changed since {baseline}");
return Ok(());
}
}
visible.sort_by(|(a, _), (b, _)| a.name.cmp(&b.name));
for (wave, depth) in depths.iter().enumerate().rev() {
for (package, _) in visible.iter().filter(|(_, d)| d == depth) {
println!("{:>2} {}", wave, package.name);
}
}
Ok(())
}
fn package_depth<'a>(
name: &'a str,
dependents: &HashMap<&'a str, Vec<&'a str>>,
memo: &mut HashMap<&'a str, usize>,
) -> usize {
if let Some(&depth) = memo.get(name) {
return depth;
}
let depth = match dependents.get(name) {
None => 0,
Some(deps) =>
deps.iter().map(|dep| package_depth(dep, dependents, memo)).max().unwrap_or(0) + 1,
};
memo.insert(name, depth);
depth
}