1use radicle_surf::diff::*;
76
77use std::io;
78
79use crate::git::unified_diff;
80use crate::git::unified_diff::{Encode, Writer};
81use crate::terminal as term;
82
83#[derive(Clone, Debug, PartialEq, Eq)]
86pub enum DiffModification {
87 AdditionAddition { line: Line, line_no: u32 },
89 AdditionContext {
90 line: Line,
91 line_no_old: u32,
92 line_no_new: u32,
93 },
94 AdditionDeletion { line: Line, line_no: u32 },
96 ContextAddition { line: Line, line_no: u32 },
98 ContextContext {
100 line: Line,
101 line_no_old: u32,
102 line_no_new: u32,
103 },
104 ContextDeletion { line: Line, line_no: u32 },
106 DeletionAddition { line: Line, line_no: u32 },
108 DeletionContext {
110 line: Line,
111 line_no_old: u32,
112 line_no_new: u32,
113 },
114 DeletionDeletion { line: Line, line_no: u32 },
116}
117
118impl unified_diff::Decode for Hunk<DiffModification> {
119 fn decode(r: &mut impl io::BufRead) -> Result<Self, unified_diff::Error> {
120 let header = unified_diff::HunkHeader::decode(r)?;
121
122 let mut lines = Vec::new();
123 let mut new_line: u32 = 0;
124 let mut old_line: u32 = 0;
125
126 while old_line < header.old_size || new_line < header.new_size {
127 if old_line > header.old_size {
128 return Err(unified_diff::Error::syntax(format!(
129 "expected '{0}' old lines",
130 header.old_size,
131 )));
132 } else if new_line > header.new_size {
133 return Err(unified_diff::Error::syntax(format!(
134 "expected '{0}' new lines",
135 header.new_size,
136 )));
137 }
138
139 let mut line = DiffModification::decode(r).map_err(|e| {
140 if e.is_eof() {
141 unified_diff::Error::syntax(format!(
142 "expected '{}' old lines and '{}' new lines, but found '{}' and '{}'",
143 header.old_size, header.new_size, old_line, new_line,
144 ))
145 } else {
146 e
147 }
148 })?;
149
150 match &mut line {
151 DiffModification::AdditionAddition { line_no, .. } => {
152 *line_no = new_line;
153 new_line += 1;
154 }
155 DiffModification::AdditionContext {
156 line_no_old,
157 line_no_new,
158 ..
159 } => {
160 *line_no_old = old_line;
161 *line_no_new = new_line;
162 old_line += 1;
163 new_line += 1;
164 }
165 DiffModification::AdditionDeletion { line_no, .. } => {
166 *line_no = old_line;
167 old_line += 1;
168 }
169 DiffModification::ContextAddition { line_no, .. } => {
170 *line_no = new_line;
171 new_line += 1;
172 }
173 DiffModification::ContextContext {
174 line_no_old,
175 line_no_new,
176 ..
177 } => {
178 *line_no_old = old_line;
179 *line_no_new = new_line;
180 old_line += 1;
181 new_line += 1;
182 }
183 DiffModification::ContextDeletion { line_no, .. } => {
184 *line_no = old_line;
185 old_line += 1;
186 }
187 DiffModification::DeletionAddition { line_no, .. } => {
188 *line_no = new_line;
189 new_line += 1;
190 }
191 DiffModification::DeletionContext {
192 line_no_old,
193 line_no_new,
194 ..
195 } => {
196 *line_no_old = old_line;
197 *line_no_new = new_line;
198 old_line += 1;
199 new_line += 1;
200 }
201 DiffModification::DeletionDeletion { line_no, .. } => {
202 *line_no = old_line;
203 old_line += 1;
204 }
205 };
206
207 lines.push(line);
208 }
209
210 Ok(Hunk {
211 header: Line::from(header.to_unified_string()?),
212 lines,
213 old: header.old_line_range(),
214 new: header.new_line_range(),
215 })
216 }
217}
218
219impl unified_diff::Encode for Hunk<DiffModification> {
220 fn encode(&self, w: &mut Writer) -> Result<(), unified_diff::Error> {
221 w.magenta(self.header.from_utf8_lossy().trim_end())?;
225 for l in &self.lines {
226 l.encode(w)?;
227 }
228 Ok(())
229 }
230}
231
232#[derive(Clone, Debug, PartialEq)]
234pub struct FileDDiff {
235 pub path: std::path::PathBuf,
236 pub old: DiffFile,
237 pub new: DiffFile,
238 pub hunks: Hunks<DiffModification>,
239 pub eof: EofNewLine,
240}
241
242impl From<&FileDDiff> for unified_diff::FileHeader {
243 fn from(value: &FileDDiff) -> Self {
244 unified_diff::FileHeader::Modified {
245 path: value.path.clone(),
246 old: value.old.clone(),
247 new: value.new.clone(),
248 binary: false,
249 }
250 }
251}
252
253impl unified_diff::Decode for DiffModification {
254 fn decode(r: &mut impl std::io::BufRead) -> Result<Self, unified_diff::Error> {
255 let mut line = String::new();
256 if r.read_line(&mut line)? == 0 {
257 return Err(unified_diff::Error::UnexpectedEof);
258 }
259
260 let mut chars = line.chars();
261
262 let first = chars.next().ok_or(unified_diff::Error::UnexpectedEof)?;
263 let second = chars.next().ok_or(unified_diff::Error::UnexpectedEof)?;
264
265 let line = match (first, second) {
266 ('+', '+') => DiffModification::AdditionAddition {
267 line: chars.as_str().to_string().into(),
268 line_no: 0,
269 },
270 ('+', '-') => DiffModification::DeletionAddition {
271 line: chars.as_str().to_string().into(),
272 line_no: 0,
273 },
274 ('+', ' ') => DiffModification::ContextAddition {
275 line: chars.as_str().to_string().into(),
276 line_no: 0,
277 },
278 ('-', '+') => DiffModification::AdditionDeletion {
279 line: chars.as_str().to_string().into(),
280 line_no: 0,
281 },
282 ('-', '-') => DiffModification::DeletionDeletion {
283 line: chars.as_str().to_string().into(),
284 line_no: 0,
285 },
286 ('-', ' ') => DiffModification::ContextDeletion {
287 line: chars.as_str().to_string().into(),
288 line_no: 0,
289 },
290 (' ', '+') => DiffModification::AdditionContext {
291 line: chars.as_str().to_string().into(),
292 line_no_old: 0,
293 line_no_new: 0,
294 },
295 (' ', '-') => DiffModification::DeletionContext {
296 line: chars.as_str().to_string().into(),
297 line_no_old: 0,
298 line_no_new: 0,
299 },
300 (' ', ' ') => DiffModification::ContextContext {
301 line: chars.as_str().to_string().into(),
302 line_no_old: 0,
303 line_no_new: 0,
304 },
305 (v1, v2) => {
306 return Err(unified_diff::Error::syntax(format!(
307 "indicator character expected, but got '{v1}{v2}'"
308 )))
309 }
310 };
311
312 Ok(line)
313 }
314}
315
316impl unified_diff::Encode for DiffModification {
317 fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
318 match self {
319 DiffModification::AdditionAddition { line, .. } => {
320 let s = format!("++{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
321 w.write(s, term::Style::new(term::Color::Green))?;
322 }
323 DiffModification::AdditionDeletion { line, .. } => {
324 let s = format!("-+{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
325 w.write(s, term::Style::new(term::Color::Red))?;
326 }
327 DiffModification::ContextAddition { line, .. } => {
328 let s = format!("+ {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
329 w.write(s, term::Style::new(term::Color::Green))?;
330 }
331 DiffModification::DeletionAddition { line, .. } => {
332 let s = format!("+-{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
333 w.write(s, term::Style::new(term::Color::Green))?;
334 }
335 DiffModification::DeletionDeletion { line, .. } => {
336 let s = format!("--{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
337 w.write(s, term::Style::new(term::Color::Red))?;
338 }
339 DiffModification::ContextDeletion { line, .. } => {
340 let s = format!("- {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
341 w.write(s, term::Style::new(term::Color::Red))?;
342 }
343 DiffModification::AdditionContext { line, .. } => {
344 let s = format!(" +{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
345 w.write(s, term::Style::new(term::Color::Green).dim())?
346 }
347 DiffModification::DeletionContext { line, .. } => {
348 let s = format!(" -{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
349 w.write(s, term::Style::new(term::Color::Red).dim())?;
350 }
351 DiffModification::ContextContext { line, .. } => {
352 let s = format!(" {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
353 w.write(s, term::Style::default().dim())?;
354 }
355 }
356
357 Ok(())
358 }
359}
360
361impl unified_diff::Encode for FileDDiff {
362 fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
363 w.encode(&unified_diff::FileHeader::from(self))?;
364 for h in self.hunks.iter() {
365 h.encode(w)?;
366 }
367
368 Ok(())
369 }
370}
371
372#[derive(Clone, Debug, PartialEq, Default)]
374pub struct DDiff {
375 files: Vec<FileDDiff>,
376}
377
378impl DDiff {
379 pub fn files(&self) -> impl Iterator<Item = &FileDDiff> {
381 self.files.iter()
382 }
383
384 pub fn into_files(self) -> Vec<FileDDiff> {
386 self.files
387 }
388}
389
390impl unified_diff::Encode for DDiff {
391 fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
392 for v in self.files() {
393 v.encode(w)?;
394 }
395 Ok(())
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 use crate::git::unified_diff::{Decode, Encode};
404
405 #[test]
406 fn diff_encode_decode_ddiff_hunk() {
407 let ddiff = Hunk::<DiffModification>::parse(include_str!(concat!(
408 env!("CARGO_MANIFEST_DIR"),
409 "/tests/data/ddiff_hunk.diff"
410 )))
411 .unwrap();
412 assert_eq!(
413 include_str!(concat!(
414 env!("CARGO_MANIFEST_DIR"),
415 "/tests/data/ddiff_hunk.diff"
416 )),
417 ddiff.to_unified_string().unwrap()
418 );
419 }
420}