1use std::path::Path;
5
6use similar::{ChangeTag, TextDiff};
7
8#[derive(Debug, Default)]
10pub struct DiffStats {
11 pub files_changed: usize,
12 pub lines_added: usize,
13 pub lines_removed: usize,
14}
15
16impl DiffStats {
17 pub const fn add(&mut self, other: &Self) {
18 self.files_changed += other.files_changed;
19 self.lines_added += other.lines_added;
20 self.lines_removed += other.lines_removed;
21 }
22
23 pub fn print_summary(&self) {
24 println!("\nSummary:");
25 println!("Files changed: {}", self.files_changed);
26 println!("Lines added: {}", self.lines_added);
27 println!("Lines removed: {}", self.lines_removed);
28 }
29}
30
31pub fn generate_unified_diff(
41 path: &Path,
42 original: &str,
43 modified: &str,
44 context_lines: usize,
45) -> (String, DiffStats) {
46 let diff = TextDiff::from_lines(original, modified);
47
48 let mut output = String::new();
49 let mut stats = DiffStats::default();
50
51 let path_str = path.display().to_string();
53 output.push_str(&format!("--- {}\n", path_str));
54 output.push_str(&format!("+++ {}\n", path_str));
55
56 for change in diff.iter_all_changes() {
58 match change.tag() {
59 ChangeTag::Insert => stats.lines_added += 1,
60 ChangeTag::Delete => stats.lines_removed += 1,
61 ChangeTag::Equal => {}
62 }
63 }
64
65 let unified = diff
67 .unified_diff()
68 .context_radius(context_lines)
69 .to_string();
70
71 output.push_str(&unified);
72
73 if stats.lines_added > 0 || stats.lines_removed > 0 {
74 stats.files_changed = 1;
75 }
76
77 (output, stats)
78}
79
80pub fn print_diff(path: &Path, original: &str, modified: &str) -> DiffStats {
91 let (diff_output, stats) = generate_unified_diff(path, original, modified, 3);
92
93 if stats.files_changed > 0 {
95 print!("{}", diff_output);
96 }
97
98 stats
99}
100
101pub fn print_summary_diff(path: &Path, original: &str, modified: &str) -> DiffStats {
113 use similar::{ChangeTag, TextDiff};
114
115 let diff = TextDiff::from_lines(original, modified);
116 let mut stats = DiffStats::default();
117 let mut changes = Vec::new();
118
119 let mut current_line = 1;
121 for change in diff.iter_all_changes() {
122 match change.tag() {
123 ChangeTag::Delete => {
124 changes.push((current_line, '-', change.to_string()));
125 stats.lines_removed += 1;
126 current_line += 1;
127 }
128 ChangeTag::Insert => {
129 changes.push((current_line, '+', change.to_string()));
130 stats.lines_added += 1;
131 }
132 ChangeTag::Equal => {
133 current_line += 1;
134 }
135 }
136 }
137
138 if !changes.is_empty() {
139 stats.files_changed = 1;
140
141 println!("\nš Changes for {}:\n", path.display());
142
143 let mut i = 0;
145 while i < changes.len() {
146 let (line_num, tag, content) = &changes[i];
147
148 print!("{:>5} | {}{}", line_num, tag, content);
150
151 i += 1;
152 }
153
154 println!("\nā {} changes in {}", changes.len(), path.display());
155 }
156
157 stats
158}
159
160#[cfg(test)]
161mod tests {
162 use std::path::PathBuf;
163
164 use super::*;
165
166 #[test]
167 fn test_generate_unified_diff() {
168 let original = "pub struct User {\n id: u64,\n name: String,\n}\n";
169 let modified = "pub struct User {\n id: u64,\n age: u32,\n name: String,\n}\n";
170 let path = PathBuf::from("src/user.rs");
171
172 let (diff, stats) = generate_unified_diff(&path, original, modified, 3);
173
174 assert!(diff.contains("--- src/user.rs"));
176 assert!(diff.contains("+++ src/user.rs"));
177
178 assert!(diff.contains("+ age: u32,"));
180
181 assert_eq!(stats.files_changed, 1);
183 assert_eq!(stats.lines_added, 1);
184 assert_eq!(stats.lines_removed, 0);
185 }
186
187 #[test]
188 fn test_generate_unified_diff_no_changes() {
189 let content = "pub struct User {\n id: u64,\n}\n";
190 let path = PathBuf::from("src/user.rs");
191
192 let (diff, stats) = generate_unified_diff(&path, content, content, 3);
193
194 assert!(diff.contains("--- src/user.rs"));
196 assert!(diff.contains("+++ src/user.rs"));
197
198 assert_eq!(stats.files_changed, 0);
200 assert_eq!(stats.lines_added, 0);
201 assert_eq!(stats.lines_removed, 0);
202 }
203
204 #[test]
205 fn test_generate_unified_diff_with_removal() {
206 let original =
207 "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}