From c98ca240a6ed236e560962eb8ffd366439f48756 Mon Sep 17 00:00:00 2001 From: extrawurst Date: Fri, 15 Dec 2023 19:33:27 +0100 Subject: [PATCH] Add command to use ai to generate commit msg --- Cargo.lock | 1 + Cargo.toml | 1 + asyncgit/src/sync/diff.rs | 31 +++++++++++++++++++++++++++- asyncgit/src/sync/mod.rs | 2 +- src/components/commit.rs | 43 +++++++++++++++++++++++++++++++++++++++ src/keys/key_list.rs | 2 ++ src/strings.rs | 13 ++++++++++++ 7 files changed, 91 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d922393a..70f37d1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -765,6 +765,7 @@ dependencies = [ "filetreelist", "fuzzy-matcher", "gh-emoji", + "git2-summarize", "indexmap", "itertools", "log", diff --git a/Cargo.toml b/Cargo.toml index 43104819..bb70efea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ keywords = ["git", "gui", "cli", "terminal", "ui"] [dependencies] anyhow = "1.0" asyncgit = { path = "./asyncgit", version = "0.24", default-features = false } +git2-summarize = { path = "./git2-summarize", version = "0.1"} backtrace = "0.3" bitflags = "1.3" bugreport = "0.5" diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index f02e1be3..5f876914 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -9,7 +9,7 @@ use crate::{ error::Error, error::Result, hash, - sync::{get_stashes, repository::repo}, + sync::{get_stashes, repository::repo, utils::bytes2string}, }; use easy_cast::Conv; use git2::{ @@ -398,6 +398,35 @@ fn raw_diff_to_file_diff( Ok(res.into_inner()) } +/// +pub fn unified_stage_diff(repo_path: &RepoPath) -> Result { + scope_time!("unified_stage_diff"); + + let repo = repo(repo_path)?; + + let diff = get_diff_raw(&repo, "*", true, false, None)?; + + let mut output = String::with_capacity(32); + + diff.print(DiffFormat::Patch, |_delta, _hunk, line| { + let prefix = match line.origin_value() { + git2::DiffLineType::Addition => "+", + git2::DiffLineType::Deletion => "-", + _ => "", + }; + + output.push_str(&format!( + "{}{}", + prefix, + bytes2string(line.content()).unwrap() + )); + + true + })?; + + Ok(output) +} + const fn is_newline(c: char) -> bool { c == '\n' || c == '\r' } diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 909bea6f..ff3a2c08 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -62,7 +62,7 @@ pub use config::{ get_config_string, untracked_files_config, ShowUntrackedFilesConfig, }; -pub use diff::get_diff_commit; +pub use diff::{get_diff_commit, unified_stage_diff}; pub use git2::BranchType; pub use hooks::{ hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult, diff --git a/src/components/commit.rs b/src/components/commit.rs index ba32394a..95081b30 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -59,6 +59,7 @@ pub struct CommitComponent { commit_msg_history_idx: usize, options: SharedOptions, verify: bool, + open_ai_token: Option, } const FIRST_LINE_LIMIT: usize = 50; @@ -90,6 +91,7 @@ impl CommitComponent { commit_msg_history_idx: 0, options, verify: true, + open_ai_token: std::env::var("OPENAI_API_KEY").ok(), } } @@ -352,6 +354,30 @@ impl CommitComponent { self.input.set_text(signed_msg); } } + + fn commit_summarize(&mut self) -> Result<()> { + use std::result::Result::Ok; + + if let Some(api_key) = self.open_ai_token.as_ref() { + let mut unified_diff = + sync::unified_stage_diff(&self.repo.borrow())?; + + while unified_diff.len() > 3500 { + unified_diff.pop(); + } + + match git2_summarize::git_diff_summarize( + &api_key, + &unified_diff, + FIRST_LINE_LIMIT, + ) { + Ok(msg) => self.input.set_text(msg), + Err(e) => bail!(e), + }; + } + + Ok(()) + } fn toggle_verify(&mut self) { self.verify = !self.verify; } @@ -539,6 +565,14 @@ impl Component for CommitComponent { self.options.borrow().has_commit_msg_history(), true, )); + + out.push(CommandInfo::new( + strings::commands::commit_msg_summarize( + &self.key_config, + ), + self.open_ai_token.is_some(), + true, + )); } visibility_blocking(self) @@ -596,6 +630,15 @@ impl Component for CommitComponent { self.key_config.keys.toggle_signoff, ) { self.signoff_commit(); + } else if key_match( + e, + self.key_config.keys.commit_msg_summarize, + ) { + try_or_popup!( + self, + "commit summary error:", + self.commit_summarize() + ); } // stop key event propagation return Ok(EventState::Consumed); diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 55307193..916e1334 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -120,6 +120,7 @@ pub struct KeysList { pub view_submodule_parent: GituiKeyEvent, pub update_submodule: GituiKeyEvent, pub commit_history_next: GituiKeyEvent, + pub commit_msg_summarize: GituiKeyEvent, } #[rustfmt::skip] @@ -209,6 +210,7 @@ impl Default for KeysList { view_submodule_parent: GituiKeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty()), update_submodule: GituiKeyEvent::new(KeyCode::Char('u'), KeyModifiers::empty()), commit_history_next: GituiKeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL), + commit_msg_summarize: GituiKeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL), } } } diff --git a/src/strings.rs b/src/strings.rs index 6c3b061a..448ccff3 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -965,6 +965,19 @@ pub mod commands { CMD_GROUP_COMMIT_POPUP, ) } + pub fn commit_msg_summarize( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Summarize [{}]", + key_config + .get_hint(key_config.keys.commit_msg_summarize), + ), + "use openai chat-gpt to generate commit message", + CMD_GROUP_COMMIT_POPUP, + ) + } pub fn commit_enter(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!(