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#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::path::PathBuf;
100
101    #[test]
102    fn test_generate_unified_diff() {
103        let original = "pub struct User {\n    id: u64,\n    name: String,\n}\n";
104        let modified = "pub struct User {\n    id: u64,\n    age: u32,\n    name: String,\n}\n";
105        let path = PathBuf::from("src/user.rs");
106
107        let (diff, stats) = generate_unified_diff(&path, original, modified, 3);
108
109        // Check that diff contains expected headers
110        assert!(diff.contains("--- src/user.rs"));
111        assert!(diff.contains("+++ src/user.rs"));
112
113        // Check that diff contains the added line
114        assert!(diff.contains("+    age: u32,"));
115
116        // Check statistics
117        assert_eq!(stats.files_changed, 1);
118        assert_eq!(stats.lines_added, 1);
119        assert_eq!(stats.lines_removed, 0);
120    }
121
122    #[test]
123    fn test_generate_unified_diff_no_changes() {
124        let content = "pub struct User {\n    id: u64,\n}\n";
125        let path = PathBuf::from("src/user.rs");
126
127        let (diff, stats) = generate_unified_diff(&path, content, content, 3);
128
129        // Should have headers but no hunks
130        assert!(diff.contains("--- src/user.rs"));
131        assert!(diff.contains("+++ src/user.rs"));
132
133        // Statistics should show no changes
134        assert_eq!(stats.files_changed, 0);
135        assert_eq!(stats.lines_added, 0);
136        assert_eq!(stats.lines_removed, 0);
137    }
138
139    #[test]
140    fn test_generate_unified_diff_with_removal() {
141        let original = "pub struct User {\n    id: u64,\n    name: String,\n    email: String,\n}\n";
142        let modified = "pub struct User {\n    id: u64,\n    name: String,\n}\n";
143        let path = PathBuf::from("src/user.rs");
144
145        let (diff, stats) = generate_unified_diff(&path, original, modified, 3);
146
147        // Check that diff contains the removed line
148        assert!(diff.contains("-    email: String,"));
149
150        // Check statistics
151        assert_eq!(stats.files_changed, 1);
152        assert_eq!(stats.lines_added, 0);
153        assert_eq!(stats.lines_removed, 1);
154    }
155
156    #[test]
157    fn test_diff_stats_add() {
158        let mut stats1 = DiffStats {
159            files_changed: 1,
160            lines_added: 5,
161            lines_removed: 2,
162        };
163
164        let stats2 = DiffStats {
165            files_changed: 2,
166            lines_added: 3,
167            lines_removed: 1,
168        };
169
170        stats1.add(&stats2);
171
172        assert_eq!(stats1.files_changed, 3);
173        assert_eq!(stats1.lines_added, 8);
174        assert_eq!(stats1.lines_removed, 3);
175    }
176
177    #[test]
178    fn test_print_diff_returns_stats() {
179        let original = "line1\nline2\n";
180        let modified = "line1\nline2\nline3\n";
181        let path = PathBuf::from("test.txt");
182
183        let stats = print_diff(&path, original, modified);
184
185        assert_eq!(stats.files_changed, 1);
186        assert_eq!(stats.lines_added, 1);
187        assert_eq!(stats.lines_removed, 0);
188    }
189}