use std::path::PathBuf;
use clap::Parser;
use git_commit_sage::{
AiClient, GitRepo, Config, Error, Result, AVAILABLE_MODELS,
is_conventional_commit,
};
use tracing::{info, warn};
use std::io::{self, Write};
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
path: Option<PathBuf>,
#[arg(short = 'k', long, env = "TOGETHER_API_KEY")]
api_key: Option<String>,
#[arg(short, long)]
model: Option<String>,
#[arg(short = 't', long, default_value = "0.3")]
temperature: f32,
#[arg(long, default_value = "100")]
max_tokens: u32,
#[arg(short, long)]
untracked: bool,
#[arg(short, long)]
show_diff: bool,
#[arg(short = 'a', long)]
auto_commit: bool,
#[arg(long)]
no_verify: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(short = 'f', long)]
config: Option<PathBuf>,
#[arg(short, long)]
list_models: bool,
#[arg(short, long)]
debug: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::dotenv().ok();
let args = Args::parse();
if args.list_models {
println!("Available models:");
for (model, description) in AVAILABLE_MODELS {
println!(" {} - {}", model, description);
}
return Ok(());
}
setup_logging(args.debug);
let mut config = if let Some(config_path) = args.config {
info!("Loading configuration from {}", config_path.display());
let config_str = std::fs::read_to_string(config_path)?;
toml::from_str(&config_str)?
} else {
Config::default()
};
if let Some(path) = args.path {
config.git.repo_path = path;
}
if let Some(model) = args.model {
config.ai.model = model;
}
config.ai.temperature = args.temperature;
config.ai.max_tokens = args.max_tokens;
config.git.include_untracked = args.untracked;
config.git.show_diff = args.show_diff;
config.commit.auto_commit = args.auto_commit;
config.commit.verify_format = !args.no_verify;
config.commit.require_confirmation = !args.yes;
info!("Opening git repository at {}", config.git.repo_path.display());
let repo = GitRepo::new(config.git.clone())?;
if !repo.has_changes()? {
warn!("No changes to commit!");
return Err(Error::NoChanges);
}
let api_key = args.api_key
.or_else(|| std::env::var("TOGETHER_API_KEY").ok())
.ok_or_else(|| Error::NoApiKey)?;
let ai_client = AiClient::new(api_key, config.ai.clone());
info!("Getting git diff");
let diff = repo.get_diff()?;
if config.git.show_diff {
println!("\nChanges to be committed:\n{}", diff);
}
info!("Generating commit message using model {}", config.ai.model);
let commit_message = ai_client.generate_commit_message(&diff).await?;
if config.commit.verify_format && !is_conventional_commit(&commit_message) {
return Err(Error::CommitMessageGeneration(
"Generated message does not follow conventional commit format".to_string(),
));
}
println!("\nSuggested commit message:\n{}", commit_message);
if config.commit.auto_commit {
if config.commit.require_confirmation {
print!("\nDo you want to commit with this message? [y/N] ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
println!("Commit aborted.");
return Ok(());
}
}
info!("Auto-committing changes");
repo.commit(&commit_message)?;
println!("Changes committed successfully!");
}
Ok(())
}
fn setup_logging(debug: bool) {
let filter = if debug { "debug" } else { "info" };
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false)
.with_file(false)
.with_line_number(false)
.init();
}