turbocommit 0.5.2

A CLI tool to create commit messages with gpt-3.5-turbo
use std::{env, process};

use colored::Colorize;

use inquire::validator::Validation;
use inquire::{Confirm, CustomUserError};

use config::Config;
use openai::Message;

mod cli;
mod config;
mod git;
mod openai;

const MODEL: &str = "gpt-3.5-turbo";

fn main() {
    let options = cli::Options::new(env::args());
    let mut config = Config::load();
    match config.save() {
        Ok(_) => (),
        Err(err) => {
            println!("{}", format!("Unable to write to config: {}", err).red());
            process::exit(1);
        }
    }
    config.overwrite(&options);

    let Ok(api_key) = env::var("OPENAI_API_KEY") else {
        println!("{} {}", "OPENAI_API_KEY not set.".red(), "Refer to step 3 here: https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety".bright_black());
        process::exit(1);
    };

    if !git::is_repo() {
        println!(
            "{} {}",
            "Not a git repository.".red(),
            "Please run this command in a git repository.".bright_black()
        );
        process::exit(1);
    }

    println!();
    let full_diff = git::diff();

    if full_diff.trim().is_empty() {
        println!(
            "{} {}",
            "No staged files.".red(),
            "Please stage the files you want to commit.".bright_black()
        );
        process::exit(1);
    }

    let system_len = openai::count_token(&config.default_system_msg).unwrap_or(0);
    let extra_len = if options.msg.as_ref().is_some() && !options.msg.as_ref().unwrap().is_empty() {
        openai::count_token(options.msg.as_ref().unwrap()).unwrap_or(0)
    } else {
        0
    };

    let diff = match git::check_diff(&full_diff, system_len, extra_len) {
        Ok(diff) => diff,
        Err(e) => {
            println!("{e}");
            process::exit(1);
        }
    };

    let mut messages = vec![
        Message::system(config.default_system_msg),
        Message::user(diff),
    ];

    if !options.msg.as_ref().unwrap_or(&String::from("")).is_empty() {
        messages.push(Message::user(options.msg.unwrap_or(String::from(""))));
    }

    let req = openai::Request::new(
        String::from(MODEL),
        messages,
        options.n.unwrap_or(1),
        options.t.unwrap_or(1.0),
        options.f.unwrap_or(0.0),
    );

    let json = match serde_json::to_string(&req) {
        Ok(json) => json,
        Err(e) => {
            println!("{e}");
            process::exit(1);
        }
    };

    let client = reqwest::blocking::Client::new();

    println!("{}", "Asking AI...".bright_black());

    let start = std::time::Instant::now();
    let response = client
        .post("https://api.openai.com/v1/chat/completions")
        .header("Content-Type", "application/json")
        .header("Authorization", format!("Bearer {api_key}"))
        .body(json)
        .send();

    match response {
        Ok(response) => {
            if response.status() == reqwest::StatusCode::OK {
                let body = match response.text() {
                    Ok(body) => body,
                    Err(e) => {
                        println!("{e}");
                        process::exit(1);
                    }
                };
                let resp = match serde_json::from_str::<openai::Response>(&body) {
                    Ok(resp) => resp,
                    Err(e) => {
                        println!("error parsing response: {e}\n {body:?}");
                        process::exit(1);
                    }
                };
                let duration = start.elapsed();
                println!(
                    "{} {}",
                    "request took".bright_black(),
                    format!("{}.{:03}s", duration.as_secs(), duration.subsec_millis()).purple()
                );
                println!(
                    "This used {} token, costing you ~{}$",
                    format!("{}", resp.usage.total_tokens).purple(),
                    format!("{:0.5}", openai::cost(resp.usage.total_tokens)).purple()
                );
                for (i, choice) in resp.choices.iter().enumerate() {
                    println!(
                        "\n{}",
                        format!("[{}]============================", i.to_string().purple())
                            .bright_black()
                    );
                    println!("{}", choice.message.content);
                }
                println!("{}", "\n================================".bright_black());
                if resp.choices.len() == 1 {
                    let answer = match Confirm::new("Do you want to commit with this message? ")
                        .with_default(true)
                        .prompt()
                    {
                        Ok(answer) => answer,
                        Err(e) => {
                            println!("{e}");
                            process::exit(1);
                        }
                    };
                    if answer {
                        git::commit(resp.choices[0].message.content.clone());
                        println!("{} 🎉", "Commit successful!".purple());
                        process::exit(0);
                    } else {
                        process::exit(0);
                    }
                }
                let max_index = resp.choices.len();
                let commit_index = match inquire::CustomType::<usize>::new(&format!(
                    "Which commit message do you want to use? {}",
                    "<ESC> to cancel".bright_black()
                ))
                .with_validator(move |i: &usize| {
                    if *i >= max_index {
                        Err(CustomUserError::from("Invalid index"))
                    } else {
                        Ok(Validation::Valid)
                    }
                })
                .prompt()
                {
                    Ok(i) => i,
                    Err(e) => {
                        println!("{e}");
                        process::exit(1);
                    }
                };
                let commit_msg = resp.choices[commit_index].message.content.clone();
                git::commit(commit_msg);
                println!("{} 🎉", "Commit successful!".purple());
            } else {
                let e = match response.text() {
                    Ok(e) => e,
                    Err(e) => {
                        println!("{e}");
                        process::exit(1);
                    }
                };
                let error = match serde_json::from_str::<openai::ErrorRoot>(&e) {
                    Ok(error) => error.error,
                    Err(e) => {
                        println!("{e}");
                        process::exit(1);
                    }
                };
                println!("{error}");
            }
        }
        Err(e) => {
            println!("{e}");
            process::exit(1);
        }
    }
}