use anyhow::{Context, Result};
use auto_commit::{
api::create_client_for_provider,
cli::Cli,
config::Config,
formatter::{CommitData, CommitFormatter},
git::GitOperations,
};
use log::info;
use question::{Answer, Question};
use spinners::{Spinner, Spinners};
use std::{env, io::Write, path::Path, process::Command};
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse_args();
env_logger::Builder::new()
.filter_level(cli.verbose.log_level_filter())
.init();
info!("Starting auto-commit");
if !GitOperations::has_staged_changes()? {
eprintln!("No staged changes found. Stage your changes with `git add` first.");
std::process::exit(1);
}
let config = load_config()?;
let diff = GitOperations::get_staged_diff()?;
if diff.is_empty() {
eprintln!("No changes detected in staged files.");
std::process::exit(1);
}
let provider_name = config.provider.to_string();
let mut spinner = Spinner::new(
Spinners::Dots,
format!("Generating commit message using {}...", provider_name),
);
let client = create_client_for_provider(config.provider, config.api_key.clone());
let (title, description) = client
.generate_commit_message(&diff, Some(&config.gitmessage_template))
.await
.context("Failed to generate commit message")?;
spinner.stop_with_message(format!("✓ Commit message generated ({})", provider_name));
let raw_message = format!("{}\n\n{}", title, description);
let commit_data = CommitData::from_message(&raw_message);
let formatter = CommitFormatter::new(cli.format);
let formatted_message = formatter.format_message(commit_data)?;
if cli.dry_run {
println!("\n{}", formatted_message);
return Ok(());
}
let final_message = if cli.review {
edit_message(&formatted_message)?
} else {
formatted_message
};
if !cli.force {
println!("\nProposed commit message:\n{}", final_message);
let answer = Question::new("Do you want to create this commit?")
.default(Answer::YES)
.show_defaults()
.confirm();
if answer != Answer::YES {
println!("Commit cancelled.");
return Ok(());
}
}
GitOperations::create_commit(&final_message)?;
println!("✓ Commit created successfully!");
Ok(())
}
fn load_config() -> Result<Config> {
let env_path = Path::new("src/.env");
if env_path.exists() {
if let Ok(config) = Config::from_env_file(env_path) {
return Ok(config);
}
}
Config::from_env()
}
fn edit_message(message: &str) -> Result<String> {
let temp_file = tempfile::NamedTempFile::new()?;
temp_file.as_file().write_all(message.as_bytes())?;
let editor = env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
let status = Command::new(&editor)
.arg(temp_file.path())
.status()
.context("Failed to open editor")?;
if !status.success() {
anyhow::bail!("Editor exited with non-zero status");
}
let edited = std::fs::read_to_string(temp_file.path())?;
Ok(edited.trim().to_string())
}