use aigitcommit::built_info::{PKG_NAME, PKG_VERSION};
use aigitcommit::cli::Cli;
use aigitcommit::git::message::GitMessage;
use aigitcommit::git::repository::Repository;
use aigitcommit::openai;
use aigitcommit::openai::OpenAI;
use arboard::Clipboard;
use async_openai::types::{
ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs,
};
use clap::Parser;
use std::error::Error;
use std::fs;
use std::io::Write;
use tracing::{Level, debug, error, info, trace};
use aigitcommit::utils::{
OutputFormat, check_env_variables, env, format_openai_error, save_to_file, should_signoff,
};
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn Error>> {
let cli = Cli::parse();
if cli.verbose {
tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.without_time()
.with_target(false)
.init();
trace!(
"verbose mode enabled, set the log level to TRACE. It will makes a little bit noise."
);
}
let model_name = env::get("OPENAI_MODEL_NAME", "gpt-5");
let client = openai::OpenAI::new();
if cli.check_env {
trace!("check env option is enabled");
debug!("model name: `{}`", &model_name);
check_env_variables();
return Ok(());
}
if cli.check_model {
trace!("check model option is enabled");
debug!("model name: `{}`", &model_name);
match client.check_model(&model_name).await {
Ok(()) => {
println!(
"the model name `{}` is available, {} is ready for use!",
model_name, PKG_NAME
);
}
Err(e) => {
return Err(format!("the model name `{model_name}` is not available: {e}").into());
}
}
return Ok(());
}
let repo_dir = fs::canonicalize(&cli.repo_path)?;
if !repo_dir.is_dir() {
return Err("the specified path is not a directory".into());
}
trace!("specified repository directory: {:?}", repo_dir);
let repository = Repository::new(repo_dir.to_str().unwrap_or("."))?;
let diffs = repository.get_diff()?;
debug!("got diff size is {}", diffs.len());
if diffs.is_empty() {
return Err("no diff found".into());
}
let logs = repository.get_logs(5)?;
debug!("got logs size is {}", logs.len());
if logs.is_empty() {
return Err("no commit logs found".into());
}
let content = OpenAI::prompt(&logs, &diffs)?;
let system_prompt = include_str!("../templates/system.txt");
let messages = vec![
ChatCompletionRequestSystemMessageArgs::default()
.content(system_prompt)
.build()?
.into(),
ChatCompletionRequestUserMessageArgs::default()
.content(content)
.build()?
.into(),
];
let result = match client.chat(&model_name, messages).await {
Ok(s) => s,
Err(e) => {
let message = format_openai_error(e);
return Err(message.into());
}
};
let (title, content) = result
.split_once("\n\n")
.ok_or("Invalid response format: expected title and content separated by double newline")?;
let need_signoff = should_signoff(&repository, cli.signoff);
let message: GitMessage = GitMessage::new(&repository, title, content, need_signoff)?;
let output_format = OutputFormat::detect(cli.json, cli.no_table);
output_format.write(&message)?;
if cli.copy_to_clipboard {
let mut clipboard = Clipboard::new()?;
clipboard.set_text(message.to_string())?;
writeln!(
std::io::stdout(),
"the commit message has been copied to clipboard."
)?;
}
if cli.commit {
trace!("commit option is enabled, will commit the changes directly to the repository");
if cli.yes || {
cliclack::intro(format!("{PKG_NAME} v{PKG_VERSION}"))?;
cliclack::confirm("Are you sure to commit with generated message below?").interact()?
} {
match repository.commit(&message) {
Ok(oid) => {
cliclack::note("Commit successful, last commit ID:", oid)?;
}
Err(e) => {
cliclack::note("Commit failed", e)?;
}
}
}
cliclack::outro("Bye~")?;
}
if !cli.save.is_empty() {
trace!("save option is enabled, will save the commit message to a file");
match save_to_file(&cli.save, &message) {
Ok(f) => {
info!("commit message saved to file: {:?}", f);
}
Err(e) => {
error!("failed to save commit message to file: {}", e);
}
}
}
Ok(())
}