1use std::path::Path;
5use similar::{ChangeTag, TextDiff};
6
7#[derive(Debug, Default)]
9pub struct DiffStats {
10 pub files_changed: usize,
11 pub lines_added: usize,
12 pub lines_removed: usize,
13}
14
15impl DiffStats {
16 pub fn add(&mut self, other: &DiffStats) {
17 self.files_changed += other.files_changed;
18 self.lines_added += other.lines_added;
19 self.lines_removed += other.lines_removed;
20 }
21
22 pub fn print_summary(&self) {
23 println!("\nSummary:");
24 println!("Files changed: {}", self.files_changed);
25 println!("Lines added: {}", self.lines_added);
26 println!("Lines removed: {}", self.lines_removed);
27 }
28}
29
30pub fn generate_unified_diff(
40 path: &Path,
41 original: &str,
42 modified: &str,
43 context_lines: usize,
44) -> (String, DiffStats) {
45 let diff = TextDiff::from_lines(original, modified);
46
47 let mut output = String::new();
48 let mut stats = DiffStats::default();
49
50 let path_str = path.display().to_string();
52 output.push_str(&format!("--- {}\n", path_str));
53 output.push_str(&format!("+++ {}\n", path_str));
54
55 for change in diff.iter_all_changes() {
57 match change.tag() {
58 ChangeTag::Insert => stats.lines_added += 1,
59 ChangeTag::Delete => stats.lines_removed += 1,
60 ChangeTag::Equal => {}
61 }
62 }
63
64 let unified = diff.unified_diff()
66 .context_radius(context_lines)
67 .to_string();
68
69 output.push_str(&unified);
70
71 if stats.lines_added > 0 || stats.lines_removed > 0 {
72 stats.files_changed = 1;
73 }
74
75 (output, stats)
76}
77
78pub fn print_diff(path: &Path, original: &str, modified: &str) -> DiffStats {
89 let (diff_output, stats) = generate_unified_diff(path, original, modified, 3);
90
91 if stats.files_changed > 0 {
93 print!("{}", diff_output);
94 }
95
96 stats
97}
98
99pub fn print_summary_diff(path: &Path, original: &str, modified: &str) -> DiffStats {
111 use similar::{ChangeTag, TextDiff};
112
113 let diff = TextDiff::from_lines(original, modified);
114 let mut stats = DiffStats::default();
115 let mut changes = Vec::new();
116
117 let mut current_line = 1;
119 for change in diff.iter_all_changes() {
120 match change.tag() {
121 ChangeTag::Delete => {
122 changes.push((current_line, '-', change.to_string()));
123 stats.lines_removed += 1;
124 current_line += 1;
125 }
126 ChangeTag::Insert => {
127 changes.push((current_line, '+', change.to_string()));
128 stats.lines_added += 1;
129 }
130 ChangeTag::Equal => {
131 current_line += 1;
132 }
133 }
134 }
135
136 if !changes.is_empty() {
137 stats.files_changed = 1;
138
139 println!("\nš Changes for {}:\n", path.display());
140
141 let mut i = 0;
143 while i < changes.len() {
144 let (line_num, tag, content) = &changes[i];
145
146 if *tag == '-' {
148 print!("{:>5} | {}{}", line_num, tag, content);
149 } else {
150 print!("{:>5} | {}{}", line_num, tag, content);
151 }
152
153 i += 1;
154 }
155
156 println!("\nā {} changes in {}", changes.len(), path.display());
157 }
158
159 stats
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::path::PathBuf;
166
167 #[test]
168 fn test_generate_unified_diff() {
169 let original = "pub struct User {\n id: u64,\n name: String,\n}\n";
170 let modified = "pub struct User {\n id: u64,\n age: u32,\n name: String,\n}\n";
171 let path = PathBuf::from("src/user.rs");
172
173 let (diff, stats) = generate_unified_diff(&path, original, modified, 3);
174
175 assert!(diff.contains("--- src/user.rs"));
177 assert!(diff.contains("+++ src/user.rs"));
178
179 assert!(diff.contains("+ age: u32,"));
181
182 assert_eq!(stats.files_changed, 1);
184 assert_eq!(stats.lines_added, 1);
185 assert_eq!(stats.lines_removed, 0);
186 }
187
188 #[test]
189 fn test_generate_unified_diff_no_changes() {
190 let content = "pub struct User {\n id: u64,\n}\n";
191 let path = PathBuf::from("src/user.rs");
192
193 let (diff, stats) = generate_unified_diff(&path, content, content, 3);
194
195 assert!(diff.contains("--- src/user.rs"));
197 assert!(diff.contains("+++ src/user.rs"));
198
199 assert_eq!(stats.files_changed, 0);
201 assert_eq!(stats.lines_added, 0);
202 assert_eq!(stats.lines_removed, 0);
203 }
204
205 #[test]
206 fn test_generate_unified_diff_with_removal() {
207 let original = "pub struct User {\n id: u64,\n name: String,\n email: String,\n}\n";
208 let modified = "pub struct User {\n id: u64,\n name: String,\n}\n";
209 let path = PathBuf::from("src/user.rs");
210
211 let (diff, stats) = generate_unified_diff(&path, original, modified, 3);
212
213 assert!(diff.contains("- email: String,"));
215
216 assert_eq!(stats.files_changed, 1);
218 assert_eq!(stats.lines_added, 0);
219 assert_eq!(stats.lines_removed, 1);
220 }
221
222 #[test]
223 fn test_diff_stats_add() {
224 let mut stats1 = DiffStats {
225 files_changed: 1,
226 lines_added: 5,
227 lines_removed: 2,
228 };
229
230 let stats2 = DiffStats {
231 files_changed: 2,
232 lines_added: 3,
233 lines_removed: 1,
234 };
235
236 stats1.add(&stats2);
237
238 assert_eq!(stats1.files_changed, 3);
239 assert_eq!(stats1.lines_added, 8);
240 assert_eq!(stats1.lines_removed, 3);
241 }
242
243 #[test]
244 fn test_print_diff_returns_stats() {
245 let original = "line1\nline2\n";
246 let modified = "line1\nline2\nline3\n";
247 let path = PathBuf::from("test.txt");
248
249 let stats = print_diff(&path, original, modified);
250
251 assert_eq!(stats.files_changed, 1);
252 assert_eq!(stats.lines_added, 1);
253 assert_eq!(stats.lines_removed, 0);
254 }
255}