From 51d8f96348eade846eb82721bc190a8c93ace2a0 Mon Sep 17 00:00:00 2001 From: extrawurst Date: Fri, 15 Dec 2023 19:28:11 +0100 Subject: [PATCH] new crate that uses openai to summarize git diffs --- Cargo.lock | 100 ++++++++++++++++++++++++++++ Cargo.toml | 2 +- git2-summarize/Cargo.toml | 19 ++++++ git2-summarize/LICENSE.md | 1 + git2-summarize/README.md | 3 + git2-summarize/examples/simple.diff | 49 ++++++++++++++ git2-summarize/examples/simple.rs | 13 ++++ git2-summarize/src/lib.rs | 85 +++++++++++++++++++++++ 8 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 git2-summarize/Cargo.toml create mode 120000 git2-summarize/LICENSE.md create mode 100644 git2-summarize/README.md create mode 100644 git2-summarize/examples/simple.diff create mode 100644 git2-summarize/examples/simple.rs create mode 100644 git2-summarize/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1792def0..d922393a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -725,6 +725,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "git2-summarize" +version = "0.1.0" +dependencies = [ + "git2", + "log", + "openai-api-rs", + "thiserror", +] + [[package]] name = "git2-testing" version = "0.1.0" @@ -1056,6 +1066,21 @@ dependencies = [ "adler", ] +[[package]] +name = "minreq" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3371dfc7b772c540da1380123674a8e20583aca99907087d990ca58cf44203" +dependencies = [ + "log", + "once_cell", + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "webpki-roots", +] + [[package]] name = "mio" version = "0.8.5" @@ -1142,6 +1167,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "openai-api-rs" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ead8e910b541f342b445ab68dd52ae24d695abaf781aee2ae933a77ac29ea5de" +dependencies = [ + "minreq", + "serde", + "serde_json", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -1382,6 +1418,20 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "ron" version = "0.8.0" @@ -1413,6 +1463,28 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.14" @@ -1447,6 +1519,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "serde" version = "1.0.156" @@ -1579,6 +1661,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.10.0" @@ -1783,6 +1871,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -1882,6 +1976,12 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "which" version = "4.4.0" diff --git a/Cargo.toml b/Cargo.toml index ce834bdf..43104819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ trace-libgit = ["asyncgit/trace-libgit"] vendor-openssl = ["asyncgit/vendor-openssl"] [workspace] -members = ["asyncgit", "filetreelist", "git2-hooks", "git2-testing", "scopetime"] +members = ["asyncgit", "filetreelist", "git2-hooks", "git2-summarize", "git2-testing", "scopetime"] [profile.release] lto = true diff --git a/git2-summarize/Cargo.toml b/git2-summarize/Cargo.toml new file mode 100644 index 00000000..7d2041e8 --- /dev/null +++ b/git2-summarize/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "git2-summarize" +version = "0.1.0" +authors = ["extrawurst "] +edition = "2021" +description = "use openai gpt to summarize git2 diffs" +homepage = "https://github.com/extrawurst/gitui" +repository = "https://github.com/extrawurst/gitui" +documentation = "https://docs.rs/git2-summarize/" +readme = "README.md" +license = "MIT" +categories = ["development-tools"] +keywords = ["git"] + +[dependencies] +git2 = ">=0.17" +log = "0.4" +thiserror = "1.0" +openai-api-rs = "2.1" diff --git a/git2-summarize/LICENSE.md b/git2-summarize/LICENSE.md new file mode 120000 index 00000000..7eabdb1c --- /dev/null +++ b/git2-summarize/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/git2-summarize/README.md b/git2-summarize/README.md new file mode 100644 index 00000000..8c108ec1 --- /dev/null +++ b/git2-summarize/README.md @@ -0,0 +1,3 @@ +# git2-summarize + +this uses open ai chat-gpt to summarize a git diff \ No newline at end of file diff --git a/git2-summarize/examples/simple.diff b/git2-summarize/examples/simple.diff new file mode 100644 index 00000000..f9f54599 --- /dev/null +++ b/git2-summarize/examples/simple.diff @@ -0,0 +1,49 @@ +diff --git a/git2-hooks/src/lib.rs b/git2-hooks/src/lib.rs +index 0cf5ac8..042ac87 100644 +--- a/git2-hooks/src/lib.rs ++++ b/git2-hooks/src/lib.rs +@@ -27,6 +27,7 @@ use git2::Repository; + pub const HOOK_POST_COMMIT: &str = "post-commit"; + pub const HOOK_PRE_COMMIT: &str = "pre-commit"; + pub const HOOK_COMMIT_MSG: &str = "commit-msg"; ++pub const HOOK_PRE_PUSH: &str = "pre-push"; + + const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG"; + +@@ -152,6 +153,36 @@ pub fn hooks_post_commit( + hook.run_hook(&[]) + } + ++/// see [`hooks_pre_push`] ++pub enum PrePushHookLocalRef<'a> { ++ Delete, ++ Ref { ++ local_ref: &'a str, ++ local_obj_name: &'a str, ++ }, ++} ++ ++/// see https://git-scm.com/docs/githooks#_pre_push ++/// ++/// # Arguments ++/// ++/// * `remote_obj_name` - pass `None` if foreign ref not yet exists ++pub fn hooks_pre_push( ++ repo: &Repository, ++ other_paths: Option<&[&str]>, ++ local_ref: PrePushHookLocalRef, ++ remote_ref: &str, ++ remote_obj_name: Option<&str>, ++) -> Result { ++ let hook = HookPaths::new(repo, other_paths, HOOK_PRE_PUSH)?; ++ ++ if !hook.found() { ++ return Ok(HookResult::NoHookFound); ++ } ++ ++ hook.run_hook(&[]) ++} ++ + #[cfg(test)] + mod tests { + use super::*; diff --git a/git2-summarize/examples/simple.rs b/git2-summarize/examples/simple.rs new file mode 100644 index 00000000..a39e800d --- /dev/null +++ b/git2-summarize/examples/simple.rs @@ -0,0 +1,13 @@ +use std::env; + +fn main() { + let diff = include_str!("simple.diff"); + + let summary = git2_summarize::git_diff_summarize_old( + &env::var("OPENAI_API_KEY").unwrap(), + diff, + ) + .unwrap(); + + println!("{summary}"); +} diff --git a/git2-summarize/src/lib.rs b/git2-summarize/src/lib.rs new file mode 100644 index 00000000..20d8242f --- /dev/null +++ b/git2-summarize/src/lib.rs @@ -0,0 +1,85 @@ +//! Uses Open API GPT-3 to summarize unified git diffs + +use openai_api_rs::v1::{ + api::Client, + chat_completion::{self, ChatCompletionRequest}, + common::GPT3_5_TURBO, + completion::{self, CompletionRequest}, +}; + +/// Uses old GPT3_TEXT_DAVINCI_003 model to generate message +/// +/// # Arguments +/// +/// * `api_key` - open api key +/// * `diff` - expects a diff formatted as a unified diff +pub fn git_diff_summarize_old( + api_key: &str, + diff: &str, +) -> Result { + let client = Client::new(api_key.to_string()); + + let req = CompletionRequest::new( + completion::GPT3_TEXT_DAVINCI_003.to_string(), + format!("Generate a Git commit message based on the following summary: {}\n\nCommit message: ",diff), + ) + .max_tokens(500) + .temperature(0.5) + .n(1); + + let result = client.completion(req).map_err(|e| e.message)?; + Ok(result + .choices + .get(0) + .ok_or_else(|| String::from("choises empty"))? + .text + .clone()) +} + +/// Uses GPT3_5_TURBO model to generate message using chat completion API +/// +/// # Arguments +/// +/// * `api_key` - open api key +/// * `diff` - expects a diff formatted as a unified diff +pub fn git_diff_summarize( + api_key: &str, + diff: &str, + line_length: usize, +) -> Result { + let client = Client::new(api_key.to_string()); + + let prompt = format!( + r#"You are a smart git commit message creator software. + Now you are going to create a git commit message. + The commit messages you generate aim to explain why the changes were introduced. + Write a one-sentence message no longer than {line_length} characters, followed by two newline characters. + Create a commit message for these changes:\n{} + "#, + diff + ); + + let req = ChatCompletionRequest::new( + GPT3_5_TURBO.to_string(), + vec![chat_completion::ChatCompletionMessage { + role: chat_completion::MessageRole::system, + content: prompt, + name: None, + function_call: None, + }], + ) + .max_tokens(200); + + let result = + client.chat_completion(req).map_err(|e| e.message)?; + + Ok(result + .choices + .get(0) + .ok_or_else(|| String::from("response.choises empty"))? + .message + .content + .as_ref() + .ok_or_else(|| String::from("choise[0].message empty"))? + .clone()) +}