use crate::engine::diff_parser;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct DiffChunk {
pub index: usize,
pub total: usize,
pub label: String,
pub diff: String,
pub file_count: usize,
}
pub fn chunk_diff(diff: &str, max_size: usize) -> Vec<DiffChunk> {
let file_chunks = diff_parser::parse_diff(diff);
if file_chunks.is_empty() {
return vec![DiffChunk {
index: 0,
total: 1,
label: "empty".to_string(),
diff: diff.to_string(),
file_count: 0,
}];
}
let file_diffs = split_raw_diff_by_file(diff, &file_chunks);
let groups = group_by_size(&file_diffs, max_size);
let total = groups.len();
groups
.into_iter()
.enumerate()
.map(|(i, group)| {
let label = derive_label(&group);
let combined_diff = group.iter().map(|f| f.diff.as_str()).collect::<String>();
let file_count = group.len();
DiffChunk {
index: i,
total,
label,
diff: combined_diff,
file_count,
}
})
.collect()
}
struct FileDiff {
dir_prefix: String,
diff: String,
size: usize,
}
fn split_raw_diff_by_file(diff: &str, file_chunks: &[diff_parser::FileChunk]) -> Vec<FileDiff> {
let mut file_diffs = Vec::new();
let lines: Vec<&str> = diff.lines().collect();
let mut line_idx = 0;
for fc in file_chunks {
let path = fc
.new_path
.as_deref()
.or(fc.old_path.as_deref())
.unwrap_or("unknown");
let start = find_file_start(&lines, line_idx, path);
if let Some(start) = start {
let end = find_file_end(&lines, start + 1);
let file_diff_text = lines[start..end].join("\n");
file_diffs.push(FileDiff {
dir_prefix: get_dir_prefix(path),
diff: file_diff_text,
size: lines[start..end].join("\n").len(),
});
line_idx = end;
} else {
debug!(path = path, "could not locate file in raw diff, skipping");
}
}
if file_diffs.is_empty() && !diff.is_empty() {
file_diffs.push(FileDiff {
dir_prefix: "all".to_string(),
diff: diff.to_string(),
size: diff.len(),
});
}
file_diffs
}
fn find_file_start(lines: &[&str], start_from: usize, path: &str) -> Option<usize> {
for i in start_from..lines.len() {
let line = lines[i];
if line.starts_with("--- ") {
if i + 1 < lines.len() {
let next = lines[i + 1];
if next.starts_with("+++ ") {
let matched = if line.contains("/dev/null") {
next.contains(path)
} else {
line.contains(path) || next.contains(path)
};
if matched {
return Some(i);
}
}
}
}
}
None
}
fn find_file_end(lines: &[&str], start_from: usize) -> usize {
for i in start_from..lines.len() {
if lines[i].starts_with("--- ") && i + 1 < lines.len() && lines[i + 1].starts_with("+++ ") {
if lines[i].starts_with("--- a/") || lines[i].starts_with("--- /dev/null") {
return i;
}
}
}
lines.len()
}
fn group_by_size(file_diffs: &[FileDiff], max_size: usize) -> Vec<Vec<&FileDiff>> {
let mut groups: Vec<Vec<&FileDiff>> = Vec::new();
let mut current_group: Vec<&FileDiff> = Vec::new();
let mut current_size: usize = 0;
for fd in file_diffs {
if !current_group.is_empty() && current_size + fd.size > max_size {
groups.push(current_group);
current_group = Vec::new();
current_size = 0;
}
current_size += fd.size;
current_group.push(fd);
}
if !current_group.is_empty() {
groups.push(current_group);
}
groups
}
fn get_dir_prefix(path: &str) -> String {
let path = path.trim_start_matches("a/").trim_start_matches("b/");
path.split('/').next().unwrap_or("root").to_string()
}
fn derive_label(group: &[&FileDiff]) -> String {
if group.len() == 1 {
let path = group[0].dir_prefix.clone();
format!("{}/*", path)
} else {
let dirs: Vec<&str> = group.iter().map(|f| f.dir_prefix.as_str()).collect();
let unique: Vec<&str> = {
let mut d = dirs.clone();
d.sort();
d.dedup();
d
};
if unique.len() == 1 {
format!("{}/*", unique[0])
} else if unique.len() <= 3 {
unique.join(", ")
} else {
format!("{} dirs", unique.len())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_diff() -> &'static str {
"\
diff --git a/src/main.rs b/src/main.rs
index abc123..def456 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
fn main() {
- println!(\"hello\");
+ println!(\"hello world\");
+ // added line
}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,3 @@
+pub fn add(a: i32, b: i32) -> i32 {
+ a + b
+}
diff --git a/docs/README.md b/docs/README.md
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,3 +1,4 @@
# Title
+Added line
Some text
End
"
}
#[test]
fn chunk_diff_single_chunk_when_small() {
let chunks = chunk_diff(sample_diff(), 100_000);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].total, 1);
}
#[test]
fn chunk_diff_splits_when_exceeds_max() {
let chunks = chunk_diff(sample_diff(), 50);
assert!(chunks.len() > 1, "Should split into multiple chunks");
}
#[test]
fn chunk_labels_are_human_readable() {
let chunks = chunk_diff(sample_diff(), 50);
for chunk in &chunks {
assert!(!chunk.label.is_empty());
}
}
#[test]
fn empty_diff_returns_single_chunk() {
let chunks = chunk_diff("", 1000);
assert_eq!(chunks.len(), 1);
}
#[test]
fn get_dir_prefix_strips_a_prefix() {
assert_eq!(get_dir_prefix("a/src/main.rs"), "src");
assert_eq!(get_dir_prefix("b/docs/README.md"), "docs");
assert_eq!(get_dir_prefix("lib.rs"), "lib.rs");
}
#[test]
fn group_by_size_respects_max() {
let files = vec![
FileDiff {
dir_prefix: "src".to_string(),
diff: "a".repeat(100),
size: 100,
},
FileDiff {
dir_prefix: "docs".to_string(),
diff: "b".repeat(100),
size: 100,
},
FileDiff {
dir_prefix: "src".to_string(),
diff: "c".repeat(50),
size: 50,
},
];
let groups = group_by_size(&files, 150);
assert_eq!(groups.len(), 2);
}
#[test]
fn derive_label_single_file() {
let fd = FileDiff {
dir_prefix: "src".to_string(),
diff: String::new(),
size: 0,
};
let files = vec![&fd];
assert_eq!(derive_label(&files), "src/*");
}
#[test]
fn derive_label_multiple_dirs() {
let fd1 = FileDiff {
dir_prefix: "src".to_string(),
diff: String::new(),
size: 0,
};
let fd2 = FileDiff {
dir_prefix: "docs".to_string(),
diff: String::new(),
size: 0,
};
let files = vec![&fd1, &fd2];
assert_eq!(derive_label(&files), "docs, src");
}
}