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