diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 5c86d79eee41e61d7cc904bfe148be6e8c9abdd2..e810672c69b9ed602ddf76c2ca1f1035b958cd26 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -11,6 +11,7 @@ use crate::{ use agent_settings::AgentSettings; use anyhow::Context as _; use askpass::AskPassDelegate; +use cloud_llm_client::CompletionIntent; use db::kvp::KEY_VALUE_STORE; use editor::{ Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, @@ -68,14 +69,11 @@ use ui::{ use util::paths::PathStyle; use util::{ResultExt, TryFutureExt, maybe}; use workspace::SERIALIZATION_THROTTLE_TIME; - -use cloud_llm_client::CompletionIntent; use workspace::{ Workspace, dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt}, }; - actions!( git_panel, [ @@ -275,6 +273,69 @@ impl GitStatusEntry { } } +struct TruncatedPatch { + header: String, + hunks: Vec, + hunks_to_keep: usize, +} + +impl TruncatedPatch { + fn from_unified_diff(patch_str: &str) -> Option { + let lines: Vec<&str> = patch_str.lines().collect(); + if lines.len() < 2 { + return None; + } + let header = format!("{}\n{}\n", lines[0], lines[1]); + let mut hunks = Vec::new(); + let mut current_hunk = String::new(); + for line in &lines[2..] { + if line.starts_with("@@") { + if !current_hunk.is_empty() { + hunks.push(current_hunk); + } + current_hunk = format!("{}\n", line); + } else if !current_hunk.is_empty() { + current_hunk.push_str(line); + current_hunk.push('\n'); + } + } + if !current_hunk.is_empty() { + hunks.push(current_hunk); + } + if hunks.is_empty() { + return None; + } + let hunks_to_keep = hunks.len(); + Some(TruncatedPatch { + header, + hunks, + hunks_to_keep, + }) + } + fn calculate_size(&self) -> usize { + let mut size = self.header.len(); + for (i, hunk) in self.hunks.iter().enumerate() { + if i < self.hunks_to_keep { + size += hunk.len(); + } + } + size + } + fn to_string(&self) -> String { + let mut out = self.header.clone(); + for (i, hunk) in self.hunks.iter().enumerate() { + if i < self.hunks_to_keep { + out.push_str(hunk); + } + } + let skipped_hunks = self.hunks.len() - self.hunks_to_keep; + if skipped_hunks > 0 { + out.push_str(&format!("[...skipped {} hunks...]\n", skipped_hunks)); + } + out + } +} + pub struct GitPanel { pub(crate) active_repository: Option>, pub(crate) commit_editor: Entity, @@ -1816,6 +1877,96 @@ impl GitPanel { self.generate_commit_message(cx); } + fn split_patch(patch: &str) -> Vec { + let mut result = Vec::new(); + let mut current_patch = String::new(); + + for line in patch.lines() { + if line.starts_with("---") && !current_patch.is_empty() { + result.push(current_patch.trim_end_matches('\n').into()); + current_patch = String::new(); + } + current_patch.push_str(line); + current_patch.push('\n'); + } + + if !current_patch.is_empty() { + result.push(current_patch.trim_end_matches('\n').into()); + } + + result + } + fn truncate_iteratively(patch: &str, max_bytes: usize) -> String { + let mut current_size = patch.len(); + if current_size <= max_bytes { + return patch.to_string(); + } + let file_patches = Self::split_patch(patch); + let mut file_infos: Vec = file_patches + .iter() + .filter_map(|patch| TruncatedPatch::from_unified_diff(patch)) + .collect(); + + if file_infos.is_empty() { + return patch.to_string(); + } + + current_size = file_infos.iter().map(|f| f.calculate_size()).sum::(); + while current_size > max_bytes { + let file_idx = file_infos + .iter() + .enumerate() + .filter(|(_, f)| f.hunks_to_keep > 1) + .max_by_key(|(_, f)| f.hunks_to_keep) + .map(|(idx, _)| idx); + match file_idx { + Some(idx) => { + let file = &mut file_infos[idx]; + let size_before = file.calculate_size(); + file.hunks_to_keep -= 1; + let size_after = file.calculate_size(); + let saved = size_before.saturating_sub(size_after); + current_size = current_size.saturating_sub(saved); + } + None => { + break; + } + } + } + + file_infos + .iter() + .map(|info| info.to_string()) + .collect::>() + .join("\n") + } + + pub fn compress_commit_diff(diff_text: &str, max_bytes: usize) -> String { + if diff_text.len() <= max_bytes { + return diff_text.to_string(); + } + + let mut compressed = diff_text + .lines() + .map(|line| { + if line.len() > 256 { + format!("{}...[truncated]\n", &line[..256]) + } else { + format!("{}\n", line) + } + }) + .collect::>() + .join(""); + + if compressed.len() <= max_bytes { + return compressed; + } + + compressed = Self::truncate_iteratively(&compressed, max_bytes); + + compressed + } + /// Generates a commit message using an LLM. pub fn generate_commit_message(&mut self, cx: &mut Context) { if !self.can_commit() || !AgentSettings::get_global(cx).enabled(cx) { @@ -1874,10 +2025,8 @@ impl GitPanel { } }; - const ONE_MB: usize = 1_000_000; - if diff_text.len() > ONE_MB { - diff_text = diff_text.chars().take(ONE_MB).collect() - } + const MAX_DIFF_BYTES: usize = 20_000; + diff_text = Self::compress_commit_diff(&diff_text, MAX_DIFF_BYTES); let subject = this.update(cx, |this, cx| { this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default() @@ -5032,6 +5181,7 @@ mod tests { status::{StatusCode, UnmergedStatus, UnmergedStatusCode}, }; use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; + use indoc::indoc; use project::FakeFs; use serde_json::json; use settings::SettingsStore; @@ -5731,4 +5881,64 @@ mod tests { ); } } + + #[test] + fn test_compress_diff_no_truncation() { + let diff = indoc! {" + --- a/file.txt + +++ b/file.txt + @@ -1,2 +1,2 @@ + -old + +new + "}; + let result = GitPanel::compress_commit_diff(diff, 1000); + assert_eq!(result, diff); + } + + #[test] + fn test_compress_diff_truncate_long_lines() { + let long_line = "a".repeat(300); + let diff = indoc::formatdoc! {" + --- a/file.txt + +++ b/file.txt + @@ -1,2 +1,3 @@ + context + +{} + more context + ", long_line}; + let result = GitPanel::compress_commit_diff(&diff, 100); + assert!(result.contains("...[truncated]")); + assert!(result.len() < diff.len()); + } + + #[test] + fn test_compress_diff_truncate_hunks() { + let diff = indoc! {" + --- a/file.txt + +++ b/file.txt + @@ -1,2 +1,2 @@ + context + -old1 + +new1 + @@ -5,2 +5,2 @@ + context 2 + -old2 + +new2 + @@ -10,2 +10,2 @@ + context 3 + -old3 + +new3 + "}; + let result = GitPanel::compress_commit_diff(diff, 100); + let expected = indoc! {" + --- a/file.txt + +++ b/file.txt + @@ -1,2 +1,2 @@ + context + -old1 + +new1 + [...skipped 2 hunks...] + "}; + assert_eq!(result, expected); + } }