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