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:
Bhuminjay Soni
2025-11-20 11:30:34 +00:00
committed by GitHub
co-authored by Oleksiy Syvokon
parent 29f9853978
commit 9094eb811b
+217 -7
View File
@@ -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);
}
}