cargo-governor 2.0.0

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

pub mod cargo;
pub mod changelog;
pub mod git;
pub mod version;

pub use cargo::{
    get_workspace_version, read_cargo_toml, update_workspace_version, write_cargo_toml,
};
pub use changelog::update_changelog;
pub use git::perform_git_operations;
pub use version::determine_new_version;

use crate::cli::{BumpOpts, OutputFormat};
use crate::error::{CommandExitCode, Result};
use governor_core::domain::version::SemanticVersion;
use serde_json::json;

/// Service for bumping versions
pub struct BumpService {
    workspace_path: String,
    opts: BumpOpts,
}

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

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

        let (_content, mut value) = read_cargo_toml(&self.workspace_path)?;

        let current_version_str = get_workspace_version(&value)?;
        let current = SemanticVersion::parse(&current_version_str).map_err(|e| {
            crate::error::Error::Version(format!("Failed to parse current version: {e}"))
        })?;

        let new_version = self.determine_new_version(&current).await?;
        let version_str = new_version.to_string();

        update_workspace_version(&mut value, &version_str)?;

        let dry_run = Self::is_dry_run();

        // Check if Cargo.lock exists (needed before write operations)
        let cargo_lock_path = std::path::PathBuf::from(&self.workspace_path).join("Cargo.lock");
        let has_cargo_lock = cargo_lock_path.exists();

        if !dry_run {
            write_cargo_toml(&self.workspace_path, &value)?;

            // Update Cargo.lock to reflect new version in workspace crates
            // This ensures Cargo.lock is in sync with Cargo.toml before commit
            if has_cargo_lock {
                let lock_update_result = std::process::Command::new("cargo")
                    .args(["generate-lockfile"])
                    .current_dir(&self.workspace_path)
                    .output();

                if let Err(e) = lock_update_result {
                    eprintln!("Warning: Failed to update Cargo.lock: {e}");
                }
            }
        }

        let changelog_path = std::path::PathBuf::from(&self.workspace_path).join("CHANGELOG.md");
        let changelog_updated = if self.opts.no_changelog {
            false
        } else {
            update_changelog(&changelog_path, &version_str, &self.workspace_path, dry_run)?
        };

        let (commit_hash, tag_created) = perform_git_operations(
            &self.workspace_path,
            &new_version,
            &version_str,
            &std::path::PathBuf::from(&self.workspace_path).join("Cargo.toml"),
            &changelog_path,
            &cargo_lock_path,
            self.opts.no_commit,
            self.opts.no_tag,
            self.opts.commit_template.as_deref(),
            self.opts.tag_template.as_deref(),
            dry_run,
        )
        .await?;

        let response = json!({
            "success": true,
            "command": "bump",
            "workspace": self.workspace_path,
            "result": {
                "previous_version": current_version_str,
                "new_version": version_str,
                "files_modified": if dry_run { vec![] } else {
                    let mut files = vec!["Cargo.toml".to_string()];
                    if changelog_updated {
                        files.push("CHANGELOG.md".to_string());
                    }
                    if has_cargo_lock {
                        files.push("Cargo.lock".to_string());
                    }
                    files
                },
                "commit": {
                    "created": commit_hash.is_some(),
                    "hash": commit_hash,
                },
                "tag": {
                    "created": tag_created,
                },
                "dry_run": dry_run,
            },
            "metrics": {
                "execution_time_ms": start_time.elapsed().as_millis(),
                "git_operations": if dry_run { 0 } else { 2 },
                "api_calls": 0,
            }
        });

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

    async fn determine_new_version(&self, current: &SemanticVersion) -> Result<SemanticVersion> {
        if self.opts.version.is_some() {
            let ver_str = self.opts.version.as_ref().unwrap();
            return SemanticVersion::parse(ver_str).map_err(|e| {
                crate::error::Error::Version(format!("Failed to parse target version: {e}"))
            });
        }

        determine_new_version(&self.workspace_path, None, current).await
    }

    fn is_dry_run() -> bool {
        std::env::var("CARGO_GOVERNOR_DRY_RUN")
            .or_else(|_| std::env::var("DRY_RUN"))
            .is_ok()
    }
}