use cargo_metadata::{Dependency, DependencyKind, Package};
use tracing::debug;
pub fn release_order<'a>(packages: &'a [&Package]) -> anyhow::Result<Vec<&'a Package>> {
let mut order = vec![];
let mut passed = vec![];
for p in packages {
release_order_inner(packages, p, &mut order, &mut passed)?;
}
debug!(
"Release order: {:?}",
order.iter().map(|p| &p.name).collect::<Vec<_>>()
);
Ok(order)
}
fn release_order_inner<'a>(
packages: &[&'a Package],
pkg: &'a Package,
order: &mut Vec<&'a Package>,
passed: &mut Vec<&'a Package>,
) -> anyhow::Result<()> {
if is_package_in(pkg, order) {
return Ok(());
}
passed.push(pkg);
for d in &pkg.dependencies {
if let Some(dep) = packages.iter().find(|p| {
d.name == *p.name
&& p.name != pkg.name
&& should_dep_be_released_before(d, pkg)
}) {
anyhow::ensure!(
!is_package_in(dep, passed),
"Circular dependency detected: {} -> {}",
dep.name,
pkg.name,
);
release_order_inner(packages, dep, order, passed)?;
}
}
order.push(pkg);
passed.clear();
Ok(())
}
fn is_package_in(pkg: &Package, packages: &[&Package]) -> bool {
packages.iter().any(|p| p.name == pkg.name)
}
fn is_dep_in_features(pkg: &Package, dep: &str) -> bool {
pkg.features
.values()
.any(|enabled_features| {
enabled_features
.iter()
.filter_map(|feature| feature.split_once('/').map(|split| split.0))
.any(|enabled_dependency| enabled_dependency == dep)
})
}
fn should_dep_be_released_before(dep: &Dependency, pkg: &Package) -> bool {
matches!(dep.kind, DependencyKind::Normal | DependencyKind::Build)
|| is_dep_in_features(pkg, &dep.name)
}
#[cfg(test)]
mod tests {
use fake_package::{FakeDependency, FakePackage};
use super::*;
use crate::publishable_packages_from_manifest;
#[test]
fn workspace_release_order_is_correct() {
let public_packages = publishable_packages_from_manifest("../../Cargo.toml").unwrap();
let pkgs = &public_packages.iter().collect::<Vec<_>>();
assert_eq!(
order(pkgs),
[
"cargo_utils",
"git_cmd",
"test_logs",
"next_version",
"release_plz_core",
"release-plz"
]
);
}
fn pkg(name: &str, deps: &[FakeDependency]) -> Package {
FakePackage::new(name)
.with_dependencies(deps.to_vec())
.into()
}
fn dep(name: &str) -> FakeDependency {
FakeDependency::new(name)
}
fn dev_dep(name: &str) -> FakeDependency {
FakeDependency::new(name).dev()
}
fn order<'a>(pkgs: &'a [&'a Package]) -> Vec<&'a str> {
release_order(pkgs)
.unwrap()
.iter()
.map(|p| p.name.as_str())
.collect()
}
#[test]
fn single_package_is_returned() {
let pkgs = [&pkg("a", &[dep("b")])];
assert_eq!(order(&pkgs), ["a"]);
}
#[test]
fn two_packages_cycle_is_detected() {
let pkgs = [&pkg("a", &[dep("b")]), &pkg("b", &[dep("a")])];
expect_test::expect!["Circular dependency detected: a -> b"]
.assert_eq(&release_order(&pkgs).unwrap_err().to_string());
}
#[test]
fn two_packages_dev_cycle_is_ok() {
let pkgs = [&pkg("a", &[dev_dep("b")]), &pkg("b", &[dep("a")])];
assert_eq!(order(&pkgs), ["a", "b"]);
let pkgs = [&pkg("b", &[dep("a")]), &pkg("a", &[dev_dep("b")])];
assert_eq!(order(&pkgs), ["a", "b"]);
}
#[test]
fn three_packages_cycle_is_detected() {
let pkgs = [
&pkg("a", &[dep("b")]),
&pkg("a", &[dep("c")]),
&pkg("b", &[dep("a")]),
&pkg("c", &[dep("b")]),
];
expect_test::expect!["Circular dependency detected: a -> b"]
.assert_eq(&release_order(&pkgs).unwrap_err().to_string());
}
#[test]
fn three_packages_are_ordered() {
let pkgs = [
&pkg("a", &[dep("b")]),
&pkg("b", &[dep("c")]),
&pkg("c", &[]),
];
assert_eq!(order(&pkgs), ["c", "b", "a"]);
}
#[test]
fn two_packages_dev_cycle_with_package_in_features_is_detected() {
let mut a = pkg("a", &[dev_dep("b")]);
a.features = [("my_feat".to_string(), vec!["b/feat".to_string()])].into();
let pkgs = [&a, &pkg("b", &[dep("a")])];
expect_test::expect!["Circular dependency detected: a -> b"]
.assert_eq(&release_order(&pkgs).unwrap_err().to_string());
}
#[test]
fn two_packages_dev_cycle_with_random_feature_is_ok() {
let mut a = pkg("a", &[dev_dep("b")]);
a.features = [(
"my_feat".to_string(),
vec!["b".to_string(), "rand/b".to_string()],
)]
.into();
let pkgs = [&a, &pkg("b", &[dep("a")])];
assert_eq!(order(&pkgs), ["a", "b"]);
}
}