1use crate::config::CommitConfig;
3
4#[derive(Debug, Clone)]
5pub struct FileDiff {
6 pub filename: String,
7 pub header: String, pub content: String, pub additions: usize,
10 pub deletions: usize,
11 pub is_binary: bool,
12}
13
14impl FileDiff {
15 pub const fn size(&self) -> usize {
16 self.header.len() + self.content.len()
17 }
18
19 pub fn priority(&self, config: &CommitConfig) -> i32 {
20 if self.is_binary {
22 return -100; }
24
25 let filename_lower = self.filename.to_lowercase();
27 if filename_lower.ends_with("cargo.toml")
28 || filename_lower.ends_with("package.json")
29 || filename_lower.ends_with("go.mod")
30 || filename_lower.ends_with("requirements.txt")
31 || filename_lower.ends_with("pyproject.toml")
32 {
33 return 70; }
35
36 if self.filename.contains("/test")
38 || self.filename.contains("test_")
39 || self.filename.contains("_test.")
40 || self.filename.contains(".test.")
41 {
42 return 10;
43 }
44
45 let ext = self.filename.rsplit('.').next().unwrap_or("");
47 if config
48 .low_priority_extensions
49 .iter()
50 .any(|e| e.trim_start_matches('.') == ext)
51 {
52 return 20;
53 }
54
55 match ext {
57 "rs" | "go" | "py" | "js" | "ts" | "java" | "c" | "cpp" | "h" | "hpp" => 100,
58 "sql" | "sh" | "bash" => 80,
59 _ => 50,
60 }
61 }
62
63 pub fn truncate(&mut self, max_size: usize) {
64 if self.size() <= max_size {
65 return;
66 }
67
68 let available = max_size.saturating_sub(self.header.len() + 50); if available < 50 {
72 self.content = "... (truncated)".to_string();
74 } else {
75 let lines: Vec<&str> = self.content.lines().collect();
77 if lines.len() > 30 {
78 let keep_start = 15;
80 let keep_end = 10;
81 let omitted = lines.len() - keep_start - keep_end;
82 let est_size = keep_start * 60 + keep_end * 60 + 50;
84 let mut truncated = String::with_capacity(est_size);
85 for (i, line) in lines[..keep_start].iter().enumerate() {
86 if i > 0 {
87 truncated.push('\n');
88 }
89 truncated.push_str(line);
90 }
91 use std::fmt::Write;
92 write!(&mut truncated, "\n... (truncated {omitted} lines) ...\n").unwrap();
93 for (i, line) in lines[lines.len() - keep_end..].iter().enumerate() {
94 if i > 0 {
95 truncated.push('\n');
96 }
97 truncated.push_str(line);
98 }
99 self.content = truncated;
100 } else {
101 self.content.truncate(available);
103 self.content.push_str("\n... (truncated)");
104 }
105 }
106 }
107}
108
109pub fn parse_diff(diff: &str) -> Vec<FileDiff> {
111 let mut file_diffs = Vec::new();
112 let mut current_file: Option<FileDiff> = None;
113 let mut in_diff_header = false;
114
115 for line in diff.lines() {
116 if line.starts_with("diff --git") {
117 if let Some(file) = current_file.take() {
119 file_diffs.push(file);
120 }
121
122 let filename = line
124 .split_whitespace()
125 .nth(3)
126 .map_or("unknown", |s| s.trim_start_matches("b/"))
127 .to_string();
128
129 current_file = Some(FileDiff {
130 filename,
131 header: String::from(line),
132 content: String::new(),
133 additions: 0,
134 deletions: 0,
135 is_binary: false,
136 });
137 in_diff_header = true;
138 } else if let Some(ref mut file) = current_file {
139 if line.starts_with("Binary files") {
140 file.is_binary = true;
141 file.header.reserve(line.len() + 1);
142 file.header.push('\n');
143 file.header.push_str(line);
144 } else if line.starts_with("index ")
145 || line.starts_with("new file")
146 || line.starts_with("deleted file")
147 || line.starts_with("rename ")
148 || line.starts_with("similarity index")
149 || line.starts_with("+++")
150 || line.starts_with("---")
151 {
152 file.header.reserve(line.len() + 1);
154 file.header.push('\n');
155 file.header.push_str(line);
156 } else if line.starts_with("@@") {
157 in_diff_header = false;
159 file.header.reserve(line.len() + 1);
160 file.header.push('\n');
161 file.header.push_str(line);
162 } else if !in_diff_header {
163 if !file.content.is_empty() {
165 file.content.push('\n');
166 }
167 file.content.push_str(line);
168
169 if line.starts_with('+') && !line.starts_with("+++") {
170 file.additions += 1;
171 } else if line.starts_with('-') && !line.starts_with("---") {
172 file.deletions += 1;
173 }
174 } else {
175 file.header.reserve(line.len() + 1);
177 file.header.push('\n');
178 file.header.push_str(line);
179 }
180 }
181 }
182
183 if let Some(file) = current_file {
185 file_diffs.push(file);
186 }
187
188 file_diffs
189}
190
191pub fn smart_truncate_diff(diff: &str, max_length: usize, config: &CommitConfig) -> String {
193 let mut file_diffs = parse_diff(diff);
194
195 file_diffs.retain(|f| {
197 !config
198 .excluded_files
199 .iter()
200 .any(|excluded| f.filename.ends_with(excluded))
201 });
202
203 if file_diffs.is_empty() {
204 return "No relevant files to analyze (only lock files or excluded files were changed)"
205 .to_string();
206 }
207
208 file_diffs.sort_by_key(|f| -f.priority(config));
210
211 let total_size: usize = file_diffs.iter().map(|f| f.size()).sum();
213
214 if total_size <= max_length {
215 return reconstruct_diff(&file_diffs);
217 }
218
219 let mut included_files = Vec::new();
222 let mut current_size = 0;
223
224 let header_only_size: usize = file_diffs.iter().map(|f| f.header.len() + 20).sum();
226 let total_files = file_diffs.len();
227
228 if header_only_size <= max_length {
229 let remaining_space = max_length - header_only_size;
231 let space_per_file = if file_diffs.is_empty() {
232 0
233 } else {
234 remaining_space / file_diffs.len()
235 };
236
237 included_files.reserve(file_diffs.len());
238 for file in file_diffs {
239 if file.is_binary {
240 included_files.push(FileDiff {
242 filename: file.filename,
243 header: file.header,
244 content: String::new(),
245 additions: file.additions,
246 deletions: file.deletions,
247 is_binary: true,
248 });
249 } else {
250 let mut truncated = file;
251 let target_size = truncated.header.len() + space_per_file;
252 if truncated.size() > target_size {
253 truncated.truncate(target_size);
254 }
255 included_files.push(truncated);
256 }
257 }
258 } else {
259 for mut file in file_diffs {
261 if file.is_binary {
262 continue; }
264
265 let file_size = file.size();
266 if current_size + file_size <= max_length {
267 current_size += file_size;
268 included_files.push(file);
269 } else if current_size < max_length / 2 && file.priority(config) >= 50 {
270 let remaining = max_length - current_size;
273 file.truncate(remaining.saturating_sub(100)); included_files.push(file);
275 break;
276 }
277 }
278 }
279
280 if included_files.is_empty() {
281 return "Error: Could not include any files in the diff".to_string();
282 }
283
284 let mut result = reconstruct_diff(&included_files);
285
286 let excluded_count = total_files - included_files.len();
288 if excluded_count > 0 {
289 use std::fmt::Write;
290 write!(result, "\n\n... ({excluded_count} files omitted) ...").unwrap();
291 }
292
293 result
294}
295
296pub fn reconstruct_diff(files: &[FileDiff]) -> String {
298 let capacity: usize = files.iter().map(|f| f.size() + 1).sum();
300 let mut result = String::with_capacity(capacity);
301
302 for (i, file) in files.iter().enumerate() {
303 if i > 0 {
304 result.push('\n');
305 }
306 result.push_str(&file.header);
307 if !file.content.is_empty() {
308 result.push('\n');
309 result.push_str(&file.content);
310 }
311 }
312
313 result
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 fn test_config() -> CommitConfig {
321 CommitConfig::default()
322 }
323
324 #[test]
325 fn test_parse_diff_simple() {
326 let diff = r#"diff --git a/src/main.rs b/src/main.rs
327index 123..456 100644
328--- a/src/main.rs
329+++ b/src/main.rs
330@@ -1,3 +1,4 @@
331+use std::collections::HashMap;
332 fn main() {
333 println!("hello");
334 }"#;
335 let files = parse_diff(diff);
336 assert_eq!(files.len(), 1);
337 assert_eq!(files[0].filename, "src/main.rs");
338 assert_eq!(files[0].additions, 1);
339 assert_eq!(files[0].deletions, 0);
340 assert!(!files[0].is_binary);
341 assert!(files[0].header.contains("diff --git"));
342 assert!(files[0].content.contains("use std::collections::HashMap"));
343 }
344
345 #[test]
346 fn test_parse_diff_multi_file() {
347 let diff = r"diff --git a/src/lib.rs b/src/lib.rs
348index 111..222 100644
349--- a/src/lib.rs
350+++ b/src/lib.rs
351@@ -1,2 +1,3 @@
352+pub mod utils;
353 pub fn test() {}
354diff --git a/src/main.rs b/src/main.rs
355index 333..444 100644
356--- a/src/main.rs
357+++ b/src/main.rs
358@@ -1,1 +1,2 @@
359 fn main() {}
360+fn helper() {}";
361 let files = parse_diff(diff);
362 assert_eq!(files.len(), 2);
363 assert_eq!(files[0].filename, "src/lib.rs");
364 assert_eq!(files[1].filename, "src/main.rs");
365 assert_eq!(files[0].additions, 1);
366 assert_eq!(files[1].additions, 1);
367 }
368
369 #[test]
370 fn test_parse_diff_rename() {
371 let diff = r"diff --git a/old.rs b/new.rs
372similarity index 95%
373rename from old.rs
374rename to new.rs
375index 123..456 100644
376--- a/old.rs
377+++ b/new.rs
378@@ -1,2 +1,3 @@
379 fn test() {}
380+fn helper() {}";
381 let files = parse_diff(diff);
382 assert_eq!(files.len(), 1);
383 assert_eq!(files[0].filename, "new.rs");
384 assert!(files[0].header.contains("rename from"));
385 assert!(files[0].header.contains("rename to"));
386 assert_eq!(files[0].additions, 1);
387 }
388
389 #[test]
390 fn test_parse_diff_binary() {
391 let diff = r"diff --git a/image.png b/image.png
392index 123..456 100644
393Binary files a/image.png and b/image.png differ";
394 let files = parse_diff(diff);
395 assert_eq!(files.len(), 1);
396 assert_eq!(files[0].filename, "image.png");
397 assert!(files[0].is_binary);
398 assert!(files[0].header.contains("Binary files"));
399 }
400
401 #[test]
402 fn test_parse_diff_empty() {
403 let diff = "";
404 let files = parse_diff(diff);
405 assert_eq!(files.len(), 0);
406 }
407
408 #[test]
409 fn test_parse_diff_malformed_missing_hunks() {
410 let diff = r"diff --git a/src/main.rs b/src/main.rs
411index 123..456 100644
412--- a/src/main.rs
413+++ b/src/main.rs";
414 let files = parse_diff(diff);
415 assert_eq!(files.len(), 1);
416 assert_eq!(files[0].filename, "src/main.rs");
417 assert!(files[0].content.is_empty());
418 }
419
420 #[test]
421 fn test_parse_diff_new_file() {
422 let diff = r"diff --git a/new.rs b/new.rs
423new file mode 100644
424index 000..123 100644
425--- /dev/null
426+++ b/new.rs
427@@ -0,0 +1,2 @@
428+fn test() {}
429+fn main() {}";
430 let files = parse_diff(diff);
431 assert_eq!(files.len(), 1);
432 assert_eq!(files[0].filename, "new.rs");
433 assert!(files[0].header.contains("new file mode"));
434 assert_eq!(files[0].additions, 2);
435 }
436
437 #[test]
438 fn test_parse_diff_deleted_file() {
439 let diff = r"diff --git a/old.rs b/old.rs
440deleted file mode 100644
441index 123..000 100644
442--- a/old.rs
443+++ /dev/null
444@@ -1,2 +0,0 @@
445-fn test() {}
446-fn main() {}";
447 let files = parse_diff(diff);
448 assert_eq!(files.len(), 1);
449 assert_eq!(files[0].filename, "old.rs");
450 assert!(files[0].header.contains("deleted file mode"));
451 assert_eq!(files[0].deletions, 2);
452 }
453
454 #[test]
455 fn test_file_diff_size() {
456 let file = FileDiff {
457 filename: "test.rs".to_string(),
458 header: "header".to_string(),
459 content: "content".to_string(),
460 additions: 0,
461 deletions: 0,
462 is_binary: false,
463 };
464 assert_eq!(file.size(), 6 + 7); }
466
467 #[test]
468 fn test_file_diff_priority_source_files() {
469 let config = test_config();
470 let rs_file = FileDiff {
471 filename: "src/main.rs".to_string(),
472 header: String::new(),
473 content: String::new(),
474 additions: 0,
475 deletions: 0,
476 is_binary: false,
477 };
478 assert_eq!(rs_file.priority(&config), 100);
479
480 let py_file = FileDiff {
481 filename: "script.py".to_string(),
482 header: String::new(),
483 content: String::new(),
484 additions: 0,
485 deletions: 0,
486 is_binary: false,
487 };
488 assert_eq!(py_file.priority(&config), 100);
489
490 let js_file = FileDiff {
491 filename: "app.js".to_string(),
492 header: String::new(),
493 content: String::new(),
494 additions: 0,
495 deletions: 0,
496 is_binary: false,
497 };
498 assert_eq!(js_file.priority(&config), 100);
499 }
500
501 #[test]
502 fn test_file_diff_priority_binary() {
503 let config = test_config();
504 let binary = FileDiff {
505 filename: "image.png".to_string(),
506 header: String::new(),
507 content: String::new(),
508 additions: 0,
509 deletions: 0,
510 is_binary: true,
511 };
512 assert_eq!(binary.priority(&config), -100);
513 }
514
515 #[test]
516 fn test_file_diff_priority_test_files() {
517 let config = test_config();
518 let test_file = FileDiff {
519 filename: "src/test_utils.rs".to_string(),
520 header: String::new(),
521 content: String::new(),
522 additions: 0,
523 deletions: 0,
524 is_binary: false,
525 };
526 assert_eq!(test_file.priority(&config), 10);
527
528 let test_dir = FileDiff {
529 filename: "tests/integration_test.rs".to_string(),
530 header: String::new(),
531 content: String::new(),
532 additions: 0,
533 deletions: 0,
534 is_binary: false,
535 };
536 assert_eq!(test_dir.priority(&config), 10);
537 }
538
539 #[test]
540 fn test_file_diff_priority_low_priority_extensions() {
541 let config = test_config();
542 let md_file = FileDiff {
543 filename: "README.md".to_string(),
544 header: String::new(),
545 content: String::new(),
546 additions: 0,
547 deletions: 0,
548 is_binary: false,
549 };
550 assert_eq!(md_file.priority(&config), 20);
551
552 let toml_file = FileDiff {
553 filename: "config.toml".to_string(),
554 header: String::new(),
555 content: String::new(),
556 additions: 0,
557 deletions: 0,
558 is_binary: false,
559 };
560 assert_eq!(toml_file.priority(&config), 20);
561 }
562
563 #[test]
564 fn test_file_diff_priority_dependency_manifests() {
565 let config = test_config();
566
567 let cargo_toml = FileDiff {
568 filename: "Cargo.toml".to_string(),
569 header: String::new(),
570 content: String::new(),
571 additions: 0,
572 deletions: 0,
573 is_binary: false,
574 };
575 assert_eq!(cargo_toml.priority(&config), 70);
576
577 let package_json = FileDiff {
578 filename: "package.json".to_string(),
579 header: String::new(),
580 content: String::new(),
581 additions: 0,
582 deletions: 0,
583 is_binary: false,
584 };
585 assert_eq!(package_json.priority(&config), 70);
586
587 let go_mod = FileDiff {
588 filename: "go.mod".to_string(),
589 header: String::new(),
590 content: String::new(),
591 additions: 0,
592 deletions: 0,
593 is_binary: false,
594 };
595 assert_eq!(go_mod.priority(&config), 70);
596 }
597
598 #[test]
599 fn test_file_diff_priority_default() {
600 let config = test_config();
601 let other = FileDiff {
602 filename: "data.csv".to_string(),
603 header: String::new(),
604 content: String::new(),
605 additions: 0,
606 deletions: 0,
607 is_binary: false,
608 };
609 assert_eq!(other.priority(&config), 50);
610 }
611
612 #[test]
613 fn test_file_diff_truncate_small() {
614 let mut file = FileDiff {
615 filename: "test.rs".to_string(),
616 header: "header".to_string(),
617 content: "short content".to_string(),
618 additions: 0,
619 deletions: 0,
620 is_binary: false,
621 };
622 let original_size = file.size();
623 file.truncate(1000);
624 assert_eq!(file.size(), original_size);
625 assert_eq!(file.content, "short content");
626 }
627
628 #[test]
629 fn test_file_diff_truncate_large() {
630 let lines: Vec<String> = (0..100).map(|i| format!("line {i}")).collect();
631 let content = lines.join("\n");
632 let mut file = FileDiff {
633 filename: "test.rs".to_string(),
634 header: "header".to_string(),
635 content,
636 additions: 0,
637 deletions: 0,
638 is_binary: false,
639 };
640 file.truncate(500);
641 assert!(file.content.contains("... (truncated"));
642 assert!(file.content.contains("line 0")); assert!(file.content.contains("line 99")); }
645
646 #[test]
647 fn test_file_diff_truncate_preserves_context() {
648 let lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
649 let content = lines.join("\n");
650 let original_lines = content.lines().count();
651 let mut file = FileDiff {
652 filename: "test.rs".to_string(),
653 header: "header".to_string(),
654 content,
655 additions: 0,
656 deletions: 0,
657 is_binary: false,
658 };
659 file.truncate(300);
661 assert!(file.content.contains("line 0"));
663 assert!(file.content.contains("line 14"));
664 assert!(file.content.contains("line 40"));
665 assert!(file.content.contains("line 49"));
666 let truncated_lines = file.content.lines().count();
668 assert!(truncated_lines < original_lines, "Content should be truncated");
669 assert!(file.content.contains("truncated"), "Should have truncation message");
670 }
671
672 #[test]
673 fn test_file_diff_truncate_very_small_space() {
674 let mut file = FileDiff {
675 filename: "test.rs".to_string(),
676 header: "long header content here".to_string(),
677 content: "lots of content that needs to be truncated".to_string(),
678 additions: 0,
679 deletions: 0,
680 is_binary: false,
681 };
682 file.truncate(30);
683 assert_eq!(file.content, "... (truncated)");
684 }
685
686 #[test]
687 fn test_smart_truncate_diff_under_limit() {
688 let config = test_config();
689 let diff = r"diff --git a/src/main.rs b/src/main.rs
690index 123..456 100644
691--- a/src/main.rs
692+++ b/src/main.rs
693@@ -1,2 +1,3 @@
694+use std::io;
695 fn main() {}";
696 let result = smart_truncate_diff(diff, 10000, &config);
697 assert!(result.contains("use std::io"));
698 assert!(result.contains("src/main.rs"));
699 }
700
701 #[test]
702 fn test_smart_truncate_diff_over_limit() {
703 let config = test_config();
704 let lines: Vec<String> = (0..200).map(|i| format!("+line {i}")).collect();
705 let content = lines.join("\n");
706 let diff = format!(
707 "diff --git a/src/main.rs b/src/main.rs\nindex 123..456 100644\n--- a/src/main.rs\n+++ \
708 b/src/main.rs\n@@ -1,1 +1,200 @@\n{content}"
709 );
710 let result = smart_truncate_diff(&diff, 500, &config);
711 assert!(result.len() <= 600); assert!(result.contains("src/main.rs"));
713 }
714
715 #[test]
716 fn test_smart_truncate_diff_priority_allocation() {
717 let config = test_config();
718 let diff = r"diff --git a/src/lib.rs b/src/lib.rs
720index 111..222 100644
721--- a/src/lib.rs
722+++ b/src/lib.rs
723@@ -1,1 +1,50 @@
724+pub fn important_function() {}
725+pub fn another_function() {}
726+pub fn yet_another() {}
727diff --git a/README.md b/README.md
728index 333..444 100644
729--- a/README.md
730+++ b/README.md
731@@ -1,1 +1,50 @@
732+# Documentation
733+More docs here";
734 let result = smart_truncate_diff(diff, 300, &config);
735 assert!(result.contains("src/lib.rs"));
737 assert!(result.contains("important_function") || result.contains("truncated"));
738 }
739
740 #[test]
741 fn test_smart_truncate_diff_binary_excluded() {
742 let config = test_config();
743 let diff = r"diff --git a/image.png b/image.png
744index 123..456 100644
745Binary files a/image.png and b/image.png differ
746diff --git a/src/main.rs b/src/main.rs
747index 789..abc 100644
748--- a/src/main.rs
749+++ b/src/main.rs
750@@ -1,1 +1,2 @@
751 fn main() {}
752+fn helper() {}";
753 let result = smart_truncate_diff(diff, 10000, &config);
754 assert!(result.contains("src/main.rs"));
755 assert!(result.contains("image.png"));
756 assert!(result.contains("Binary files"));
757 }
758
759 #[test]
760 fn test_smart_truncate_diff_excluded_files() {
761 let config = test_config();
762 let diff = r"diff --git a/Cargo.lock b/Cargo.lock
763index 123..456 100644
764--- a/Cargo.lock
765+++ b/Cargo.lock
766@@ -1,1 +1,100 @@
767+lots of lock file content
768diff --git a/src/main.rs b/src/main.rs
769index 789..abc 100644
770--- a/src/main.rs
771+++ b/src/main.rs
772@@ -1,1 +1,2 @@
773 fn main() {}
774+fn helper() {}";
775 let result = smart_truncate_diff(diff, 10000, &config);
776 assert!(!result.contains("Cargo.lock"));
777 assert!(result.contains("src/main.rs"));
778 }
779
780 #[test]
781 fn test_smart_truncate_diff_all_files_excluded() {
782 let config = test_config();
783 let diff = r"diff --git a/Cargo.lock b/Cargo.lock
784index 123..456 100644
785--- a/Cargo.lock
786+++ b/Cargo.lock
787@@ -1,1 +1,2 @@
788+dependency update";
789 let result = smart_truncate_diff(diff, 10000, &config);
790 assert!(result.contains("No relevant files"));
791 }
792
793 #[test]
794 fn test_smart_truncate_diff_header_preservation() {
795 let config = test_config();
796 let lines: Vec<String> = (0..100).map(|i| format!("+line {i}")).collect();
797 let content = lines.join("\n");
798 let diff = format!(
799 "diff --git a/src/a.rs b/src/a.rs\nindex 111..222 100644\n--- a/src/a.rs\n+++ \
800 b/src/a.rs\n@@ -1,1 +1,100 @@\n{content}\ndiff --git a/src/b.rs b/src/b.rs\nindex \
801 333..444 100644\n--- a/src/b.rs\n+++ b/src/b.rs\n@@ -1,1 +1,100 @@\n{content}"
802 );
803 let result = smart_truncate_diff(&diff, 600, &config);
804 assert!(result.contains("src/a.rs"));
806 assert!(result.contains("src/b.rs"));
807 }
808
809 #[test]
810 fn test_reconstruct_diff_single_file() {
811 let files = vec![FileDiff {
812 filename: "test.rs".to_string(),
813 header: "diff --git a/test.rs b/test.rs".to_string(),
814 content: "+new line".to_string(),
815 additions: 1,
816 deletions: 0,
817 is_binary: false,
818 }];
819 let result = reconstruct_diff(&files);
820 assert_eq!(result, "diff --git a/test.rs b/test.rs\n+new line");
821 }
822
823 #[test]
824 fn test_reconstruct_diff_multiple_files() {
825 let files = vec![
826 FileDiff {
827 filename: "a.rs".to_string(),
828 header: "diff --git a/a.rs b/a.rs".to_string(),
829 content: "+line a".to_string(),
830 additions: 1,
831 deletions: 0,
832 is_binary: false,
833 },
834 FileDiff {
835 filename: "b.rs".to_string(),
836 header: "diff --git a/b.rs b/b.rs".to_string(),
837 content: "+line b".to_string(),
838 additions: 1,
839 deletions: 0,
840 is_binary: false,
841 },
842 ];
843 let result = reconstruct_diff(&files);
844 assert!(result.contains("a.rs"));
845 assert!(result.contains("b.rs"));
846 assert!(result.contains("+line a"));
847 assert!(result.contains("+line b"));
848 }
849
850 #[test]
851 fn test_reconstruct_diff_empty_content() {
852 let files = vec![FileDiff {
853 filename: "test.rs".to_string(),
854 header: "diff --git a/test.rs b/test.rs".to_string(),
855 content: String::new(),
856 additions: 0,
857 deletions: 0,
858 is_binary: false,
859 }];
860 let result = reconstruct_diff(&files);
861 assert_eq!(result, "diff --git a/test.rs b/test.rs");
862 }
863
864 #[test]
865 fn test_reconstruct_diff_empty_vec() {
866 let files: Vec<FileDiff> = vec![];
867 let result = reconstruct_diff(&files);
868 assert_eq!(result, "");
869 }
870}