governor-core 1.10.1

Core domain and application logic for cargo-governor
Documentation
//! Release domain entity

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use super::version::SemanticVersion;

/// A release of a crate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Release {
    /// Crate name
    pub crate_name: String,
    /// Release version
    pub version: SemanticVersion,
    /// Release date
    pub released_at: DateTime<Utc>,
    /// Git tag for this release
    pub tag: Option<String>,
    /// Git commit hash for this release
    pub commit_hash: Option<String>,
    /// Release notes/changelog
    pub notes: Option<String>,
    /// Whether this release is published to crates.io
    pub published: bool,
    /// Whether this is a pre-release
    pub is_prerelease: bool,
    /// Downloads (if published)
    pub downloads: Option<u64>,
}

impl Release {
    /// Create a new release
    #[must_use]
    pub const fn new(
        crate_name: String,
        version: SemanticVersion,
        released_at: DateTime<Utc>,
    ) -> Self {
        let is_prerelease = version.is_prerelease;
        Self {
            crate_name,
            version,
            released_at,
            tag: None,
            commit_hash: None,
            notes: None,
            published: false,
            is_prerelease,
            downloads: None,
        }
    }

    /// Get the release version
    #[must_use]
    pub const fn version(&self) -> &SemanticVersion {
        &self.version
    }

    /// Check if this is a stable release
    #[must_use]
    pub const fn is_stable(&self) -> bool {
        !self.is_prerelease
    }

    /// Get the tag name for this release
    #[must_use]
    pub fn tag_name(&self) -> String {
        self.tag
            .clone()
            .unwrap_or_else(|| format!("v{}", self.version))
    }

    /// Mark as published
    pub const fn mark_published(&mut self) {
        self.published = true;
    }

    /// Set git tag
    #[must_use]
    pub fn with_tag(mut self, tag: String) -> Self {
        self.tag = Some(tag);
        self
    }

    /// Set commit hash
    #[must_use]
    pub fn with_commit_hash(mut self, hash: String) -> Self {
        self.commit_hash = Some(hash);
        self
    }

    /// Set release notes
    #[must_use]
    pub fn with_notes(mut self, notes: String) -> Self {
        self.notes = Some(notes);
        self
    }
}

/// Release status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReleaseStatus {
    /// Release is planned
    Planned,
    /// Release is in progress
    InProgress,
    /// Release completed successfully
    Completed,
    /// Release failed
    Failed,
    /// Release was rolled back
    RolledBack,
}

/// Release plan for a workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleasePlan {
    /// Target version for the release
    pub version: SemanticVersion,
    /// Crates to include in this release
    pub crates: Vec<CrateReleasePlan>,
    /// Release status
    pub status: ReleaseStatus,
    /// When the plan was created
    pub created_at: DateTime<Utc>,
    /// Release notes
    pub release_notes: Option<String>,
}

impl ReleasePlan {
    /// Create a new release plan
    #[must_use]
    pub fn new(version: SemanticVersion) -> Self {
        Self {
            version,
            crates: Vec::new(),
            status: ReleaseStatus::Planned,
            created_at: Utc::now(),
            release_notes: None,
        }
    }

    /// Add a crate to the release plan
    pub fn add_crate(&mut self, plan: CrateReleasePlan) {
        self.crates.push(plan);
    }

    /// Get crates that need to be published
    #[must_use]
    pub fn publishable_crates(&self) -> Vec<&CrateReleasePlan> {
        self.crates.iter().filter(|c| c.should_publish).collect()
    }

    /// Get crates in publish order
    #[must_use]
    pub fn publish_order(&self) -> Vec<&CrateReleasePlan> {
        let mut crates = self.publishable_crates();
        crates.sort_by_key(|c| c.publish_order);
        crates
    }

    /// Check if all crates are published
    #[must_use]
    pub fn all_published(&self) -> bool {
        self.publishable_crates().iter().all(|c| c.published)
    }

    /// Mark as in progress
    pub const fn mark_in_progress(&mut self) {
        self.status = ReleaseStatus::InProgress;
    }

    /// Mark as completed
    pub const fn mark_completed(&mut self) {
        self.status = ReleaseStatus::Completed;
    }

    /// Mark as failed
    pub const fn mark_failed(&mut self) {
        self.status = ReleaseStatus::Failed;
    }
}

/// Release plan for a single crate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateReleasePlan {
    /// Crate name
    pub name: String,
    /// Current version
    pub current_version: SemanticVersion,
    /// Target version
    pub target_version: SemanticVersion,
    /// Whether this crate should be published
    pub should_publish: bool,
    /// Whether the crate is already published
    pub published: bool,
    /// Publish order (lower = publish first)
    pub publish_order: usize,
    /// Dependencies (workspace crates that must be published first)
    pub dependencies: Vec<String>,
    /// Estimated publish time in seconds
    pub estimated_time_secs: u64,
}

impl CrateReleasePlan {
    /// Create a new crate release plan
    #[must_use]
    pub const fn new(
        name: String,
        current_version: SemanticVersion,
        target_version: SemanticVersion,
    ) -> Self {
        Self {
            name,
            current_version,
            target_version,
            should_publish: false,
            published: false,
            publish_order: 0,
            dependencies: Vec::new(),
            estimated_time_secs: 45,
        }
    }

    /// Mark as should publish
    #[must_use]
    pub const fn with_publish(mut self) -> Self {
        self.should_publish = true;
        self
    }

    /// Set publish order
    #[must_use]
    pub const fn with_order(mut self, order: usize) -> Self {
        self.publish_order = order;
        self
    }

    /// Add dependency
    #[must_use]
    pub fn with_dependency(mut self, dep: String) -> Self {
        self.dependencies.push(dep);
        self
    }

    /// Mark as published
    pub const fn mark_published(&mut self) {
        self.published = true;
    }

    /// Get crates.io URL
    #[must_use]
    pub fn crates_io_url(&self) -> String {
        format!(
            "https://crates.io/crates/{}/{}",
            self.name, self.target_version
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_release_creation() {
        let version = SemanticVersion::parse("1.0.0").unwrap();
        let release = Release::new("test-crate".to_string(), version.clone(), Utc::now());
        assert_eq!(release.crate_name, "test-crate");
        assert_eq!(release.version(), &version);
        assert!(release.is_stable());
        assert!(!release.published);
    }

    #[test]
    fn test_release_plan() {
        let version = SemanticVersion::parse("1.0.0").unwrap();
        let mut plan = ReleasePlan::new(version.clone());
        assert_eq!(plan.status, ReleaseStatus::Planned);

        let crate_plan = CrateReleasePlan::new(
            "crate1".to_string(),
            SemanticVersion::parse("0.1.0").unwrap(),
            version,
        )
        .with_publish()
        .with_order(1);

        plan.add_crate(crate_plan);
        assert_eq!(plan.publishable_crates().len(), 1);
    }

    #[test]
    fn test_prerelease() {
        let version = SemanticVersion::parse("1.0.0-beta.1").unwrap();
        let release = Release::new("test-crate".to_string(), version, Utc::now());
        assert!(release.is_prerelease);
        assert!(!release.is_stable());
    }

    #[test]
    fn test_tag_name() {
        let version = SemanticVersion::parse("1.2.3").unwrap();
        let release = Release::new("test-crate".to_string(), version, Utc::now());
        assert_eq!(release.tag_name(), "v1.2.3");
    }
}