@@ -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);
+ }
}