1use sha2::{Digest, Sha256};
2use similar::{ChangeTag, TextDiff};
3
4use crate::utils::{hex, DIFF_HASH_BYTES};
5
6#[derive(Debug, Clone)]
8pub struct DiffHunk {
9 pub new_start: u32,
11 pub new_count: u32,
13 pub content: Vec<String>,
15}
16
17impl DiffHunk {
18 pub fn content_hash(&self) -> String {
20 let mut hasher = Sha256::new();
21 for line in &self.content {
22 hasher.update(line.as_bytes());
23 hasher.update(b"\n");
24 }
25 let result = hasher.finalize();
26 hex::encode(&result[..DIFF_HASH_BYTES])
27 }
28}
29
30#[derive(Debug)]
32pub struct DiffResult {
33 pub hunks: Vec<DiffHunk>,
35 pub lines_added: u32,
37 pub lines_removed: u32,
39}
40
41pub fn compute_diff(old_content: &str, new_content: &str) -> DiffResult {
43 let diff = TextDiff::from_lines(old_content, new_content);
44
45 let mut hunks = Vec::new();
46 let mut lines_added = 0u32;
47 let mut lines_removed = 0u32;
48
49 let mut current_hunk: Option<DiffHunk> = None;
50 let mut new_line_num = 0u32;
51
52 for change in diff.iter_all_changes() {
53 match change.tag() {
54 ChangeTag::Equal => {
55 if let Some(hunk) = current_hunk.take() {
57 if !hunk.content.is_empty() {
58 hunks.push(hunk);
59 }
60 }
61 new_line_num += 1;
62 }
63 ChangeTag::Insert => {
64 new_line_num += 1;
65 lines_added += 1;
66
67 let line = change.value().trim_end_matches('\n').to_string();
68
69 if let Some(ref mut hunk) = current_hunk {
70 if hunk.new_start + hunk.new_count == new_line_num {
72 hunk.new_count += 1;
73 hunk.content.push(line);
74 } else {
75 let old_hunk = current_hunk.take().unwrap();
77 if !old_hunk.content.is_empty() {
78 hunks.push(old_hunk);
79 }
80 current_hunk = Some(DiffHunk {
81 new_start: new_line_num,
82 new_count: 1,
83 content: vec![line],
84 });
85 }
86 } else {
87 current_hunk = Some(DiffHunk {
89 new_start: new_line_num,
90 new_count: 1,
91 content: vec![line],
92 });
93 }
94 }
95 ChangeTag::Delete => {
96 lines_removed += 1;
97 }
99 }
100 }
101
102 if let Some(hunk) = current_hunk {
104 if !hunk.content.is_empty() {
105 hunks.push(hunk);
106 }
107 }
108
109 DiffResult {
110 hunks,
111 lines_added,
112 lines_removed,
113 }
114}
115
116pub fn compute_create_diff(content: &str) -> DiffResult {
118 compute_diff("", content)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_simple_addition() {
127 let old = "line1\nline2\n";
128 let new = "line1\nline2\nline3\n";
129
130 let result = compute_diff(old, new);
131
132 assert_eq!(result.lines_added, 1);
133 assert_eq!(result.lines_removed, 0);
134 assert_eq!(result.hunks.len(), 1);
135 assert_eq!(result.hunks[0].new_start, 3);
136 assert_eq!(result.hunks[0].new_count, 1);
137 assert_eq!(result.hunks[0].content, vec!["line3"]);
138 }
139
140 #[test]
141 fn test_insertion_in_middle() {
142 let old = "line1\nline3\n";
143 let new = "line1\nline2\nline3\n";
144
145 let result = compute_diff(old, new);
146
147 assert_eq!(result.lines_added, 1);
148 assert_eq!(result.hunks.len(), 1);
149 assert_eq!(result.hunks[0].new_start, 2);
150 }
151
152 #[test]
153 fn test_multiple_hunks() {
154 let old = "a\nb\nc\nd\ne\n";
155 let new = "a\nX\nb\nc\nY\nd\ne\n";
156
157 let result = compute_diff(old, new);
158
159 assert_eq!(result.lines_added, 2);
160 assert_eq!(result.hunks.len(), 2);
161 assert_eq!(result.hunks[0].new_start, 2);
162 assert_eq!(result.hunks[0].content, vec!["X"]);
163 assert_eq!(result.hunks[1].new_start, 5);
164 assert_eq!(result.hunks[1].content, vec!["Y"]);
165 }
166
167 #[test]
168 fn test_create_diff() {
169 let content = "line1\nline2\nline3\n";
170 let result = compute_create_diff(content);
171
172 assert_eq!(result.lines_added, 3);
173 assert_eq!(result.hunks.len(), 1);
174 assert_eq!(result.hunks[0].new_start, 1);
175 assert_eq!(result.hunks[0].new_count, 3);
176 }
177
178 #[test]
179 fn test_content_hash() {
180 let hunk = DiffHunk {
181 new_start: 1,
182 new_count: 2,
183 content: vec!["hello".to_string(), "world".to_string()],
184 };
185
186 let hash = hunk.content_hash();
187 assert_eq!(hash.len(), 16); let hunk2 = DiffHunk {
191 new_start: 100,
192 new_count: 2,
193 content: vec!["hello".to_string(), "world".to_string()],
194 };
195 assert_eq!(hunk.content_hash(), hunk2.content_hash());
196 }
197
198 #[test]
199 fn test_replacement() {
200 let old = "old line\n";
201 let new = "new line\n";
202
203 let result = compute_diff(old, new);
204
205 assert_eq!(result.lines_added, 1);
206 assert_eq!(result.lines_removed, 1);
207 assert_eq!(result.hunks.len(), 1);
208 assert_eq!(result.hunks[0].content, vec!["new line"]);
209 }
210}