mod ai;
mod config;
mod git;
mod ui;
use colored::*;
use git2::Repository;
use std::env;
use std::thread;
fn main() {
let verbose = parse_args();
let repo = Repository::discover(".").unwrap_or_else(|_| {
eprintln!("{}", "Error opening git repo •◠•".red());
std::process::exit(1);
});
let repo_name = repo
.path()
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("no name");
ui::print_repo_name(repo_name);
git::stage_all();
let (insertions, deletions, files) = git::get_stats(&repo).unwrap_or_else(|_| {
eprintln!("{}", "Error reading git info •◠•".red());
std::process::exit(1);
});
if files.is_empty() {
println!("{}", "No changes to commit •◡•".yellow());
std::process::exit(0);
}
ui::print_changes(&files, insertions, deletions);
let api_key = config::get_api_key().unwrap_or_else(|e| {
eprintln!("{}", format!("Error: {}", e).red());
std::process::exit(1);
});
let model = config::get_model().unwrap_or_else(|e| {
eprintln!("{}", format!("Error: {}", e).red());
std::process::exit(1);
});
let semantic_types = config::get_semantic_types().unwrap_or_else(|e| {
eprintln!("{}", format!("Error: {}", e).red());
std::process::exit(1);
});
let diff = git::build_diff_context(&files, insertions, deletions);
let ai_handle = thread::spawn(move || {
ai::generate_commit_info(&diff, true, &api_key, &model, &semantic_types)
});
let create_new_branch =
ui::prompt_input("\nCreate new branch? (y/N): ").eq_ignore_ascii_case("y");
let spinner = ui::Spinner::start("Generating commit message...");
let result = ai_handle
.join()
.unwrap_or_else(|_| Err("AI thread panicked".to_string()));
spinner.stop();
let generated = result.unwrap_or_else(|e| {
eprintln!("{}", format!("AI generation failed: {}", e).red());
eprintln!("{}", "Falling back to manual input.".yellow());
ai::GeneratedCommitInfo {
commit_message: String::new(),
branch_name: Some(String::new()),
stats: ai::QueryStats {
total_time_ms: None,
input_tokens: None,
output_tokens: None,
},
}
});
if verbose {
ui::print_ai_query_stats(&generated.stats);
}
let final_message = ui::editable_prompt("Commit: ", &generated.commit_message.trim())
.trim()
.to_string();
let final_branch = if create_new_branch {
Some(
ui::editable_prompt(
"Branch: ",
&generated.branch_name.unwrap_or_default().trim(),
)
.trim()
.to_string(),
)
} else {
None
};
if let Some(ref branch) = final_branch {
git::create_branch(&repo, branch);
}
git::stage_and_commit(&final_message);
let push_input = ui::prompt_input("Push to remote? (Y/n): ");
if !push_input.eq_ignore_ascii_case("n") {
git::push(&repo, final_branch.is_some());
}
}
fn parse_args() -> bool {
let mut verbose = false;
for arg in env::args().skip(1) {
match arg.as_str() {
"-v" | "--verbose" => verbose = true,
"-h" | "--help" => {
println!("Usage: qc [-v|--verbose]");
println!(" -v, --verbose Show AI total time and tokens");
std::process::exit(0);
}
_ => {
eprintln!("{}", format!("Unknown argument: {}", arg).red());
eprintln!("Usage: qc [-v|--verbose]");
std::process::exit(2);
}
}
}
verbose
}