radicle_surf/
diff.rs

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