use crate::error::{VersionError, VersionResult};
use crate::types::{
Changeset, CircularDependency, DependencyUpdate, PackageInfo, UpdateReason, Version,
VersionBump, VersioningStrategy,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VersionResolution {
pub updates: Vec<PackageUpdate>,
pub circular_dependencies: Vec<CircularDependency>,
}
impl VersionResolution {
#[must_use]
pub fn new() -> Self {
Self { updates: Vec::new(), circular_dependencies: Vec::new() }
}
#[must_use]
pub fn has_updates(&self) -> bool {
!self.updates.is_empty()
}
#[must_use]
pub fn update_count(&self) -> usize {
self.updates.len()
}
#[must_use]
pub fn has_circular_dependencies(&self) -> bool {
!self.circular_dependencies.is_empty()
}
pub(crate) fn add_update(&mut self, update: PackageUpdate) {
self.updates.push(update);
}
#[allow(dead_code)]
pub(crate) fn add_circular_dependencies(&mut self, circular: Vec<CircularDependency>) {
self.circular_dependencies.extend(circular);
}
}
impl Default for VersionResolution {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PackageUpdate {
pub name: String,
pub path: PathBuf,
pub current_version: Version,
pub next_version: Version,
pub reason: UpdateReason,
pub dependency_updates: Vec<DependencyUpdate>,
}
impl PackageUpdate {
#[must_use]
pub fn new(
name: String,
path: PathBuf,
current_version: Version,
next_version: Version,
reason: UpdateReason,
) -> Self {
Self { name, path, current_version, next_version, reason, dependency_updates: Vec::new() }
}
#[must_use]
pub fn is_direct_change(&self) -> bool {
matches!(self.reason, UpdateReason::DirectChange)
}
#[must_use]
pub fn is_propagated(&self) -> bool {
matches!(self.reason, UpdateReason::DependencyPropagation { .. })
}
pub(crate) fn add_dependency_update(&mut self, dep_update: DependencyUpdate) {
self.dependency_updates.push(dep_update);
}
}
pub async fn resolve_versions(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
strategy: VersioningStrategy,
) -> VersionResult<VersionResolution> {
validate_packages_exist(changeset, packages)?;
match strategy {
VersioningStrategy::Independent => resolve_independent(changeset, packages).await,
VersioningStrategy::Unified => resolve_unified(changeset, packages).await,
}
}
fn validate_packages_exist(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
) -> VersionResult<()> {
for package_name in &changeset.packages {
if !packages.contains_key(package_name) {
return Err(VersionError::PackageNotFound {
name: package_name.clone(),
workspace_root: PathBuf::from("."),
});
}
}
Ok(())
}
pub async fn resolve_versions_with_prerelease(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
strategy: VersioningStrategy,
prerelease_config: Option<&crate::types::prerelease::PrereleaseConfig>,
) -> VersionResult<VersionResolution> {
validate_packages_exist(changeset, packages)?;
match strategy {
VersioningStrategy::Independent => {
resolve_independent_with_prerelease(changeset, packages, prerelease_config).await
}
VersioningStrategy::Unified => {
resolve_unified_with_prerelease(changeset, packages, prerelease_config).await
}
}
}
async fn resolve_independent(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
) -> VersionResult<VersionResolution> {
let mut resolution = VersionResolution::new();
for package_name in &changeset.packages {
let package_info =
packages.get(package_name).ok_or_else(|| VersionError::PackageNotFound {
name: package_name.clone(),
workspace_root: PathBuf::from("."),
})?;
let current_version = package_info.version();
let next_version = current_version.bump(changeset.bump)?;
let update = PackageUpdate::new(
package_name.clone(),
package_info.path().to_path_buf(),
current_version,
next_version,
UpdateReason::DirectChange,
);
resolution.add_update(update);
}
Ok(resolution)
}
async fn resolve_unified_with_prerelease(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
prerelease_config: Option<&crate::types::prerelease::PrereleaseConfig>,
) -> VersionResult<VersionResolution> {
let mut resolution = VersionResolution::new();
let mut highest_version: Option<Version> = None;
for package_info in packages.values() {
let current_version = package_info.version();
highest_version = match highest_version {
None => Some(current_version),
Some(ref h) if current_version > *h => Some(current_version),
Some(h) => Some(h),
};
}
let unified_next_version = if let Some(version) = highest_version {
version.bump_with_prerelease(changeset.bump, prerelease_config)?
} else {
return Ok(resolution);
};
for (package_name, package_info) in packages {
let current_version = package_info.version();
let reason = if changeset.packages.contains(package_name) {
UpdateReason::DirectChange
} else {
UpdateReason::UnifiedStrategy
};
let update = PackageUpdate::new(
package_name.clone(),
package_info.path().to_path_buf(),
current_version,
unified_next_version.clone(),
reason,
);
resolution.add_update(update);
}
Ok(resolution)
}
pub async fn resolve_versions_with_prerelease_auto(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
strategy: VersioningStrategy,
prerelease_tag: Option<&str>,
) -> VersionResult<VersionResolution> {
validate_packages_exist(changeset, packages)?;
match strategy {
VersioningStrategy::Independent => {
resolve_independent_with_prerelease_auto(changeset, packages, prerelease_tag).await
}
VersioningStrategy::Unified => {
resolve_unified_with_prerelease_auto(changeset, packages, prerelease_tag).await
}
}
}
async fn resolve_independent_with_prerelease(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
prerelease_config: Option<&crate::types::prerelease::PrereleaseConfig>,
) -> VersionResult<VersionResolution> {
let mut resolution = VersionResolution::new();
for package_name in &changeset.packages {
let package_info =
packages.get(package_name).ok_or_else(|| VersionError::PackageNotFound {
name: package_name.clone(),
workspace_root: PathBuf::from("."),
})?;
let current_version = package_info.version();
let next_version =
current_version.bump_with_prerelease(changeset.bump, prerelease_config)?;
let update = PackageUpdate::new(
package_name.clone(),
package_info.path().to_path_buf(),
current_version,
next_version,
UpdateReason::DirectChange,
);
resolution.add_update(update);
}
Ok(resolution)
}
async fn resolve_independent_with_prerelease_auto(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
prerelease_tag: Option<&str>,
) -> VersionResult<VersionResolution> {
let mut resolution = VersionResolution::new();
for package_name in &changeset.packages {
let package_info =
packages.get(package_name).ok_or_else(|| VersionError::PackageNotFound {
name: package_name.clone(),
workspace_root: PathBuf::from("."),
})?;
let current_version = package_info.version();
let next_version =
current_version.bump_with_prerelease_auto(changeset.bump, prerelease_tag)?;
let update = PackageUpdate::new(
package_name.clone(),
package_info.path().to_path_buf(),
current_version,
next_version,
UpdateReason::DirectChange,
);
resolution.add_update(update);
}
Ok(resolution)
}
async fn resolve_unified_with_prerelease_auto(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
prerelease_tag: Option<&str>,
) -> VersionResult<VersionResolution> {
let mut resolution = VersionResolution::new();
let mut highest_version: Option<Version> = None;
for package_info in packages.values() {
let current_version = package_info.version();
highest_version = match highest_version {
None => Some(current_version),
Some(ref h) if current_version > *h => Some(current_version),
Some(h) => Some(h),
};
}
let unified_next_version = if let Some(version) = highest_version {
version.bump_with_prerelease_auto(changeset.bump, prerelease_tag)?
} else {
let base = Version::new(1, 0, 0);
if let Some(tag) = prerelease_tag {
base.bump_with_prerelease_auto(VersionBump::None, Some(tag))?
} else {
base
}
};
for (package_name, package_info) in packages {
let current_version = package_info.version();
let reason = if changeset.packages.contains(package_name) {
UpdateReason::DirectChange
} else {
UpdateReason::UnifiedStrategy
};
let update = PackageUpdate::new(
package_name.clone(),
package_info.path().to_path_buf(),
current_version,
unified_next_version.clone(),
reason,
);
resolution.add_update(update);
}
Ok(resolution)
}
async fn resolve_unified(
changeset: &Changeset,
packages: &HashMap<String, PackageInfo>,
) -> VersionResult<VersionResolution> {
let mut resolution = VersionResolution::new();
let mut highest_version: Option<Version> = None;
for package_info in packages.values() {
let current_version = package_info.version();
highest_version = match highest_version {
Some(ref existing) => {
if ¤t_version > existing {
Some(current_version)
} else {
highest_version
}
}
None => Some(current_version),
};
}
let unified_next_version = if let Some(version) = highest_version {
version.bump(changeset.bump)?
} else {
return Ok(resolution);
};
for (package_name, package_info) in packages {
let current_version = package_info.version();
let reason = if changeset.packages.contains(package_name) {
UpdateReason::DirectChange
} else {
UpdateReason::UnifiedStrategy
};
let update = PackageUpdate::new(
package_name.clone(),
package_info.path().to_path_buf(),
current_version,
unified_next_version.clone(),
reason,
);
resolution.add_update(update);
}
Ok(resolution)
}