commit-wizard 0.0.2

A lightweight CLI assistant for Conventional Commits, semantic versioning, and changelog automation.
Documentation
use std::time::Instant;

use crate::{
    core::{Context, CoreResult},
    engine::{
        capabilities::commit::{check::validate_commits, push::evaluate_push},
        error::ErrorCode,
        models::policy::commit::CommitModel,
    },
};

pub fn run(
    ctx: &Context,
    from: Option<String>,
    to: String,
    remote: String,
    branch: Option<String>,
    allow_empty: bool,
) -> CoreResult<()> {
    let ui = ctx.ui();
    let start = Instant::now();

    let resolved_config = ctx.config().ok_or_else(|| {
        ErrorCode::ConfigUnreadable
            .error()
            .with_context("context", "Config not resolved")
    })?;
    let policy = CommitModel::from_config(resolved_config);
    let push_policy = ctx.policy();
    let git = ctx.git();

    let current_branch = git.current_branch()?;
    let push_branch = branch.unwrap_or_else(|| current_branch.clone());

    let raw_commits = git.list_commits(from.as_deref(), &to)?;
    let checked = validate_commits(raw_commits, &policy);
    let report = evaluate_push(push_policy, &push_branch, checked.commits);

    let duration_ms = start.elapsed().as_millis() as u64;

    let mut content = ctx
        .ui()
        .new_output_content()
        .title("Push Validation")
        .subtitle("Validate push against active rules before pushing")
        .data("current_branch", current_branch.clone())
        .data("push_branch", push_branch.clone())
        .data("remote", remote.clone())
        .data("range_to", to.clone())
        .data("protected_branch", report.protected_branch.to_string())
        .data("total_checked", report.total_checked.to_string())
        .data("invalid_count", report.invalid_count.to_string())
        .data("blocked", report.blocked.to_string())
        .data("allow_empty", allow_empty.to_string());

    if let Some(from_ref) = from.clone() {
        content = content.data("range_from", from_ref);
    }

    if !report.block_reasons.is_empty() {
        content = content.section(
            "Block Reasons",
            report.block_reasons.join("\n"),
            "sh".to_string(),
        );
    }

    let invalid_lines = report
        .commits
        .iter()
        .filter(|c| !c.valid)
        .map(|c| format!("{} {}", c.hash, c.summary))
        .collect::<Vec<_>>();

    if !invalid_lines.is_empty() {
        content = content.section(
            "Invalid Commits",
            invalid_lines.join("\n"),
            "sh".to_string(),
        );
    }

    let meta = ui
        .new_output_meta()
        .with_duration_ms(duration_ms)
        .with_timestamp(chrono::Utc::now().to_string())
        .with_command("push".to_string())
        .with_dry_run(ctx.dry_run());

    if report.blocked {
        return ui.print_with_meta(&content, Some(&meta), true);
    }

    if ctx.dry_run() {
        content = content
            .title("Push Preview")
            .subtitle("Dry run: no push was performed");
        return ui.print_with_meta(&content, Some(&meta), true);
    }

    git.push_branch(&remote, &push_branch)?;

    content = content
        .title("Push Completed")
        .subtitle("Push completed successfully");

    ui.print_with_meta(&content, Some(&meta), true)
}