From 55d0b7c6d188d0cccca203cc330d0d72f1e7bbaf Mon Sep 17 00:00:00 2001 From: arthrod <89408329+arthrod@users.noreply.github.com> Date: Mon, 19 May 2025 03:05:38 -0400 Subject: [PATCH 1/2] Document model env and improve diff handling --- .gitignore | 1 - Cargo.lock | 2 ++ Cargo.toml | 4 ++++ README.md | 6 +++++ src/lib.rs | 36 ++++++++++++++++++++++++++++++ src/main.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++-------- 6 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 Cargo.lock create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 088ba6b..9aa1577 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3b8b69c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2 @@ +# Placeholder Cargo.lock +# Real lockfile generation failed due to missing dependencies in offline environment. diff --git a/Cargo.toml b/Cargo.toml index 39dfdaf..5344d4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,7 @@ log = { version = "0.4.8", features = ["std"] } tokio = { version = "1.28.2", features = ["full"] } clap = { version = "4.0.18", features = ["derive"] } async-openai = { version = "0.12.0", default-features = false, features = ["native-tls"] } + +[lib] +name = "auto_commit" +path = "src/lib.rs" diff --git a/README.md b/README.md index f56e69c..9d09f0f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ You may need to close and reopen your terminal after installation. Alternatively export OPENAI_API_KEY='sk-XXXXXXXX' ``` +`auto-commit` uses the `AUTO_COMMIT_MODEL` environment variable to choose which OpenAI model to use when generating commit messages. If this variable is not set, the tool defaults to `gpt-4.1-nano`. + +```bash +export AUTO_COMMIT_MODEL='gpt-4.1-nano' +``` + Once you have configured your environment, stage some changes by running, for example, `git add .`, and then run `auto-commit`. Of course, `auto-commit` also includes some options, for editing the message before commiting, or just printing the message to the terminal. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eb49b4b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,36 @@ +/// Truncate a string to the first `limit` whitespace-delimited words. +pub fn truncate_to_n_tokens(text: &str, limit: usize) -> String { + text.split_whitespace().take(limit).collect::>().join(" ") +} + +/// Fetch the model name from the `AUTO_COMMIT_MODEL` environment variable. +/// Defaults to `DEFAULT_MODEL` when the variable is not set. +pub const DEFAULT_MODEL: &str = "gpt-4.1-nano"; + +pub fn get_model_from_env() -> String { + std::env::var("AUTO_COMMIT_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_truncate_to_n_tokens() { + let text = "a b c d e"; + assert_eq!(truncate_to_n_tokens(text, 3), "a b c"); + } + + #[test] + fn test_get_model_from_env_default() { + std::env::remove_var("AUTO_COMMIT_MODEL"); + assert_eq!(get_model_from_env(), DEFAULT_MODEL); + } + + #[test] + fn test_get_model_from_env_custom() { + std::env::set_var("AUTO_COMMIT_MODEL", "custom-model"); + assert_eq!(get_model_from_env(), "custom-model"); + std::env::remove_var("AUTO_COMMIT_MODEL"); + } +} diff --git a/src/main.rs b/src/main.rs index 4f11f36..47394fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ use std::{ process::{Command, Stdio}, str, }; +use auto_commit::{get_model_from_env, truncate_to_n_tokens}; #[derive(Parser)] #[command(version)] @@ -63,6 +64,8 @@ impl ToString for Commit { } } +const MAX_DIFF_TOKENS: usize = 20_000; + #[tokio::main] async fn main() -> Result<(), ()> { let cli = Cli::parse(); @@ -79,10 +82,19 @@ async fn main() -> Result<(), ()> { .arg("diff") .arg("--staged") .output() - .expect("Couldn't find diff.") + .map_err(|e| { + error!("Failed to get staged diff: {}", e); + () + })? .stdout; - let git_staged_cmd = str::from_utf8(&git_staged_cmd).unwrap(); + let git_staged_cmd = match str::from_utf8(&git_staged_cmd) { + Ok(v) => v, + Err(e) => { + error!("Staged diff output was not valid UTF-8: {}", e); + "" + } + }; if git_staged_cmd.is_empty() { error!("There are no staged files to commit.\nTry running `git add` to stage some files."); @@ -92,23 +104,56 @@ async fn main() -> Result<(), ()> { .arg("rev-parse") .arg("--is-inside-work-tree") .output() - .expect("Failed to check if this is a git repository.") + .map_err(|e| { + error!("Failed to check if this is a git repository: {}", e); + () + })? .stdout; - if str::from_utf8(&is_repo).unwrap().trim() != "true" { + if str::from_utf8(&is_repo).unwrap_or("").trim() != "true" { error!("It looks like you are not in a git repository.\nPlease run this command from the root of a git repository, or initialize one using `git init`."); std::process::exit(1); } let client = async_openai::Client::with_config(OpenAIConfig::new().with_api_key(api_token)); - let output = Command::new("git") + let files_output = Command::new("git") + .arg("diff") + .arg("--name-only") + .arg("--staged") + .output() + .map_err(|e| { + error!("Couldn't get changed files: {}", e); + () + })? + .stdout; + let files_changed = match str::from_utf8(&files_output) { + Ok(v) => v, + Err(e) => { + error!("Changed files output was not valid UTF-8: {}", e); + "" + } + }; + + let diff_output = Command::new("git") .arg("diff") - .arg("HEAD") + .arg("--staged") .output() - .expect("Couldn't find diff.") + .map_err(|e| { + error!("Couldn't find diff: {}", e); + () + })? .stdout; - let output = str::from_utf8(&output).unwrap(); + let diff_output = match str::from_utf8(&diff_output) { + Ok(v) => v, + Err(e) => { + error!("Diff output was not valid UTF-8: {}", e); + "" + } + }; + + let combined = format!("Changed files:\n{}\n\nDiff:\n{}", files_changed, diff_output); + let output = truncate_to_n_tokens(&combined, MAX_DIFF_TOKENS); if !cli.dry_run { info!("Loading Data..."); @@ -206,7 +251,7 @@ async fn main() -> Result<(), ()> { .function_call(ChatCompletionFunctionCall::Object( json!({ "name": "commit" }), )) - .model("gpt-3.5-turbo-16k") + .model(&get_model_from_env()) .temperature(0.0) .max_tokens(2000u16) .build() From 701ed4fe3342e67ae1178d493a0ef79fd6311e3b Mon Sep 17 00:00:00 2001 From: arthrod <89408329+arthrod@users.noreply.github.com> Date: Mon, 19 May 2025 12:20:39 -0400 Subject: [PATCH 2/2] Update src/main.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/main.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index de1db10..f01b934 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,7 +110,16 @@ async fn main() -> Result<(), ()> { })? .stdout; - if str::from_utf8(&is_repo).unwrap_or("").trim() != "true" { + if match str::from_utf8(&is_repo) { + Ok(v) => v.trim() != "true", + Err(e) => { + error!("Git repository check output was not valid UTF-8: {}", e); + true // Treat as not a repo if output is invalid + } + } { + error!("It looks like you are not in a git repository.\nPlease run this command from the root of a git repository, or initialize one using `git init`."); + std::process::exit(1); + } error!("It looks like you are not in a git repository.\nPlease run this command from the root of a git repository, or initialize one using `git init`."); std::process::exit(1); }