diff --git a/Cargo.lock b/Cargo.lock index e65c54b..7901904 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,6 +204,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dirs" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" +dependencies = [ + "libc", + "redox_users", + "windows-sys 0.45.0", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -416,6 +436,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-openai", + "dirs", "dotenv", "futures", "lazy_static", @@ -424,6 +445,7 @@ dependencies = [ "tempfile", "tiktoken-rs", "tokio", + "toml", ] [[package]] @@ -807,6 +829,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.7.3" @@ -1041,6 +1074,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1266,6 +1308,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1647,6 +1723,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index f89ddc7..216628a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ futures = "0.3.28" stream-reduce = "0.1.0" tempfile = "3.5.0" shell-escape = "0.1.5" +dirs = "5.0.0" +toml = "0.7.3" diff --git a/install-commit-gpt.sh b/install-commit-gpt.sh new file mode 100755 index 0000000..47163ac --- /dev/null +++ b/install-commit-gpt.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# The user should always run as "source" and not execute directly +if [ "$0" = "$BASH_SOURCE" ]; then + printf "Please run this script as 'source install-commit-gpt.sh'\n" + exit 1 +fi + +# Check if happycommit is in the user's PATH +if ! command -v happycommit &> /dev/null; then + printf "happycommit not found in your PATH. Please install it first.\n" + exit 1 +fi + +current_shell=$(basename "$SHELL") + +# Add the git commit-gpt command to the user's shell configuration file +config_file="" +case "$current_shell" in + bash) + config_file="$HOME/.bashrc" + ;; + zsh) + config_file="$HOME/.zshrc" + ;; + *) + printf "Unsupported shell. Please add manually.\n" + exit 1 + ;; +esac + +# Check if it's Darwin so sed works properly +unameOut="$(uname -s)" +case "${unameOut}" in + Darwin*) machine=Mac;; + *) machine="UNKNOWN:${unameOut}" +esac + +printf 'Installing git command and adding to your "$PATH"\n' +# Create the directory ~/.happycommit/bin and copy the happycommit binary to it +mkdir -p ~/.happycommit/bin +cp git-commit-gpt ~/.happycommit/bin +chmod +x ~/.happycommit/bin/git-commit-gpt + +# Add the directory to the user's PATH +# if path is already in the PATH, don't add it again +if ! echo "$PATH" | grep -q "$HOME/.happycommit/bin"; then + printf "export PATH=\"$HOME/.happycommit/bin:\$PATH\"\n" >> "$config_file" +fi + +# Reload the shell configuration file +source "$config_file" + +# make sure git commit-gpt exists +if ! command -v git-commit-gpt &> /dev/null; then + printf "git commit-gpt not found in your PATH. Please add it manually.\n" + exit 1 +fi + +# Ask for user's OPENAI_API_KEY so it can be added to happycommit's config file +# Add the OPENAI_API_KEY to happycommit's config file +# if it isn't there already. +# If it is there, let the user know. + +# First check if the config file exists +if [ ! -f "$HOME/.happycommit/config.toml" ]; then + printf "Creating config file at $HOME/.happycommit/config.toml\n" + mkdir -p "$HOME/.happycommit" + touch "$HOME/.happycommit/config.toml" +fi + +OPENAI_API_KEY="" +# Check if the OPENAI_API_KEY is already in the config file +if ! grep -qF "OPENAI_API_KEY" "$HOME/.happycommit/config.toml"; then + # Read the OPENAI_API_KEY from the user + printf "Please enter your OPENAI_API_KEY (you can get it at https://beta.openai.com/account/api-keys):\nNote: Your input will not be shown on the screen.\n> " + read -rs OPENAI_API_KEY + printf "\n" + # Add the OPENAI_API_KEY to the config.toml file + printf "OPENAI_API_KEY = \"%s\"\n" "$OPENAI_API_KEY" >> "$HOME/.happycommit/config.toml" +else + printf "OPENAI_API_KEY already exists in $HOME/.happycommit/config.toml\n" + printf "Would you like to update it? (y/n) " + read -r update + if [ "$update" = "y" ]; then + # TODO: Remove code duplication + printf "Please enter your OPENAI_API_KEY (you can get it at https://beta.openai.com/account/api-keys):\nNote: Your input will not be shown on the screen.\n> " + read -rs OPENAI_API_KEY + printf "\n" + # Remove the previous OPENAI_API_KEY from the config file + if [ "$machine" = "Mac" ]; then + sed -i '' '/OPENAI_API_KEY/d' "$HOME/.happycommit/config.toml" + else + sed -i '/OPENAI_API_KEY/d' "$HOME/.happycommit/config.toml" + fi + # Add the OPENAI_API_KEY to the config file + printf "OPENAI_API_KEY = \"%s\"\n" "$OPENAI_API_KEY" >> "$HOME/.happycommit/config.toml" + fi +fi + +# Done! +printf "git commit-gpt installed successfully. You can now use 'git commit-gpt' to run happycommit.\n" diff --git a/justfile b/justfile index 012d7ef..f1c4b50 100644 --- a/justfile +++ b/justfile @@ -10,5 +10,9 @@ format: clippy: @cargo clippy +# Install the binary +install: + @cargo install --path . + # Run both 'format' and 'clippy' tasks fix: format clippy \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5a1e787..bd01929 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ pub struct ChatCompletionRequestMessage { pub role: String, } +#[allow(clippy::from_over_into)] // TODO: fix this impl Into for ChatCompletionRequestMessage { fn into(self) -> tiktoken_rs::ChatCompletionRequestMessage { tiktoken_rs::ChatCompletionRequestMessage { @@ -40,6 +41,7 @@ impl From for ChatCompletionRequestMe } } +#[allow(clippy::from_over_into)] // TODO: fix this impl Into for ChatCompletionRequestMessage { fn into(self) -> async_openai::types::ChatCompletionRequestMessage { async_openai::types::ChatCompletionRequestMessage { @@ -70,6 +72,7 @@ pub struct ChatCompletionRequestMessages { pub messages: Vec, } +#[allow(clippy::from_over_into)] // TODO: fix this impl Into> for ChatCompletionRequestMessages { fn into(self) -> Vec { self.messages @@ -221,10 +224,46 @@ async fn send_to_openai( type ChatMessage = (String, async_openai::types::Role, String); +fn load_api_key() -> Result> { + // first check in ~/.happycommit/config.toml + let happycommitconfig_checker = || -> Result> { + let config_path = dirs::home_dir().unwrap().join(".happycommit/config.toml"); + let config = std::fs::read_to_string(config_path)?; + let config: toml::Value = toml::from_str(&config)?; + let openai_api_key = config.get("openai_api_key"); + if openai_api_key.is_none() { + return Err("OPENAI_API_KEY not set in ~/.happycommit/config.toml".into()); + } + Ok(openai_api_key.unwrap().to_string()) + }; + let dotenv_checker = || -> Result> { + dotenv::dotenv().ok(); + let openai_api_key = + dotenv::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set in .env file"); + Ok(openai_api_key) + }; + + // first check happycommit config, then check dotenv - if in no places, throw + let result = happycommitconfig_checker(); + if result.is_ok() { + return result; + } + let result = dotenv_checker(); + if result.is_ok() { + return result; + } + // throw an error + + Err("OPENAI_API_KEY must be set in .env file or ~/.happycommit/config.toml".into()) +} + #[tokio::main(flavor = "current_thread")] async fn main() { - dotenv::dotenv().ok(); - let openai_api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set"); + let openai_api_key = load_api_key() + .map_err(|e| { + panic!("Error loading OpenAI API key: {}", e); + }) + .unwrap(); // by default, read in all the code changes since origin/master // TODO: allow user to specify a different origin branch or commit @@ -360,7 +399,7 @@ async fn main() { let commit_message = format!("{}\n\n{}", subject, body); let mut commit_file = tempfile::NamedTempFile::new().expect("Failed to create temporary file"); - commit_message.split("\n").for_each(|line| { + commit_message.split('\n').for_each(|line| { let _ = writeln!(commit_file, "{}", line); }); @@ -387,6 +426,7 @@ async fn main() { use anyhow::Result; +#[allow(clippy::needless_borrow)] async fn stream_multipart_commit_message( client: &Client, initial_prompt: &str,