aicm 0.1.0

AI-assisted Git commit message generator for staged changes
use std::{env, io, process::Command};

const COMMIT_MESSAGE_PROMPT: &str = r#"
Generate a single Git commit message for the provided staged changes.

Use Conventional Commits.
Return only the final commit message.
No markdown. No quotes. No commentary.

Subject must be <= 72 chars.
Use imperative mood.
Do not end with a period.
"#;

const CLI_HELP: &str = r#"
aicm {version}
AI-assisted Git commit message generator

Usage:
  aicm <command>

Commands:
    help       Print this help message

Examples:
  aicm
  aicm help
"#;

fn main() -> Result<(), String> {
    let args: Vec<String> = env::args().collect();

    if args.len() > 1 && args[1] == "help" {
        help();

        return Ok(());
    }

    let message = process_message()?;

    println!(
        "Commit message generated:\n{}, commit? [y/N]",
        message.trim_end()
    );

    let mut option = String::new();

    io::stdin()
        .read_line(&mut option)
        .map_err(|err| format!("Couldn't read user input: {err}"))?;

    if option.trim_end() == "y" {
        let commit_output = commit(message)?;
        println!("{commit_output}");

        println!("Committed successfully. Push now? [y/N]");

        option.clear();

        io::stdin()
            .read_line(&mut option)
            .map_err(|err| format!("Couldn't read user input: {err}"))?;

        if option.trim_end() == "y" {
            let push_output = push()?;
            println!("{push_output}");
            return Ok(());
        }
    }

    return Ok(());
}

fn process_message() -> Result<String, String> {
    let diff = Command::new("git")
        .args(["diff", "--cached"])
        .output()
        .map_err(|err| format!("Couldn't get git diff: {err}"))?;

    if !diff.status.success() {
        return Err(String::from_utf8_lossy(&diff.stderr).to_string());
    }

    let diff_output = String::from_utf8_lossy(&diff.stdout);

    if diff_output.trim().is_empty() {
        return Err("No staged changes found".to_string());
    }

    let prompt = format!("{COMMIT_MESSAGE_PROMPT}\n\n{diff_output}");

    let oc_cmd = Command::new("opencode")
        .args(["run", &prompt])
        .output()
        .map_err(|err| format!("Couldn't run opencode: {err}"))?;

    if !oc_cmd.status.success() {
        return Err(String::from_utf8_lossy(&oc_cmd.stderr).to_string());
    }

    let oc_output = String::from_utf8_lossy(&oc_cmd.stdout).trim().to_string();

    if oc_output.is_empty() {
        return Err("opencode returned an empty commit message".to_string());
    }

    Ok(oc_output)
}

fn commit(message: String) -> Result<String, String> {
    let commit = Command::new("git")
        .args(["commit", "-m", &message])
        .output()
        .map_err(|err| format!("Couldn't commit: {err}"))?;

    if !commit.status.success() {
        return Err(String::from_utf8_lossy(&commit.stderr).to_string());
    }

    let commit_output = String::from_utf8_lossy(&commit.stdout).trim().to_string();

    Ok(commit_output)
}

fn push() -> Result<String, String> {
    let branch_cmd = Command::new("git")
        .args(["branch", "--show-current"])
        .output()
        .map_err(|err| format!("Couldn't get the branch: {err}"))?;

    if !branch_cmd.status.success() {
        return Err(String::from_utf8_lossy(&branch_cmd.stderr).to_string());
    }

    let branch_output = String::from_utf8_lossy(&branch_cmd.stdout)
        .trim()
        .to_string();

    let push_cmd = Command::new("git")
        .args(["push", "origin", &branch_output])
        .output()
        .map_err(|err| format!("Couldn't push the commit: {err}"))?;

    if !push_cmd.status.success() {
        return Err(String::from_utf8_lossy(&push_cmd.stderr).to_string());
    }

    let push_output = String::from_utf8_lossy(&push_cmd.stdout).trim().to_string();

    if push_output.is_empty() {
        return Ok(String::from_utf8_lossy(&push_cmd.stderr).trim().to_string());
    }

    Ok(push_output)
}

fn help() {
    println!(
        "{}",
        CLI_HELP.replace("{version}", env!("CARGO_PKG_VERSION"))
    );
}