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
337pub fn truncate_diff_by_lines(diff: &str, max_lines: usize, config: &CommitConfig) -> String {
343 let files = parse_diff(diff);
344
345 let total_lines: usize = files
347 .iter()
348 .map(|f| f.header.lines().count() + f.content.lines().count())
349 .sum();
350
351 if total_lines <= max_lines {
352 return diff.to_string();
353 }
354
355 let total_priority: i32 = files.iter().map(|f| f.priority(config).max(1)).sum();
357
358 let mut result = String::with_capacity(diff.len());
359
360 for file in &files {
361 result.push_str(&file.header);
363 if !file.header.ends_with('\n') {
364 result.push('\n');
365 }
366
367 let content_lines: Vec<&str> = file.content.lines().collect();
368 let priority = file.priority(config).max(1);
369
370 #[allow(clippy::cast_sign_loss, reason = "priority and total are positive")]
372 #[allow(clippy::cast_possible_truncation, reason = "line count fits in usize")]
373 let allocated = ((max_lines as f64) * (priority as f64) / (total_priority as f64)) as usize;
374 let allocated = allocated.max(5); if content_lines.len() <= allocated {
377 result.push_str(&file.content);
378 if !file.content.ends_with('\n') {
379 result.push('\n');
380 }
381 } else {
382 let keep_start = allocated / 2;
384 let keep_end = allocated - keep_start;
385 let omitted = content_lines.len() - keep_start - keep_end;
386
387 for line in &content_lines[..keep_start] {
388 result.push_str(line);
389 result.push('\n');
390 }
391
392 use std::fmt::Write;
393 writeln!(&mut result, "[... {omitted} lines omitted ...]")
394 .expect("writing to String is infallible");
395
396 for line in &content_lines[content_lines.len() - keep_end..] {
397 result.push_str(line);
398 result.push('\n');
399 }
400 }
401 }
402
403 result
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 fn test_config() -> CommitConfig {
411 CommitConfig::default()
412 }
413
414 fn test_counter() -> TokenCounter {
415 TokenCounter::new("http://localhost:4000", None, "claude-sonnet-4.5")
416 }
417
418 #[test]
419 fn test_parse_diff_simple() {
420 let diff = r#"diff --git a/src/main.rs b/src/main.rs
421index 123..456 100644
422--- a/src/main.rs
423+++ b/src/main.rs
424@@ -1,3 +1,4 @@
425+use std::collections::HashMap;
426 fn main() {
427 println!("hello");
428 }"#;
429 let files = parse_diff(diff);
430 assert_eq!(files.len(), 1);
431 assert_eq!(files[0].filename, "src/main.rs");
432 assert_eq!(files[0].additions, 1);
433 assert_eq!(files[0].deletions, 0);
434 assert!(!files[0].is_binary);
435 assert!(files[0].header.contains("diff --git"));
436 assert!(files[0].content.contains("use std::collections::HashMap"));
437 }
438
439 #[test]
440 fn test_parse_diff_multi_file() {
441 let diff = r"diff --git a/src/lib.rs b/src/lib.rs
442index 111..222 100644
443--- a/src/lib.rs
444+++ b/src/lib.rs
445@@ -1,2 +1,3 @@
446+pub mod utils;
447 pub fn test() {}
448diff --git a/src/main.rs b/src/main.rs
449index 333..444 100644
450--- a/src/main.rs
451+++ b/src/main.rs
452@@ -1,1 +1,2 @@
453 fn main() {}
454+fn helper() {}";
455 let files = parse_diff(diff);
456 assert_eq!(files.len(), 2);
457 assert_eq!(files[0].filename, "src/lib.rs");
458 assert_eq!(files[1].filename, "src/main.rs");
459 assert_eq!(files[0].additions, 1);
460 assert_eq!(files[1].additions, 1);
461 }
462
463 #[test]
464 fn test_parse_diff_rename() {
465 let diff = r"diff --git a/old.rs b/new.rs
466similarity index 95%
467rename from old.rs
468rename to new.rs
469index 123..456 100644
470--- a/old.rs
471+++ b/new.rs
472@@ -1,2 +1,3 @@
473 fn test() {}
474+fn helper() {}";
475 let files = parse_diff(diff);
476 assert_eq!(files.len(), 1);
477 assert_eq!(files[0].filename, "new.rs");
478 assert!(files[0].header.contains("rename from"));
479 assert!(files[0].header.contains("rename to"));
480 assert_eq!(files[0].additions, 1);
481 }
482
483 #[test]
484 fn test_parse_diff_binary() {
485 let diff = r"diff --git a/image.png b/image.png
486index 123..456 100644
487Binary files a/image.png and b/image.png differ";
488 let files = parse_diff(diff);
489 assert_eq!(files.len(), 1);
490 assert_eq!(files[0].filename, "image.png");
491 assert!(files[0].is_binary);
492 assert!(files[0].header.contains("Binary files"));
493 }
494
495 #[test]
496 fn test_parse_diff_empty() {
497 let diff = "";
498 let files = parse_diff(diff);
499 assert_eq!(files.len(), 0);
500 }
501
502 #[test]
503 fn test_parse_diff_malformed_missing_hunks() {
504 let diff = r"diff --git a/src/main.rs b/src/main.rs
505index 123..456 100644
506--- a/src/main.rs
507+++ b/src/main.rs";
508 let files = parse_diff(diff);
509 assert_eq!(files.len(), 1);
510 assert_eq!(files[0].filename, "src/main.rs");
511 assert!(files[0].content.is_empty());
512 }
513
514 #[test]
515 fn test_parse_diff_new_file() {
516 let diff = r"diff --git a/new.rs b/new.rs
517new file mode 100644
518index 000..123 100644
519--- /dev/null
520+++ b/new.rs
521@@ -0,0 +1,2 @@
522+fn test() {}
523+fn main() {}";
524 let files = parse_diff(diff);
525 assert_eq!(files.len(), 1);
526 assert_eq!(files[0].filename, "new.rs");
527 assert!(files[0].header.contains("new file mode"));
528 assert_eq!(files[0].additions, 2);
529 }
530
531 #[test]
532 fn test_parse_diff_deleted_file() {
533 let diff = r"diff --git a/old.rs b/old.rs
534deleted file mode 100644
535index 123..000 100644
536--- a/old.rs
537+++ /dev/null
538@@ -1,2 +0,0 @@
539-fn test() {}
540-fn main() {}";
541 let files = parse_diff(diff);
542 assert_eq!(files.len(), 1);
543 assert_eq!(files[0].filename, "old.rs");
544 assert!(files[0].header.contains("deleted file mode"));
545 assert_eq!(files[0].deletions, 2);
546 }
547
548 #[test]
549 fn test_file_diff_size() {
550 let file = FileDiff {
551 filename: "test.rs".to_string(),
552 header: "header".to_string(),
553 content: "content".to_string(),
554 additions: 0,
555 deletions: 0,
556 is_binary: false,
557 };
558 assert_eq!(file.size(), 6 + 7); }
560
561 #[test]
562 fn test_file_diff_priority_source_files() {
563 let config = test_config();
564 let rs_file = FileDiff {
565 filename: "src/main.rs".to_string(),
566 header: String::new(),
567 content: String::new(),
568 additions: 0,
569 deletions: 0,
570 is_binary: false,
571 };
572 assert_eq!(rs_file.priority(&config), 100);
573
574 let py_file = FileDiff {
575 filename: "script.py".to_string(),
576 header: String::new(),
577 content: String::new(),
578 additions: 0,
579 deletions: 0,
580 is_binary: false,
581 };
582 assert_eq!(py_file.priority(&config), 100);
583
584 let js_file = FileDiff {
585 filename: "app.js".to_string(),
586 header: String::new(),
587 content: String::new(),
588 additions: 0,
589 deletions: 0,
590 is_binary: false,
591 };
592 assert_eq!(js_file.priority(&config), 100);
593 }
594
595 #[test]
596 fn test_file_diff_priority_binary() {
597 let config = test_config();
598 let binary = FileDiff {
599 filename: "image.png".to_string(),
600 header: String::new(),
601 content: String::new(),
602 additions: 0,
603 deletions: 0,
604 is_binary: true,
605 };
606 assert_eq!(binary.priority(&config), -100);
607 }
608
609 #[test]
610 fn test_file_diff_priority_test_files() {
611 let config = test_config();
612 let test_file = FileDiff {
613 filename: "src/test_utils.rs".to_string(),
614 header: String::new(),
615 content: String::new(),
616 additions: 0,
617 deletions: 0,
618 is_binary: false,
619 };
620 assert_eq!(test_file.priority(&config), 10);
621
622 let test_dir = FileDiff {
623 filename: "tests/integration_test.rs".to_string(),
624 header: String::new(),
625 content: String::new(),
626 additions: 0,
627 deletions: 0,
628 is_binary: false,
629 };
630 assert_eq!(test_dir.priority(&config), 10);
631 }
632
633 #[test]
634 fn test_file_diff_priority_low_priority_extensions() {
635 let config = test_config();
636 let md_file = FileDiff {
637 filename: "README.md".to_string(),
638 header: String::new(),
639 content: String::new(),
640 additions: 0,
641 deletions: 0,
642 is_binary: false,
643 };
644 assert_eq!(md_file.priority(&config), 20);
645
646 let toml_file = FileDiff {
647 filename: "config.toml".to_string(),
648 header: String::new(),
649 content: String::new(),
650 additions: 0,
651 deletions: 0,
652 is_binary: false,
653 };
654 assert_eq!(toml_file.priority(&config), 20);
655 }
656
657 #[test]
658 fn test_file_diff_priority_dependency_manifests() {
659 let config = test_config();
660
661 let cargo_toml = FileDiff {
662 filename: "Cargo.toml".to_string(),
663 header: String::new(),
664 content: String::new(),
665 additions: 0,
666 deletions: 0,
667 is_binary: false,
668 };
669 assert_eq!(cargo_toml.priority(&config), 70);
670
671 let package_json = FileDiff {
672 filename: "package.json".to_string(),
673 header: String::new(),
674 content: String::new(),
675 additions: 0,
676 deletions: 0,
677 is_binary: false,
678 };
679 assert_eq!(package_json.priority(&config), 70);
680
681 let go_mod = FileDiff {
682 filename: "go.mod".to_string(),
683 header: String::new(),
684 content: String::new(),
685 additions: 0,
686 deletions: 0,
687 is_binary: false,
688 };
689 assert_eq!(go_mod.priority(&config), 70);
690 }
691
692 #[test]
693 fn test_file_diff_priority_default() {
694 let config = test_config();
695 let other = FileDiff {
696 filename: "data.csv".to_string(),
697 header: String::new(),
698 content: String::new(),
699 additions: 0,
700 deletions: 0,
701 is_binary: false,
702 };
703 assert_eq!(other.priority(&config), 50);
704 }
705
706 #[test]
707 fn test_file_diff_truncate_small() {
708 let mut file = FileDiff {
709 filename: "test.rs".to_string(),
710 header: "header".to_string(),
711 content: "short content".to_string(),
712 additions: 0,
713 deletions: 0,
714 is_binary: false,
715 };
716 let original_size = file.size();
717 file.truncate(1000);
718 assert_eq!(file.size(), original_size);
719 assert_eq!(file.content, "short content");
720 }
721
722 #[test]
723 fn test_file_diff_truncate_large() {
724 let lines: Vec<String> = (0..100).map(|i| format!("line {i}")).collect();
725 let content = lines.join("\n");
726 let mut file = FileDiff {
727 filename: "test.rs".to_string(),
728 header: "header".to_string(),
729 content,
730 additions: 0,
731 deletions: 0,
732 is_binary: false,
733 };
734 file.truncate(500);
735 assert!(file.content.contains("... (truncated"));
736 assert!(file.content.contains("line 0")); assert!(file.content.contains("line 99")); }
739
740 #[test]
741 fn test_file_diff_truncate_preserves_context() {
742 let lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
743 let content = lines.join("\n");
744 let original_lines = content.lines().count();
745 let mut file = FileDiff {
746 filename: "test.rs".to_string(),
747 header: "header".to_string(),
748 content,
749 additions: 0,
750 deletions: 0,
751 is_binary: false,
752 };
753 file.truncate(300);
755 assert!(file.content.contains("line 0"));
757 assert!(file.content.contains("line 14"));
758 assert!(file.content.contains("line 40"));
759 assert!(file.content.contains("line 49"));
760 let truncated_lines = file.content.lines().count();
762 assert!(truncated_lines < original_lines, "Content should be truncated");
763 assert!(file.content.contains("truncated"), "Should have truncation message");
764 }
765
766 #[test]
767 fn test_file_diff_truncate_very_small_space() {
768 let mut file = FileDiff {
769 filename: "test.rs".to_string(),
770 header: "long header content here".to_string(),
771 content: "lots of content that needs to be truncated".to_string(),
772 additions: 0,
773 deletions: 0,
774 is_binary: false,
775 };
776 file.truncate(30);
777 assert_eq!(file.content, "... (truncated)");
778 }
779
780 #[test]
781 fn test_smart_truncate_diff_under_limit() {
782 let config = test_config();
783 let counter = test_counter();
784 let diff = r"diff --git a/src/main.rs b/src/main.rs
785index 123..456 100644
786--- a/src/main.rs
787+++ b/src/main.rs
788@@ -1,2 +1,3 @@
789+use std::io;
790 fn main() {}";
791 let result = smart_truncate_diff(diff, 10000, &config, &counter);
792 assert!(result.contains("use std::io"));
793 assert!(result.contains("src/main.rs"));
794 }
795
796 #[test]
797 fn test_smart_truncate_diff_over_limit() {
798 let config = test_config();
799 let counter = test_counter();
800 let lines: Vec<String> = (0..200).map(|i| format!("+line {i}")).collect();
801 let content = lines.join("\n");
802 let diff = format!(
803 "diff --git a/src/main.rs b/src/main.rs\nindex 123..456 100644\n--- a/src/main.rs\n+++ \
804 b/src/main.rs\n@@ -1,1 +1,200 @@\n{content}"
805 );
806 let result = smart_truncate_diff(&diff, 500, &config, &counter);
807 assert!(result.len() <= 600); assert!(result.contains("src/main.rs"));
809 }
810
811 #[test]
812 fn test_smart_truncate_diff_priority_allocation() {
813 let config = test_config();
814 let counter = test_counter();
815 let diff = r"diff --git a/src/lib.rs b/src/lib.rs
817index 111..222 100644
818--- a/src/lib.rs
819+++ b/src/lib.rs
820@@ -1,1 +1,50 @@
821+pub fn important_function() {}
822+pub fn another_function() {}
823+pub fn yet_another() {}
824diff --git a/README.md b/README.md
825index 333..444 100644
826--- a/README.md
827+++ b/README.md
828@@ -1,1 +1,50 @@
829+# Documentation
830+More docs here";
831 let result = smart_truncate_diff(diff, 300, &config, &counter);
832 assert!(result.contains("src/lib.rs"));
834 assert!(result.contains("important_function") || result.contains("truncated"));
835 }
836
837 #[test]
838 fn test_smart_truncate_diff_binary_excluded() {
839 let config = test_config();
840 let counter = test_counter();
841 let diff = r"diff --git a/image.png b/image.png
842index 123..456 100644
843Binary files a/image.png and b/image.png differ
844diff --git a/src/main.rs b/src/main.rs
845index 789..abc 100644
846--- a/src/main.rs
847+++ b/src/main.rs
848@@ -1,1 +1,2 @@
849 fn main() {}
850+fn helper() {}";
851 let result = smart_truncate_diff(diff, 10000, &config, &counter);
852 assert!(result.contains("src/main.rs"));
853 assert!(result.contains("image.png"));
854 assert!(result.contains("Binary files"));
855 }
856
857 #[test]
858 fn test_smart_truncate_diff_excluded_files() {
859 let config = test_config();
860 let counter = test_counter();
861 let diff = r"diff --git a/Cargo.lock b/Cargo.lock
862index 123..456 100644
863--- a/Cargo.lock
864+++ b/Cargo.lock
865@@ -1,1 +1,100 @@
866+lots of lock file content
867diff --git a/src/main.rs b/src/main.rs
868index 789..abc 100644
869--- a/src/main.rs
870+++ b/src/main.rs
871@@ -1,1 +1,2 @@
872 fn main() {}
873+fn helper() {}";
874 let result = smart_truncate_diff(diff, 10000, &config, &counter);
875 assert!(!result.contains("Cargo.lock"));
876 assert!(result.contains("src/main.rs"));
877 }
878
879 #[test]
880 fn test_smart_truncate_diff_all_files_excluded() {
881 let config = test_config();
882 let counter = test_counter();
883 let diff = r"diff --git a/Cargo.lock b/Cargo.lock
884index 123..456 100644
885--- a/Cargo.lock
886+++ b/Cargo.lock
887@@ -1,1 +1,2 @@
888+dependency update";
889 let result = smart_truncate_diff(diff, 10000, &config, &counter);
890 assert!(result.contains("No relevant files"));
891 }
892
893 #[test]
894 fn test_smart_truncate_diff_header_preservation() {
895 let config = test_config();
896 let counter = test_counter();
897 let lines: Vec<String> = (0..100).map(|i| format!("+line {i}")).collect();
898 let content = lines.join("\n");
899 let diff = format!(
900 "diff --git a/src/a.rs b/src/a.rs\nindex 111..222 100644\n--- a/src/a.rs\n+++ \
901 b/src/a.rs\n@@ -1,1 +1,100 @@\n{content}\ndiff --git a/src/b.rs b/src/b.rs\nindex \
902 333..444 100644\n--- a/src/b.rs\n+++ b/src/b.rs\n@@ -1,1 +1,100 @@\n{content}"
903 );
904 let result = smart_truncate_diff(&diff, 600, &config, &counter);
905 assert!(result.contains("src/a.rs"));
907 assert!(result.contains("src/b.rs"));
908 }
909
910 #[test]
911 fn test_reconstruct_diff_single_file() {
912 let files = vec![FileDiff {
913 filename: "test.rs".to_string(),
914 header: "diff --git a/test.rs b/test.rs".to_string(),
915 content: "+new line".to_string(),
916 additions: 1,
917 deletions: 0,
918 is_binary: false,
919 }];
920 let result = reconstruct_diff(&files);
921 assert_eq!(result, "diff --git a/test.rs b/test.rs\n+new line");
922 }
923
924 #[test]
925 fn test_reconstruct_diff_multiple_files() {
926 let files = vec![
927 FileDiff {
928 filename: "a.rs".to_string(),
929 header: "diff --git a/a.rs b/a.rs".to_string(),
930 content: "+line a".to_string(),
931 additions: 1,
932 deletions: 0,
933 is_binary: false,
934 },
935 FileDiff {
936 filename: "b.rs".to_string(),
937 header: "diff --git a/b.rs b/b.rs".to_string(),
938 content: "+line b".to_string(),
939 additions: 1,
940 deletions: 0,
941 is_binary: false,
942 },
943 ];
944 let result = reconstruct_diff(&files);
945 assert!(result.contains("a.rs"));
946 assert!(result.contains("b.rs"));
947 assert!(result.contains("+line a"));
948 assert!(result.contains("+line b"));
949 }
950
951 #[test]
952 fn test_reconstruct_diff_empty_content() {
953 let files = vec![FileDiff {
954 filename: "test.rs".to_string(),
955 header: "diff --git a/test.rs b/test.rs".to_string(),
956 content: String::new(),
957 additions: 0,
958 deletions: 0,
959 is_binary: false,
960 }];
961 let result = reconstruct_diff(&files);
962 assert_eq!(result, "diff --git a/test.rs b/test.rs");
963 }
964
965 #[test]
966 fn test_reconstruct_diff_empty_vec() {
967 let files: Vec<FileDiff> = vec![];
968 let result = reconstruct_diff(&files);
969 assert_eq!(result, "");
970 }
971}