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 '{0}{1}'",
308 v1, v2
309 )))
310 }
311 };
312
313 Ok(line)
314 }
315}
316
317impl unified_diff::Encode for DiffModification {
318 fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
319 match self {
320 DiffModification::AdditionAddition { line, .. } => {
321 let s = format!("++{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
322 w.write(s, term::Style::new(term::Color::Green))?;
323 }
324 DiffModification::AdditionDeletion { line, .. } => {
325 let s = format!("-+{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
326 w.write(s, term::Style::new(term::Color::Red))?;
327 }
328 DiffModification::ContextAddition { line, .. } => {
329 let s = format!("+ {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
330 w.write(s, term::Style::new(term::Color::Green))?;
331 }
332 DiffModification::DeletionAddition { line, .. } => {
333 let s = format!("+-{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
334 w.write(s, term::Style::new(term::Color::Green))?;
335 }
336 DiffModification::DeletionDeletion { line, .. } => {
337 let s = format!("--{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
338 w.write(s, term::Style::new(term::Color::Red))?;
339 }
340 DiffModification::ContextDeletion { line, .. } => {
341 let s = format!("- {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
342 w.write(s, term::Style::new(term::Color::Red))?;
343 }
344 DiffModification::AdditionContext { line, .. } => {
345 let s = format!(" +{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
346 w.write(s, term::Style::new(term::Color::Green).dim())?
347 }
348 DiffModification::DeletionContext { line, .. } => {
349 let s = format!(" -{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
350 w.write(s, term::Style::new(term::Color::Red).dim())?;
351 }
352 DiffModification::ContextContext { line, .. } => {
353 let s = format!(" {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
354 w.write(s, term::Style::default().dim())?;
355 }
356 }
357
358 Ok(())
359 }
360}
361
362impl unified_diff::Encode for FileDDiff {
363 fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
364 w.encode(&unified_diff::FileHeader::from(self))?;
365 for h in self.hunks.iter() {
366 h.encode(w)?;
367 }
368
369 Ok(())
370 }
371}
372
373#[derive(Clone, Debug, PartialEq, Default)]
375pub struct DDiff {
376 files: Vec<FileDDiff>,
377}
378
379impl DDiff {
380 pub fn files(&self) -> impl Iterator<Item = &FileDDiff> {
382 self.files.iter()
383 }
384
385 pub fn into_files(self) -> Vec<FileDDiff> {
387 self.files
388 }
389}
390
391impl unified_diff::Encode for DDiff {
392 fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
393 for v in self.files() {
394 v.encode(w)?;
395 }
396 Ok(())
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 use crate::git::unified_diff::{Decode, Encode};
405
406 #[test]
407 fn diff_encode_decode_ddiff_hunk() {
408 let ddiff = Hunk::<DiffModification>::parse(include_str!(concat!(
409 env!("CARGO_MANIFEST_DIR"),
410 "/tests/data/ddiff_hunk.diff"
411 )))
412 .unwrap();
413 assert_eq!(
414 include_str!(concat!(
415 env!("CARGO_MANIFEST_DIR"),
416 "/tests/data/ddiff_hunk.diff"
417 )),
418 ddiff.to_unified_string().unwrap()
419 );
420 }
421}