rs_hack/
diff.rs

1use std::path::Path;
2use similar::{ChangeTag, TextDiff};
3
4/// Represents statistics about a diff
5#[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
27/// Generate a unified diff between original and modified content
28///
29/// Returns the unified diff string and statistics about the changes.
30///
31/// # Arguments
32/// * `path` - The file path (used in diff headers)
33/// * `original` - The original file content
34/// * `modified` - The modified file content
35/// * `context_lines` - Number of context lines to show (default is 3)
36pub 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    // Generate unified diff format headers
48    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    // Count changes for statistics
53    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    // Generate the unified diff with context
62    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
75/// Print a unified diff to stdout
76///
77/// This is a convenience function that generates and prints a diff.
78///
79/// # Arguments
80/// * `path` - The file path
81/// * `original` - The original file content
82/// * `modified` - The modified file content
83///
84/// Returns statistics about the diff.
85pub fn print_diff(path: &Path, original: &str, modified: &str) -> DiffStats {
86    let (diff_output, stats) = generate_unified_diff(path, original, modified, 3);
87
88    // Only print if there are actual changes
89    if stats.files_changed > 0 {
90        print!("{}", diff_output);
91    }
92
93    stats
94}
95
96/// Print a summary of changes (only changed lines with minimal context)
97///
98/// This shows a more focused view than a full unified diff, displaying only
99/// the lines that changed with their line numbers.
100///
101/// # Arguments
102/// * `path` - The file path
103/// * `original` - The original file content
104/// * `modified` - The modified file content
105///
106/// Returns statistics about the diff.
107pub 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    // Collect all changes with their line numbers
115    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        // Group consecutive changes together
139        let mut i = 0;
140        while i < changes.len() {
141            let (line_num, tag, content) = &changes[i];
142
143            // Print the change
144            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        // Check that diff contains expected headers
173        assert!(diff.contains("--- src/user.rs"));
174        assert!(diff.contains("+++ src/user.rs"));
175
176        // Check that diff contains the added line
177        assert!(diff.contains("+    age: u32,"));
178
179        // Check statistics
180        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        // Should have headers but no hunks
193        assert!(diff.contains("--- src/user.rs"));
194        assert!(diff.contains("+++ src/user.rs"));
195
196        // Statistics should show no changes
197        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        // Check that diff contains the removed line
211        assert!(diff.contains("-    email: String,"));
212
213        // Check statistics
214        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}