cargo-governor 2.0.3

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

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

/// Service for syncing owner configuration to crates.io
pub struct SyncService {
    config: CargoConfig,
    client: OwnersClient,
}

impl SyncService {
    pub fn new(config: CargoConfig) -> Result<Self> {
        // Try to create client with cargo token for authentication
        let client = OwnersClient::try_new().unwrap_or_else(|_| OwnersClient::new());
        Ok(Self { config, client })
    }

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

        let mut partial_success = false;

        for pkg in packages {
            let result = self.sync_package(pkg, dry_run).await?;
            partial_success = partial_success || matches!(result, CommandExitCode::PartialSuccess);
        }

        if partial_success {
            Ok(CommandExitCode::PartialSuccess)
        } else {
            Ok(CommandExitCode::Success)
        }
    }

    async fn sync_package(
        &self,
        pkg: &crate::meta::PackageConfig,
        dry_run: bool,
    ) -> Result<CommandExitCode> {
        println!("Syncing 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(CommandExitCode::Success);
        }

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

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

        match self.client.sync(&pkg.name, &resolved.owners, dry_run).await {
            Ok(governor_owners::OwnersSyncResult::AlreadyInSync) => {
                println!("  Already synchronized.");
                println!();
            }
            Ok(governor_owners::OwnersSyncResult::DryRun(diff)) => {
                println!("  Dry run - changes that would be applied:");
                println!("{diff}");
                println!();
            }
            Ok(governor_owners::OwnersSyncResult::Success(diff)) => {
                Self::display_sync_success(&diff);
            }
            Ok(governor_owners::OwnersSyncResult::Partial { diff, errors }) => {
                return Ok(Self::display_sync_partial(&diff, &errors));
            }
            Err(e) => {
                eprintln!("  Error: {e}");
                return Ok(CommandExitCode::RegistryError);
            }
        }

        Ok(CommandExitCode::Success)
    }

    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)
    }

    fn display_sync_success(diff: &governor_owners::OwnersDiff) {
        println!("  Applying changes:");
        println!("{diff}");

        for owner in &diff.to_add {
            println!("  Added {owner}");
        }

        for owner in &diff.to_remove {
            println!("  Removed {owner}");
        }

        println!();
    }

    fn display_sync_partial(
        diff: &governor_owners::OwnersDiff,
        errors: &[(String, String)],
    ) -> CommandExitCode {
        println!("  Partial success:");
        println!("{diff}");

        for (owner, error) in errors {
            eprintln!("    Failed to {owner}: {error}");
        }

        println!();
        CommandExitCode::PartialSuccess
    }
}