cargo-governor 2.0.0

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

use crate::cli::{OutputFormat, StatusOpts};
use crate::error::{CommandExitCode, Result};
use cargo_metadata::MetadataCommand;
use governor_core::{
    domain::{commit::Commit, dependency::DependencyGraph, workspace::WorkingTreeStatus},
    traits::source_control::SourceControl,
};
use governor_git::GitAdapter;
use serde_json::json;
use std::path::PathBuf;

/// Service for showing release status
pub struct StatusService {
    workspace_path: String,
    opts: StatusOpts,
}

impl StatusService {
    pub const fn new(workspace_path: String, opts: StatusOpts) -> 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_metadata()?;
        let workspace_packages = self.get_workspace_packages(&metadata);
        let (current_branch, last_tag, commits_since_tag, working_tree_status) =
            self.fetch_git_status().await?;

        let workspace_name = self.get_workspace_name(&metadata);
        let current_version = self.get_current_version(&workspace_packages);
        let dep_graph = self.build_dependency_graph(&metadata, &workspace_packages);
        let publish_order = dep_graph.publish_order().ok().unwrap_or_default();
        let crates = self.build_crates_list(&workspace_packages, &commits_since_tag);

        let response = json!({
            "success": true,
            "command": "status",
            "workspace": workspace_name,
            "result": {
                "workspace": {
                    "name": workspace_name,
                    "current_version": current_version,
                    "branch": current_branch,
                    "last_tag": last_tag,
                    "commits_since_tag": commits_since_tag.len(),
                },
                "git": {
                    "clean": !working_tree_status.has_changes,
                    "modified": working_tree_status.modified,
                    "added": working_tree_status.added,
                    "deleted": working_tree_status.deleted,
                    "untracked": working_tree_status.untracked,
                },
                "crates": crates,
                "dependency_graph": if self.opts.show_deps {
                    Some(json!({
                        "crates": publish_order.len(),
                        "publish_order": publish_order,
                        "has_cycles": dep_graph.has_cycles(),
                    }))
                } else {
                    None
                },
            },
            "metrics": {
                "execution_time_ms": start_time.elapsed().as_millis(),
                "git_operations": 3,
                "api_calls": 0,
            }
        });

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

    fn parse_metadata(&self) -> Result<cargo_metadata::Metadata> {
        MetadataCommand::new()
            .current_dir(PathBuf::from(&self.workspace_path))
            .no_deps()
            .exec()
            .map_err(|e| crate::error::Error::Config(format!("Failed to read cargo metadata: {e}")))
    }

    fn get_workspace_packages(
        &self,
        metadata: &cargo_metadata::Metadata,
    ) -> Vec<cargo_metadata::Package> {
        let workspace_members: Vec<_> = metadata
            .workspace_members
            .iter()
            .map(std::string::ToString::to_string)
            .collect();

        metadata
            .packages
            .clone()
            .into_iter()
            .filter(|p| workspace_members.contains(&p.id.to_string()))
            .collect()
    }

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

        let current_branch = git
            .get_current_branch()
            .await
            .map_err(|e| crate::error::Error::Git(format!("Failed to get current branch: {e}")))?;

        let last_tag = git
            .get_last_tag(Some("v*"))
            .await
            .map_err(|e| crate::error::Error::Git(format!("Failed to get last tag: {e}")))?;

        let commits_since_tag = if let Some(ref tag) = last_tag {
            git.get_commits_since(Some(tag))
                .await
                .map_err(|e| crate::error::Error::Git(format!("Failed to get commits: {e}")))?
        } else {
            Vec::new()
        };

        let working_tree_status = git.get_working_tree_status().await.map_err(|e| {
            crate::error::Error::Git(format!("Failed to get working tree status: {e}"))
        })?;

        Ok((
            current_branch,
            last_tag,
            commits_since_tag,
            working_tree_status,
        ))
    }

    fn get_workspace_name(&self, metadata: &cargo_metadata::Metadata) -> String {
        metadata
            .workspace_root
            .file_name()
            .map_or_else(|| "workspace".to_string(), std::string::ToString::to_string)
    }

    fn get_current_version(&self, workspace_packages: &[cargo_metadata::Package]) -> String {
        workspace_packages
            .first()
            .map_or_else(|| "0.0.0".to_string(), |p| p.version.to_string())
    }

    fn build_dependency_graph(
        &self,
        metadata: &cargo_metadata::Metadata,
        workspace_packages: &[cargo_metadata::Package],
    ) -> DependencyGraph {
        let mut dep_graph = DependencyGraph::new();
        let workspace_root = &metadata.workspace_root;

        for pkg in workspace_packages {
            for dep in &pkg.dependencies {
                if let Some(ref dep_path) = dep.path
                    && dep_path.starts_with(workspace_root)
                {
                    dep_graph.add(governor_core::domain::dependency::WorkspaceDependency::new(
                        pkg.name.clone(),
                        dep.name.clone(),
                        dep.req.to_string(),
                    ));
                }
            }
        }
        dep_graph
    }

    fn build_crates_list(
        &self,
        workspace_packages: &[cargo_metadata::Package],
        commits_since_tag: &[Commit],
    ) -> Vec<serde_json::Value> {
        if self.opts.all {
            workspace_packages
                .iter()
                .map(|p| {
                    let version = p.version.to_string();
                    json!({
                        "name": p.name.as_str(),
                        "version": version,
                        "published": false,
                        "status": "unpublished",
                    })
                })
                .collect()
        } else {
            let changed_crates: std::collections::HashSet<String> = commits_since_tag
                .iter()
                .filter_map(|c| c.scope.clone())
                .collect();

            if changed_crates.is_empty() {
                vec![]
            } else {
                changed_crates
                    .iter()
                    .map(|name| {
                        json!({
                            "name": name,
                            "status": "changed",
                        })
                    })
                    .collect()
            }
        }
    }
}