From f0be14277a1fe9143d283f57fb2e33835690ef42 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 5 Mar 2025 16:42:48 -0500 Subject: [PATCH] git_ui: Scaffold out support for generating commit messages with an LLM (#26161) This PR adds the rough structure needed to support generating commit messages using an LLM. This functionality is not yet surfaced to the user. This is the current state, if you tweak the source to show the button: https://github.com/user-attachments/assets/66d1fbc4-09f3-4277-84f4-e9c9ebab274c Release Notes: - N/A --- Cargo.lock | 1 + crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/commit_message_prompt.txt | 15 ++ crates/git_ui/src/git_panel.rs | 160 +++++++++++++++----- 4 files changed, 139 insertions(+), 38 deletions(-) create mode 100644 crates/git_ui/src/commit_message_prompt.txt diff --git a/Cargo.lock b/Cargo.lock index 7c266773bb4aa9e1b7f56175049ae1bd453d6747..c9494b4093d17fc322adae756e30f3f3436da501 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5446,6 +5446,7 @@ dependencies = [ "gpui", "itertools 0.14.0", "language", + "language_model", "linkify", "linkme", "log", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index cd287c3f1900c523deb43f7d054ac21ef19f5010..43e2f030ec63e5ed5a8406f340f020a7f7975855 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -31,6 +31,7 @@ git.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true +language_model.workspace = true linkify.workspace = true linkme.workspace = true log.workspace = true diff --git a/crates/git_ui/src/commit_message_prompt.txt b/crates/git_ui/src/commit_message_prompt.txt new file mode 100644 index 0000000000000000000000000000000000000000..56ad556868a596b362e6a87a51a371e02f06d9e2 --- /dev/null +++ b/crates/git_ui/src/commit_message_prompt.txt @@ -0,0 +1,15 @@ +You are an expert at writing Git commits. Your job is to write a clear commit message that summarizes the changes. + +Only return the commit message in your response. Do not include any additional meta-commentary about the task. + +Follow good Git style: + +- Separate the subject from the body with a blank line +- Try to limit the subject line to 50 characters +- Capitalize the subject line +- Do not end the subject line with any punctuation +- Use the imperative mood in the subject line +- Wrap the body at 72 characters +- Use the body to explain *what* and *why* vs. *how* + +Here are the changes in this commit: diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 96e41d058d7c460e86cc3d4320231b2916d7c216..ca6cd457c511be42b3e5e4da6009379fc9395b2c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -17,6 +17,7 @@ use editor::{ scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar, }; +use futures::StreamExt as _; use git::repository::{ Branch, CommitDetails, CommitSummary, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, @@ -33,6 +34,9 @@ use gpui::{ }; use itertools::Itertools; use language::{Buffer, File}; +use language_model::{ + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, +}; use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use multi_buffer::ExcerptInfo; use panel::{ @@ -216,6 +220,7 @@ pub struct GitPanel { conflicted_staged_count: usize, current_modifiers: Modifiers, add_coauthors: bool, + generate_commit_message_task: Option>>, entries: Vec, focus_handle: FocusHandle, fs: Arc, @@ -334,44 +339,46 @@ impl GitPanel { ) .detach(); - let scrollbar_state = - ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()); - - let mut git_panel = Self { - pending_remote_operations: Default::default(), - remote_operation_id: 0, - active_repository, - commit_editor, - conflicted_count: 0, - conflicted_staged_count: 0, - current_modifiers: window.modifiers(), - add_coauthors: true, - entries: Vec::new(), - focus_handle: cx.focus_handle(), - fs, - hide_scrollbar_task: None, - new_count: 0, - new_staged_count: 0, - pending: Vec::new(), - pending_commit: None, - pending_serialization: Task::ready(None), - project, - scroll_handle, - scrollbar_state, - selected_entry: None, - marked_entries: Vec::new(), - show_scrollbar: false, - tracked_count: 0, - tracked_staged_count: 0, - update_visible_entries_task: Task::ready(()), - width: Some(px(360.)), - context_menu: None, - workspace, - modal_open: false, - }; - git_panel.schedule_update(false, window, cx); - git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); - git_panel + let scrollbar_state = + ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()); + + let mut git_panel = Self { + pending_remote_operations: Default::default(), + remote_operation_id: 0, + active_repository, + commit_editor, + conflicted_count: 0, + conflicted_staged_count: 0, + current_modifiers: window.modifiers(), + add_coauthors: true, + generate_commit_message_task: None, + entries: Vec::new(), + focus_handle: cx.focus_handle(), + fs, + hide_scrollbar_task: None, + new_count: 0, + new_staged_count: 0, + pending: Vec::new(), + pending_commit: None, + pending_serialization: Task::ready(None), + project, + scroll_handle, + scrollbar_state, + selected_entry: None, + marked_entries: Vec::new(), + show_scrollbar: false, + tracked_count: 0, + tracked_staged_count: 0, + update_visible_entries_task: Task::ready(()), + width: Some(px(360.)), + context_menu: None, + workspace, + modal_open: false, + }; + git_panel.schedule_update(false, window, cx); + git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); + git_panel + }) } pub fn entry_by_path(&self, path: &RepoPath) -> Option { @@ -1422,6 +1429,71 @@ impl GitPanel { Some(format!("{} {}", action_text, file_name)) } + /// Generates a commit message using an LLM. + fn generate_commit_message(&mut self, cx: &mut Context) { + let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else { + return; + }; + let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { + return; + }; + + if !provider.is_authenticated(cx) { + return; + } + + const PROMPT: &str = include_str!("commit_message_prompt.txt"); + + // TODO: We need to generate a diff from the actual Git state. + // + // It need not look exactly like the structure below, this is just an example generated by Claude. + let diff_text = "diff --git a/src/main.rs b/src/main.rs +index 1234567..abcdef0 100644 +--- a/src/main.rs ++++ b/src/main.rs +@@ -10,7 +10,7 @@ fn main() { + println!(\"Hello, world!\"); +- let unused_var = 42; ++ let important_value = 42; + + // Do something with the value +- // TODO: Implement this later ++ println!(\"The answer is {}\", important_value); + }"; + + let request = LanguageModelRequest { + messages: vec![LanguageModelRequestMessage { + role: Role::User, + content: vec![format!("{PROMPT}\n{diff_text}").into()], + cache: false, + }], + tools: Vec::new(), + stop: Vec::new(), + temperature: None, + }; + + self.generate_commit_message_task = Some(cx.spawn(|this, mut cx| { + async move { + let stream = model.stream_completion_text(request, &cx); + let mut messages = stream.await?; + + while let Some(message) = messages.stream.next().await { + let text = message?; + + this.update(&mut cx, |this, cx| { + this.commit_message_buffer(cx).update(cx, |buffer, cx| { + let insert_position = buffer.anchor_before(buffer.len()); + buffer.edit([(insert_position..insert_position, text)], None, cx); + }); + })?; + } + + anyhow::Ok(()) + } + .log_err() + })); + } + fn update_editor_placeholder(&mut self, cx: &mut Context) { let suggested_commit_message = self.suggest_commit_message(); let placeholder_text = suggested_commit_message @@ -2172,6 +2244,8 @@ impl GitPanel { let panel_editor_style = panel_editor_style(true, window, cx); let enable_coauthors = self.render_co_authors(cx); + // Note: This is hard-coded to `false` as it is not fully implemented. + let show_generate_commit_message_button = false; let title = self.commit_button_title(); let editor_focus_handle = self.commit_editor.focus_handle(cx); @@ -2219,8 +2293,18 @@ impl GitPanel { .absolute() .bottom_0() .right_2() + .gap_0p5() .h(footer_size) .flex_none() + .when(show_generate_commit_message_button, |parent| { + parent.child( + panel_filled_button("Generate Commit Message").on_click( + cx.listener(move |this, _event, _window, cx| { + this.generate_commit_message(cx); + }), + ), + ) + }) .children(enable_coauthors) .child( panel_filled_button(title)