cargo-governor 2.0.3

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Check owners service

use crate::error::{CommandExitCode, Result};
use crate::meta::CargoConfig;
use governor_owners::{OwnersClient, OwnersDiff, resolve_owners, validate_not_empty};

/// Service for checking owner configuration against crates.io
pub struct CheckService {
    config: CargoConfig,
    client: OwnersClient,
}

impl CheckService {
    pub const fn new(config: CargoConfig) -> Self {
        Self {
            config,
            client: OwnersClient::new(),
        }
    }

    pub async fn execute(&self, all: bool) -> Result<CommandExitCode> {
        let packages: Vec<_> = if all {
            self.config.packages.iter().collect()
        } else {
            self.config.current_package().into_iter().collect()
        };

        let mut has_drift = false;

        for pkg in packages {
            let drift = self.check_package(pkg).await?;
            has_drift = has_drift || drift;
        }

        if has_drift {
            Ok(CommandExitCode::DriftDetected)
        } else {
            Ok(CommandExitCode::Success)
        }
    }

    async fn check_package(&self, pkg: &crate::meta::PackageConfig) -> Result<bool> {
        println!("Checking owners for '{}'...", pkg.name);

        let workspace_config = self.config.workspace.as_ref();
        let package_config = pkg.owners.as_ref();

        if workspace_config.is_none() && package_config.is_none() {
            println!("  (no owners configured, skipping)");
            println!();
            return Ok(false);
        }

        let resolved = Self::resolve_owners(pkg, workspace_config);

        if let Err(e) = validate_not_empty(&resolved.owners, &pkg.name) {
            eprintln!("  Error: {e}");
            return Ok(true);
        }

        let current_owners = self.fetch_current_owners(&pkg.name).await;

        let current = match current_owners {
            Ok(owners) => owners,
            Err(e) => {
                eprintln!("  Error: {e}");
                return Ok(true);
            }
        };

        let diff = OwnersDiff::calculate(&current, &resolved.owners);

        if diff.is_empty() {
            println!("  OK: Owners match configuration.");
        } else {
            println!("  Drift detected:");
            println!("{diff}");
            println!("  Run 'cargo governor owners sync' to apply changes.");
        }

        println!();
        Ok(!diff.is_empty())
    }

    fn resolve_owners(
        pkg: &crate::meta::PackageConfig,
        workspace_config: Option<&governor_owners::WorkspaceOwnersConfig>,
    ) -> governor_owners::ResolvedOwners {
        let workspace = workspace_config.cloned().unwrap_or_default();
        let package = pkg.owners.clone().unwrap_or_default();
        resolve_owners(&workspace, &package)
    }

    async fn fetch_current_owners(&self, crate_name: &str) -> Result<Vec<String>> {
        self.client
            .list_owners(crate_name)
            .await
            .map_err(|e| crate::error::Error::Owners(e.to_string()))
    }
}