cargo-governor 2.0.3

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Git operations for bump service

use crate::error::Result;
use governor_core::domain::version::SemanticVersion;
use governor_core::traits::source_control::{
    SourceControl, format_commit_message, format_tag_name,
};
use governor_git::GitAdapter;
use std::path::{Path, PathBuf};

/// Perform git operations after version bump
pub async fn perform_git_operations(
    workspace_path: &str,
    new_version: &SemanticVersion,
    version_str: &str,
    cargo_toml_path: &Path,
    changelog_path: &Path,
    cargo_lock_path: &Path,
    no_commit: bool,
    no_tag: bool,
    commit_template: Option<&str>,
    tag_template: Option<&str>,
    dry_run: bool,
) -> Result<(Option<String>, bool)> {
    let config = governor_git::GitAdapterConfig {
        repository_path: Some(PathBuf::from(workspace_path)),
        ..Default::default()
    };

    let git = GitAdapter::open(config)
        .map_err(|e| crate::error::Error::Config(format!("Failed to open git repository: {e}")))?;

    let commit_template = commit_template.unwrap_or("chore(release): bump version to {{version}}");
    let commit_message = format_commit_message(commit_template, new_version);

    let tag_template = tag_template.unwrap_or("v{{version}}");
    let tag_name = format_tag_name(tag_template, new_version);

    let mut commit_hash = None;
    let mut tag_created = false;

    if !dry_run {
        // Convert paths to strings relative to repository root
        let repo_root = git.repo_root();
        let mut files_to_commit = Vec::new();

        // Helper to normalize path (remove leading ./)
        let normalize_path = |p: &Path| -> String {
            p.strip_prefix(&repo_root)
                .unwrap_or(p)
                .to_string_lossy()
                .to_string()
                .trim_start_matches("./")
                .to_string()
        };

        // Add Cargo.toml - use relative path from repo root
        files_to_commit.push(normalize_path(cargo_toml_path));

        // Add CHANGELOG.md if it exists
        if changelog_path.exists() {
            files_to_commit.push(normalize_path(changelog_path));
        }

        // Add Cargo.lock if it exists
        if cargo_lock_path.exists() {
            files_to_commit.push("Cargo.lock".to_string());
        }

        if !no_commit {
            let commit_result = git.commit(&commit_message, &files_to_commit).await;
            if let Ok(hash) = commit_result {
                commit_hash = Some(hash);
            } else {
                let _ = git.stage_files(&files_to_commit).await;
                if let Ok(hash) = git.commit(&commit_message, &files_to_commit).await {
                    commit_hash = Some(hash);
                }
            }
        }

        if !no_tag
            && git
                .create_tag(&tag_name, &format!("Release {version_str}"))
                .await
                .is_ok()
        {
            tag_created = true;
        }
    }

    Ok((commit_hash, tag_created))
}