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