radicle_surf/
diff.rs

1//! Types that represent diff(s) in a Git repo.
2
3use std::{
4    borrow::Cow,
5    ops::Range,
6    path::{Path, PathBuf},
7    string::FromUtf8Error,
8};
9
10#[cfg(feature = "serde")]
11use serde::{ser, ser::SerializeStruct, Serialize, Serializer};
12
13use git_ext::Oid;
14
15pub mod git;
16
17/// The serializable representation of a `git diff`.
18///
19/// A [`Diff`] can be retrieved by the following functions:
20///    * [`crate::Repository::diff`]
21///    * [`crate::Repository::diff_commit`]
22#[cfg_attr(feature = "serde", derive(Serialize))]
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct Diff {
25    files: Vec<FileDiff>,
26    stats: Stats,
27}
28
29impl Diff {
30    /// Creates an empty diff.
31    pub(crate) fn new() -> Self {
32        Diff::default()
33    }
34
35    /// Returns an iterator of the file in the diff.
36    pub fn files(&self) -> impl Iterator<Item = &FileDiff> {
37        self.files.iter()
38    }
39
40    /// Returns owned files in the diff.
41    pub fn into_files(self) -> Vec<FileDiff> {
42        self.files
43    }
44
45    pub fn added(&self) -> impl Iterator<Item = &Added> {
46        self.files().filter_map(|x| match x {
47            FileDiff::Added(a) => Some(a),
48            _ => None,
49        })
50    }
51
52    pub fn deleted(&self) -> impl Iterator<Item = &Deleted> {
53        self.files().filter_map(|x| match x {
54            FileDiff::Deleted(a) => Some(a),
55            _ => None,
56        })
57    }
58
59    pub fn moved(&self) -> impl Iterator<Item = &Moved> {
60        self.files().filter_map(|x| match x {
61            FileDiff::Moved(a) => Some(a),
62            _ => None,
63        })
64    }
65
66    pub fn modified(&self) -> impl Iterator<Item = &Modified> {
67        self.files().filter_map(|x| match x {
68            FileDiff::Modified(a) => Some(a),
69            _ => None,
70        })
71    }
72
73    pub fn copied(&self) -> impl Iterator<Item = &Copied> {
74        self.files().filter_map(|x| match x {
75            FileDiff::Copied(a) => Some(a),
76            _ => None,
77        })
78    }
79
80    pub fn stats(&self) -> &Stats {
81        &self.stats
82    }
83
84    fn update_stats(&mut self, diff: &DiffContent) {
85        self.stats.files_changed += 1;
86        if let DiffContent::Plain { hunks, .. } = diff {
87            for h in hunks.iter() {
88                for l in &h.lines {
89                    match l {
90                        Modification::Addition(_) => self.stats.insertions += 1,
91                        Modification::Deletion(_) => self.stats.deletions += 1,
92                        _ => (),
93                    }
94                }
95            }
96        }
97    }
98
99    pub fn insert_modified(
100        &mut self,
101        path: PathBuf,
102        diff: DiffContent,
103        old: DiffFile,
104        new: DiffFile,
105    ) {
106        self.update_stats(&diff);
107        let diff = FileDiff::Modified(Modified {
108            path,
109            diff,
110            old,
111            new,
112        });
113        self.files.push(diff);
114    }
115
116    pub fn insert_moved(
117        &mut self,
118        old_path: PathBuf,
119        new_path: PathBuf,
120        old: DiffFile,
121        new: DiffFile,
122        content: DiffContent,
123    ) {
124        self.update_stats(&DiffContent::Empty);
125        let diff = FileDiff::Moved(Moved {
126            old_path,
127            new_path,
128            old,
129            new,
130            diff: content,
131        });
132        self.files.push(diff);
133    }
134
135    pub fn insert_copied(
136        &mut self,
137        old_path: PathBuf,
138        new_path: PathBuf,
139        old: DiffFile,
140        new: DiffFile,
141        content: DiffContent,
142    ) {
143        self.update_stats(&DiffContent::Empty);
144        let diff = FileDiff::Copied(Copied {
145            old_path,
146            new_path,
147            old,
148            new,
149            diff: content,
150        });
151        self.files.push(diff);
152    }
153
154    pub fn insert_added(&mut self, path: PathBuf, diff: DiffContent, new: DiffFile) {
155        self.update_stats(&diff);
156        let diff = FileDiff::Added(Added { path, diff, new });
157        self.files.push(diff);
158    }
159
160    pub fn insert_deleted(&mut self, path: PathBuf, diff: DiffContent, old: DiffFile) {
161        self.update_stats(&diff);
162        let diff = FileDiff::Deleted(Deleted { path, diff, old });
163        self.files.push(diff);
164    }
165}
166
167/// A file that was added within a [`Diff`].
168#[cfg_attr(feature = "serde", derive(Serialize))]
169#[derive(Clone, Debug, PartialEq, Eq)]
170pub struct Added {
171    /// The path to this file, relative to the repository root.
172    pub path: PathBuf,
173    pub diff: DiffContent,
174    pub new: DiffFile,
175}
176
177/// A file that was deleted within a [`Diff`].
178#[cfg_attr(feature = "serde", derive(Serialize))]
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct Deleted {
181    /// The path to this file, relative to the repository root.
182    pub path: PathBuf,
183    pub diff: DiffContent,
184    pub old: DiffFile,
185}
186
187/// A file that was moved within a [`Diff`].
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct Moved {
190    /// The old path to this file, relative to the repository root.
191    pub old_path: PathBuf,
192    pub old: DiffFile,
193    /// The new path to this file, relative to the repository root.
194    pub new_path: PathBuf,
195    pub new: DiffFile,
196    pub diff: DiffContent,
197}
198
199#[cfg(feature = "serde")]
200impl Serialize for Moved {
201    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
202    where
203        S: Serializer,
204    {
205        if self.old == self.new {
206            let mut state = serializer.serialize_struct("Moved", 3)?;
207            state.serialize_field("oldPath", &self.old_path)?;
208            state.serialize_field("newPath", &self.new_path)?;
209            state.serialize_field("current", &self.new)?;
210            state.end()
211        } else {
212            let mut state = serializer.serialize_struct("Moved", 5)?;
213            state.serialize_field("oldPath", &self.old_path)?;
214            state.serialize_field("newPath", &self.new_path)?;
215            state.serialize_field("old", &self.old)?;
216            state.serialize_field("new", &self.new)?;
217            state.serialize_field("diff", &self.diff)?;
218            state.end()
219        }
220    }
221}
222
223/// A file that was copied within a [`Diff`].
224#[derive(Clone, Debug, PartialEq, Eq)]
225pub struct Copied {
226    /// The old path to this file, relative to the repository root.
227    pub old_path: PathBuf,
228    /// The new path to this file, relative to the repository root.
229    pub new_path: PathBuf,
230    pub old: DiffFile,
231    pub new: DiffFile,
232    pub diff: DiffContent,
233}
234
235#[cfg(feature = "serde")]
236impl Serialize for Copied {
237    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
238    where
239        S: Serializer,
240    {
241        if self.old == self.new {
242            let mut state = serializer.serialize_struct("Copied", 3)?;
243            state.serialize_field("oldPath", &self.old_path)?;
244            state.serialize_field("newPath", &self.new_path)?;
245            state.serialize_field("current", &self.new)?;
246            state.end()
247        } else {
248            let mut state = serializer.serialize_struct("Copied", 5)?;
249            state.serialize_field("oldPath", &self.old_path)?;
250            state.serialize_field("newPath", &self.new_path)?;
251            state.serialize_field("old", &self.old)?;
252            state.serialize_field("new", &self.new)?;
253            state.serialize_field("diff", &self.diff)?;
254            state.end()
255        }
256    }
257}
258
259#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub enum EofNewLine {
262    OldMissing,
263    NewMissing,
264    BothMissing,
265    NoneMissing,
266}
267
268impl Default for EofNewLine {
269    fn default() -> Self {
270        Self::NoneMissing
271    }
272}
273
274/// A file that was modified within a [`Diff`].
275#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
276#[derive(Clone, Debug, PartialEq, Eq)]
277pub struct Modified {
278    pub path: PathBuf,
279    pub diff: DiffContent,
280    pub old: DiffFile,
281    pub new: DiffFile,
282}
283
284/// The set of changes for a given file.
285#[cfg_attr(
286    feature = "serde",
287    derive(Serialize),
288    serde(tag = "type", rename_all = "camelCase")
289)]
290#[derive(Clone, Debug, PartialEq, Eq)]
291pub enum DiffContent {
292    /// The file is a binary file and so no set of changes can be provided.
293    Binary,
294    /// The set of changes, as [`Hunks`] for a plaintext file.
295    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
296    Plain {
297        hunks: Hunks<Modification>,
298        stats: FileStats,
299        eof: EofNewLine,
300    },
301    Empty,
302}
303
304impl DiffContent {
305    pub fn eof(&self) -> Option<EofNewLine> {
306        match self {
307            Self::Plain { eof, .. } => Some(eof.clone()),
308            _ => None,
309        }
310    }
311
312    pub fn stats(&self) -> Option<&FileStats> {
313        match &self {
314            DiffContent::Plain { stats, .. } => Some(stats),
315            DiffContent::Empty => None,
316            DiffContent::Binary => None,
317        }
318    }
319}
320
321/// File mode in a diff.
322#[derive(Clone, Debug, PartialEq, Eq)]
323#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
324pub enum FileMode {
325    /// For regular files.
326    Blob,
327    /// For regular files that are executable.
328    BlobExecutable,
329    /// For directories.
330    Tree,
331    /// For symbolic links.
332    Link,
333    /// Used for Git submodules.
334    Commit,
335}
336
337impl From<FileMode> for u32 {
338    fn from(m: FileMode) -> Self {
339        git2::FileMode::from(m).into()
340    }
341}
342
343impl From<FileMode> for i32 {
344    fn from(m: FileMode) -> Self {
345        git2::FileMode::from(m).into()
346    }
347}
348
349/// A modified file.
350#[derive(Clone, Debug, PartialEq, Eq)]
351#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
352pub struct DiffFile {
353    /// File blob id.
354    pub oid: Oid,
355    /// File mode.
356    pub mode: FileMode,
357}
358
359#[derive(Clone, Debug, PartialEq, Eq)]
360#[cfg_attr(
361    feature = "serde",
362    derive(Serialize),
363    serde(tag = "status", rename_all = "camelCase")
364)]
365pub enum FileDiff {
366    Added(Added),
367    Deleted(Deleted),
368    Modified(Modified),
369    Moved(Moved),
370    Copied(Copied),
371}
372
373impl FileDiff {
374    pub fn path(&self) -> &Path {
375        match self {
376            FileDiff::Added(x) => x.path.as_path(),
377            FileDiff::Deleted(x) => x.path.as_path(),
378            FileDiff::Modified(x) => x.path.as_path(),
379            FileDiff::Moved(x) => x.new_path.as_path(),
380            FileDiff::Copied(x) => x.new_path.as_path(),
381        }
382    }
383}
384
385/// Statistics describing a particular [`FileDiff`].
386#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
387#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
388pub struct FileStats {
389    /// Get the total number of additions in a [`FileDiff`].
390    pub additions: usize,
391    /// Get the total number of deletions in a [`FileDiff`].
392    pub deletions: usize,
393}
394
395/// Statistics describing a particular [`Diff`].
396#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
397#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
398pub struct Stats {
399    /// Get the total number of files changed in a [`Diff`]
400    pub files_changed: usize,
401    /// Get the total number of insertions in a [`Diff`].
402    pub insertions: usize,
403    /// Get the total number of deletions in a [`Diff`].
404    pub deletions: usize,
405}
406
407/// A set of changes across multiple lines.
408///
409/// The parameter `T` can be an [`Addition`], [`Deletion`], or
410/// [`Modification`].
411#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
412#[derive(Clone, Debug, PartialEq, Eq)]
413pub struct Hunk<T> {
414    pub header: Line,
415    pub lines: Vec<T>,
416    /// Old line range.
417    pub old: Range<u32>,
418    /// New line range.
419    pub new: Range<u32>,
420}
421
422/// A set of [`Hunk`] changes.
423#[cfg_attr(feature = "serde", derive(Serialize))]
424#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct Hunks<T>(pub Vec<Hunk<T>>);
426
427impl<T> Default for Hunks<T> {
428    fn default() -> Self {
429        Self(Default::default())
430    }
431}
432
433impl<T> Hunks<T> {
434    pub fn iter(&self) -> impl Iterator<Item = &Hunk<T>> {
435        self.0.iter()
436    }
437}
438
439impl<T> From<Vec<Hunk<T>>> for Hunks<T> {
440    fn from(hunks: Vec<Hunk<T>>) -> Self {
441        Self(hunks)
442    }
443}
444
445/// The content of a single line.
446#[derive(Clone, Debug, PartialEq, Eq)]
447pub struct Line(pub(crate) Vec<u8>);
448
449impl Line {
450    pub fn as_bytes(&self) -> &[u8] {
451        self.0.as_slice()
452    }
453
454    pub fn from_utf8(self) -> Result<String, FromUtf8Error> {
455        String::from_utf8(self.0)
456    }
457
458    pub fn from_utf8_lossy<'a>(&'a self) -> Cow<'a, str> {
459        String::from_utf8_lossy(&self.0)
460    }
461}
462
463impl From<Vec<u8>> for Line {
464    fn from(v: Vec<u8>) -> Self {
465        Self(v)
466    }
467}
468
469impl From<String> for Line {
470    fn from(s: String) -> Self {
471        Self(s.into_bytes())
472    }
473}
474
475#[cfg(feature = "serde")]
476impl Serialize for Line {
477    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
478    where
479        S: Serializer,
480    {
481        let s = std::str::from_utf8(&self.0).map_err(ser::Error::custom)?;
482
483        serializer.serialize_str(s)
484    }
485}
486
487/// Either the modification of a single [`Line`], or just contextual
488/// information.
489#[derive(Clone, Debug, PartialEq, Eq)]
490pub enum Modification {
491    /// A line is an addition in a file.
492    Addition(Addition),
493
494    /// A line is a deletion in a file.
495    Deletion(Deletion),
496
497    /// A contextual line in a file, i.e. there were no changes to the line.
498    Context {
499        line: Line,
500        line_no_old: u32,
501        line_no_new: u32,
502    },
503}
504
505#[cfg(feature = "serde")]
506impl Serialize for Modification {
507    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
508    where
509        S: Serializer,
510    {
511        use serde::ser::SerializeMap as _;
512
513        match self {
514            Modification::Addition(addition) => {
515                let mut map = serializer.serialize_map(Some(3))?;
516                map.serialize_entry("line", &addition.line)?;
517                map.serialize_entry("lineNo", &addition.line_no)?;
518                map.serialize_entry("type", "addition")?;
519                map.end()
520            }
521            Modification::Deletion(deletion) => {
522                let mut map = serializer.serialize_map(Some(3))?;
523                map.serialize_entry("line", &deletion.line)?;
524                map.serialize_entry("lineNo", &deletion.line_no)?;
525                map.serialize_entry("type", "deletion")?;
526                map.end()
527            }
528            Modification::Context {
529                line,
530                line_no_old,
531                line_no_new,
532            } => {
533                let mut map = serializer.serialize_map(Some(4))?;
534                map.serialize_entry("line", line)?;
535                map.serialize_entry("lineNoOld", line_no_old)?;
536                map.serialize_entry("lineNoNew", line_no_new)?;
537                map.serialize_entry("type", "context")?;
538                map.end()
539            }
540        }
541    }
542}
543
544/// A addition of a [`Line`] at the `line_no`.
545#[derive(Clone, Debug, PartialEq, Eq)]
546pub struct Addition {
547    pub line: Line,
548    pub line_no: u32,
549}
550
551#[cfg(feature = "serde")]
552impl Serialize for Addition {
553    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
554    where
555        S: Serializer,
556    {
557        use serde::ser::SerializeStruct as _;
558
559        let mut s = serializer.serialize_struct("Addition", 3)?;
560        s.serialize_field("line", &self.line)?;
561        s.serialize_field("lineNo", &self.line_no)?;
562        s.serialize_field("type", "addition")?;
563        s.end()
564    }
565}
566
567/// A deletion of a [`Line`] at the `line_no`.
568#[derive(Clone, Debug, PartialEq, Eq)]
569pub struct Deletion {
570    pub line: Line,
571    pub line_no: u32,
572}
573
574#[cfg(feature = "serde")]
575impl Serialize for Deletion {
576    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
577    where
578        S: Serializer,
579    {
580        use serde::ser::SerializeStruct as _;
581
582        let mut s = serializer.serialize_struct("Deletion", 3)?;
583        s.serialize_field("line", &self.line)?;
584        s.serialize_field("lineNo", &self.line_no)?;
585        s.serialize_field("type", "deletion")?;
586        s.end()
587    }
588}
589
590impl Modification {
591    pub fn addition(line: impl Into<Line>, line_no: u32) -> Self {
592        Self::Addition(Addition {
593            line: line.into(),
594            line_no,
595        })
596    }
597
598    pub fn deletion(line: impl Into<Line>, line_no: u32) -> Self {
599        Self::Deletion(Deletion {
600            line: line.into(),
601            line_no,
602        })
603    }
604
605    pub fn context(line: impl Into<Line>, line_no_old: u32, line_no_new: u32) -> Self {
606        Self::Context {
607            line: line.into(),
608            line_no_old,
609            line_no_new,
610        }
611    }
612}