git: Compress diff for commit message generation (#42835)
This PR compresses diff capped at 20000 bytes by: - Truncation of all lines to 256 chars - Iteratively removing last hunks from each file until size <= 20000 bytes. Closes #34486 Release Notes: - Improved: Compress large diffs for commit message generation (thanks @11happy) --------- Signed-off-by: 11happy <soni5happy@gmail.com> Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
This commit is contained in:
co-authored by
Oleksiy Syvokon
parent
29f9853978
commit
9094eb811b
@@ -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<String>,
|
||||
hunks_to_keep: usize,
|
||||
}
|
||||
|
||||
impl TruncatedPatch {
|
||||
fn from_unified_diff(patch_str: &str) -> Option<Self> {
|
||||
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<Entity<Repository>>,
|
||||
pub(crate) commit_editor: Entity<Editor>,
|
||||
@@ -1816,6 +1877,96 @@ impl GitPanel {
|
||||
self.generate_commit_message(cx);
|
||||
}
|
||||
|
||||
fn split_patch(patch: &str) -> Vec<String> {
|
||||
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<TruncatedPatch> = 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::<usize>();
|
||||
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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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<Self>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user