1use std::borrow::Cow;
2
3use serde::{Deserialize, Serialize};
4use similar::{ChangeTag, TextDiff};
5use ts_rs_forge::TS;
6
7#[derive(Debug, Clone, Serialize, Deserialize, TS)]
10#[serde(rename_all = "camelCase")]
11pub struct FileDiffDetails {
12 pub file_name: Option<String>,
13 pub content: Option<String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, TS)]
18#[serde(rename_all = "camelCase")]
19pub struct Diff {
20 pub change: DiffChangeKind,
21 pub old_path: Option<String>,
22 pub new_path: Option<String>,
23 pub old_content: Option<String>,
24 pub new_content: Option<String>,
25 pub content_omitted: bool,
27 pub additions: Option<usize>,
29 pub deletions: Option<usize>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, TS)]
33#[ts(export)]
34#[serde(rename_all = "camelCase")]
35pub enum DiffChangeKind {
36 Added,
37 Deleted,
38 Modified,
39 Renamed,
40 Copied,
41 PermissionChange,
42}
43
44pub fn create_unified_diff_hunk(old: &str, new: &str) -> String {
51 let mut old = old.to_string();
53 let mut new = new.to_string();
54 if !old.ends_with('\n') {
55 old.push('\n');
56 }
57 if !new.ends_with('\n') {
58 new.push('\n');
59 }
60
61 let diff = TextDiff::from_lines(&old, &new);
62
63 let mut out = String::new();
64
65 let old_count = diff.old_slices().len();
68 let new_count = diff.new_slices().len();
69
70 out.push_str(&format!("@@ -1,{old_count} +1,{new_count} @@\n"));
71
72 for change in diff.iter_all_changes() {
73 let sign = match change.tag() {
74 ChangeTag::Equal => ' ',
75 ChangeTag::Delete => '-',
76 ChangeTag::Insert => '+',
77 };
78 let val = change.value();
79 out.push(sign);
80 out.push_str(val);
81 }
82
83 out
84}
85
86pub fn create_unified_diff(file_path: &str, old: &str, new: &str) -> String {
88 let mut out = String::new();
89 out.push_str(format!("--- a/{file_path}\n+++ b/{file_path}\n").as_str());
90 out.push_str(&create_unified_diff_hunk(old, new));
91 out
92}
93
94pub fn compute_line_change_counts(old: &str, new: &str) -> (usize, usize) {
96 let old = ensure_newline(old);
97 let new = ensure_newline(new);
98
99 let diff = TextDiff::from_lines(&old, &new);
100
101 let mut additions = 0usize;
102 let mut deletions = 0usize;
103 for change in diff.iter_all_changes() {
104 match change.tag() {
105 ChangeTag::Insert => additions += 1,
106 ChangeTag::Delete => deletions += 1,
107 ChangeTag::Equal => {}
108 }
109 }
110
111 (additions, deletions)
112}
113
114fn ensure_newline(line: &str) -> Cow<'_, str> {
116 if line.ends_with('\n') {
117 Cow::Borrowed(line)
118 } else {
119 let mut owned = line.to_owned();
120 owned.push('\n');
121 Cow::Owned(owned)
122 }
123}
124
125pub fn extract_unified_diff_hunks(unified_diff: &str) -> Vec<String> {
128 let lines = unified_diff.split_inclusive('\n').collect::<Vec<_>>();
129
130 if !lines.iter().any(|l| l.starts_with("@@")) {
131 let hunk = lines
133 .iter()
134 .copied()
135 .filter(|line| line.starts_with([' ', '+', '-']))
136 .collect::<String>();
137
138 let old_count = lines
139 .iter()
140 .filter(|line| line.starts_with(['-', ' ']))
141 .count();
142 let new_count = lines
143 .iter()
144 .filter(|line| line.starts_with(['+', ' ']))
145 .count();
146
147 return if hunk.is_empty() {
148 vec![]
149 } else {
150 vec![format!("@@ -1,{old_count} +1,{new_count} @@\n{hunk}")]
151 };
152 }
153
154 let mut hunks = vec![];
155 let mut current_hunk: Option<String> = None;
156
157 for line in lines {
159 if line.starts_with("@@") {
160 if let Some(hunk) = current_hunk.take() {
162 if !hunk.is_empty() {
164 hunks.push(hunk);
165 }
166 }
167 current_hunk = Some(line.to_string());
168 } else if let Some(ref mut hunk) = current_hunk {
169 if line.starts_with([' ', '+', '-']) {
170 hunk.push_str(line);
172 } else {
173 if !hunk.is_empty() {
175 hunks.push(hunk.clone());
176 }
177 current_hunk = None;
178 }
179 }
180 }
181 if let Some(hunk) = current_hunk
183 && !hunk.is_empty()
184 {
185 hunks.push(hunk);
186 }
187
188 hunks = fix_hunk_headers(hunks);
190
191 hunks
192}
193
194fn fix_hunk_headers(hunks: Vec<String>) -> Vec<String> {
196 if hunks.is_empty() {
197 return hunks;
198 }
199
200 let mut new_hunks = Vec::new();
201 for hunk in hunks {
203 let mut lines = hunk
204 .split_inclusive('\n')
205 .map(str::to_string)
206 .collect::<Vec<_>>();
207 if lines.len() < 2 {
208 continue;
210 }
211
212 let header = &lines[0];
213 if !header.starts_with("@@") {
214 continue;
216 }
217
218 if header.trim() == "@@" {
219 lines.remove(0);
221 let old_count = lines
222 .iter()
223 .filter(|line| line.starts_with(['-', ' ']))
224 .count();
225 let new_count = lines
226 .iter()
227 .filter(|line| line.starts_with(['+', ' ']))
228 .count();
229 let new_header = format!("@@ -1,{old_count} +1,{new_count} @@");
230 lines.insert(0, new_header);
231 new_hunks.push(lines.join(""));
232 } else {
233 new_hunks.push(hunk);
235 }
236 }
237
238 new_hunks
239}
240
241pub fn concatenate_diff_hunks(file_path: &str, hunks: &[String]) -> String {
243 let mut unified_diff = String::new();
244
245 let header = format!("--- a/{file_path}\n+++ b/{file_path}\n");
246
247 unified_diff.push_str(&header);
248
249 if !hunks.is_empty() {
250 let lines = hunks
251 .iter()
252 .flat_map(|hunk| hunk.lines())
253 .filter(|line| line.starts_with("@@ ") || line.starts_with([' ', '+', '-']))
254 .collect::<Vec<_>>();
255 unified_diff.push_str(lines.join("\n").as_str());
256 if !unified_diff.ends_with('\n') {
257 unified_diff.push('\n');
258 }
259 }
260
261 unified_diff
262}