cargo-governor 2.0.0

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Analyze service - business logic for version analysis

pub mod analysis;
pub mod bump;
pub mod risk;

// These are exported for use by tests and binary
pub use analysis::{analyze_commits, build_breaking_changes, build_features, build_fixes};
pub use bump::{calculate_confidence, generate_reasoning};
pub use risk::calculate_risk_assessment;

use crate::cli::{AnalyzeOpts, OutputFormat};
use crate::error::{CommandExitCode, Result};
use governor_core::{
    domain::version::{BumpType, SemanticVersion},
    traits::source_control::SourceControl,
};
use governor_git::GitAdapter;
use serde_json::json;

/// Service for analyzing commits and determining version bumps
pub struct AnalyzeService {
    workspace_path: String,
    opts: AnalyzeOpts,
}

impl AnalyzeService {
    pub const fn new(workspace_path: String, opts: AnalyzeOpts) -> Self {
        Self {
            workspace_path,
            opts,
        }
    }

    pub async fn execute(&self, _format: OutputFormat) -> Result<CommandExitCode> {
        let start_time = std::time::Instant::now();

        let metadata = self.parse_workspace_metadata()?;
        let commits = self.fetch_commits().await?;
        let analysis = analyze_commits(&commits);
        let bump_type = self.determine_bump(&analysis)?;
        let current = Self::parse_current_version(&metadata.current_version)?;
        let recommended = bump_type.apply_to(&current);
        let confidence = calculate_confidence(&analysis, bump_type);
        let reasoning = generate_reasoning(&analysis, bump_type, &current, &recommended);

        let breaking_changes = build_breaking_changes(analysis.breaking, &metadata.package_name);
        let features = build_features(analysis.features, &metadata.package_name);
        let fixes = build_fixes(analysis.fixes, &metadata.package_name);

        let risk_assessment = if self.opts.risk_analysis {
            Some(calculate_risk_assessment(
                &breaking_changes,
                &features,
                &fixes,
            ))
        } else {
            None
        };

        Self::print_response(
            metadata,
            &recommended,
            bump_type,
            confidence,
            reasoning,
            &commits,
            &breaking_changes,
            &features,
            &fixes,
            risk_assessment,
            start_time,
        );

        Ok(CommandExitCode::Success)
    }

    async fn fetch_commits(&self) -> Result<Vec<governor_core::domain::commit::Commit>> {
        let git = GitAdapter::open(governor_git::GitAdapterConfig {
            repository_path: Some(std::path::PathBuf::from(&self.workspace_path)),
            ..Default::default()
        })
        .map_err(|e| crate::error::Error::Config(format!("Failed to open git repository: {e}")))?;

        let tag_ref = self.opts.since.as_deref();
        git.get_commits_since(tag_ref)
            .await
            .map_err(|e| crate::error::Error::Git(format!("Failed to get commits: {e}")))
    }

    fn determine_bump(&self, analysis: &analysis::Analysis) -> Result<BumpType> {
        use bump::analyze_bump_type;

        if let Some(ref bump) = self.opts.bump {
            match bump.as_str() {
                "major" => Ok(BumpType::Major),
                "minor" => Ok(BumpType::Minor),
                "patch" => Ok(BumpType::Patch),
                "auto" => Ok(analyze_bump_type(analysis)),
                _ => Err(crate::error::Error::InvalidArguments(format!(
                    "Invalid bump strategy: {bump}"
                ))),
            }
        } else {
            Ok(analyze_bump_type(analysis))
        }
    }

    fn parse_current_version(version: &str) -> Result<SemanticVersion> {
        SemanticVersion::parse(version).map_err(|e| {
            crate::error::Error::Version(format!("Failed to parse current version: {e}"))
        })
    }

    fn parse_workspace_metadata(&self) -> Result<WorkspaceMetadata> {
        let manifest_path = std::path::PathBuf::from(&self.workspace_path).join("Cargo.toml");

        if !manifest_path.exists() {
            return Err(crate::error::Error::Config(format!(
                "Cargo.toml not found at {}",
                manifest_path.display()
            )));
        }

        let content = std::fs::read_to_string(&manifest_path)
            .map_err(|e| crate::error::Error::Io(format!("Failed to read Cargo.toml: {e}")))?;

        let value: toml::Value = toml::from_str(&content)
            .map_err(|e| crate::error::Error::Config(format!("Failed to parse Cargo.toml: {e}")))?;

        let package = value
            .get("workspace")
            .and_then(|w| w.get("package"))
            .or_else(|| value.get("package"));

        let name = package
            .and_then(|p| p.get("name"))
            .and_then(|n| n.as_str())
            .unwrap_or("unknown")
            .to_string();

        let version = package
            .and_then(|p| p.get("version"))
            .and_then(|v| v.as_str())
            .unwrap_or("0.0.0")
            .to_string();

        Ok(WorkspaceMetadata {
            package_name: name,
            current_version: version,
        })
    }

    fn print_response(
        metadata: WorkspaceMetadata,
        recommended: &SemanticVersion,
        bump_type: BumpType,
        confidence: f64,
        reasoning: String,
        commits: &[governor_core::domain::commit::Commit],
        breaking_changes: &[governor_core::domain::version::BreakingChange],
        features: &[governor_core::domain::version::Feature],
        fixes: &[governor_core::domain::version::Fix],
        risk_assessment: Option<risk::RiskAssessment>,
        start_time: std::time::Instant,
    ) {
        let response = json!({
            "success": true,
            "command": "analyze",
            "workspace": metadata.package_name,
            "result": {
                "current_version": metadata.current_version,
                "recommended_bump": serde_json::to_value(bump_type).unwrap(),
                "new_version": recommended.to_string(),
                "confidence": confidence,
                "reasoning": reasoning,
                "commits_analyzed": commits.len(),
                "breaking_changes": breaking_changes,
                "features": features,
                "fixes": fixes,
                "changelog_entries": features.len() + fixes.len(),
                "risk_assessment": risk_assessment,
            },
            "metrics": {
                "execution_time_ms": start_time.elapsed().as_millis(),
                "git_operations": 2,
                "api_calls": 0,
            }
        });

        println!("{}", serde_json::to_string_pretty(&response).unwrap());
    }
}

/// Workspace metadata
#[derive(Debug, Clone)]
struct WorkspaceMetadata {
    package_name: String,
    current_version: String,
}