Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
Merge remote-tracking branch 'origin/surf/diff'
Fintan Halpenny committed 3 years ago
commit 29f2ca337daff9272826883fc821bf8bdaeb6461
parent dab5105
8 files changed +605 -584
modified radicle-surf/examples/diff.rs
@@ -57,7 +57,7 @@ fn init_repository_or_exit(path_to_repo: &str) -> git::Repository {
}

fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {
-
    diff.created.iter().for_each(|created| {
+
    diff.added.iter().for_each(|created| {
        println!("+++ {:?}", created.path);
    });
    diff.deleted.iter().for_each(|deleted| {
@@ -69,10 +69,10 @@ fn print_diff_summary(diff: &Diff, elapsed_nanos: u128) {

    println!(
        "created {} / deleted {} / modified {} / total {}",
-
        diff.created.len(),
+
        diff.added.len(),
        diff.deleted.len(),
        diff.modified.len(),
-
        diff.created.len() + diff.deleted.len() + diff.modified.len()
+
        diff.added.len() + diff.deleted.len() + diff.modified.len()
    );
    println!("diff took {} nanos ", elapsed_nanos);
}
modified radicle-surf/src/diff.rs
@@ -17,54 +17,111 @@

#![allow(dead_code, unused_variables, missing_docs)]

-
use std::{convert::TryFrom, path::PathBuf, slice};
+
use std::path::PathBuf;

#[cfg(feature = "serde")]
use serde::{ser, Serialize, Serializer};

pub mod git;

+
/// The serializable representation of a `git diff`.
+
///
+
/// A [`Diff`] can be retrieved by the following functions:
+
///    * [`crate::git::Repository::diff`]
+
///    * [`crate::git::Repository::diff_commit`]
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
-
#[derive(Clone, Debug, PartialEq, Eq)]
+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Diff {
-
    pub created: Vec<CreateFile>,
-
    pub deleted: Vec<DeleteFile>,
-
    pub moved: Vec<MoveFile>,
-
    pub copied: Vec<CopyFile>,
-
    pub modified: Vec<ModifiedFile>,
+
    pub added: Vec<Added>,
+
    pub deleted: Vec<Deleted>,
+
    pub moved: Vec<Moved>,
+
    pub copied: Vec<Copied>,
+
    pub modified: Vec<Modified>,
+
    pub stats: Stats,
}

-
impl Default for Diff {
-
    fn default() -> Self {
-
        Self::new()
+
impl Diff {
+
    pub fn new() -> Self {
+
        Diff::default()
+
    }
+

+
    fn modified(
+
        &mut self,
+
        path: PathBuf,
+
        hunks: impl Into<Hunks<Modification>>,
+
        eof: Option<EofNewLine>,
+
    ) {
+
        self.modified.push(Modified {
+
            path,
+
            diff: FileDiff::Plain {
+
                hunks: hunks.into(),
+
            },
+
            eof,
+
        })
+
    }
+

+
    fn moved(&mut self, old_path: PathBuf, new_path: PathBuf) {
+
        self.moved.push(Moved { old_path, new_path });
+
    }
+

+
    fn copied(&mut self, old_path: PathBuf, new_path: PathBuf) {
+
        self.copied.push(Copied { old_path, new_path });
+
    }
+

+
    fn modified_binary(&mut self, path: PathBuf) {
+
        self.modified.push(Modified {
+
            path,
+
            diff: FileDiff::Binary,
+
            eof: None,
+
        })
+
    }
+

+
    fn added(&mut self, path: PathBuf, diff: FileDiff<Addition>) {
+
        self.added.push(Added { path, diff })
+
    }
+

+
    fn deleted(&mut self, path: PathBuf, diff: FileDiff<Deletion>) {
+
        self.deleted.push(Deleted { path, diff })
    }
}

+
/// A file that was added within a [`Diff`].
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct CreateFile {
+
pub struct Added {
+
    /// The path to this file, relative to the repository root.
    pub path: PathBuf,
-
    pub diff: FileDiff,
+
    /// The set of [`Addition`]s to this file.
+
    pub diff: FileDiff<Addition>,
}

+
/// A file that was deleted within a [`Diff`].
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct DeleteFile {
+
pub struct Deleted {
+
    /// The path to this file, relative to the repository root.
    pub path: PathBuf,
-
    pub diff: FileDiff,
+
    /// The set of [`Deletion`]s to this file.
+
    pub diff: FileDiff<Deletion>,
}

+
/// A file that was moved within a [`Diff`].
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct MoveFile {
+
pub struct Moved {
+
    /// The old path to this file, relative to the repository root.
    pub old_path: PathBuf,
+
    /// The new path to this file, relative to the repository root.
    pub new_path: PathBuf,
}

+
/// A file that was copied within a [`Diff`].
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct CopyFile {
+
pub struct Copied {
+
    /// The old path to this file, relative to the repository root.
    pub old_path: PathBuf,
+
    /// The new path to this file, relative to the repository root.
    pub new_path: PathBuf,
}

@@ -76,98 +133,76 @@ pub enum EofNewLine {
    BothMissing,
}

+
/// A file that was modified within a [`Diff`].
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct ModifiedFile {
+
pub struct Modified {
+
    /// The path to this file, relative to the repository root.
    pub path: PathBuf,
-
    pub diff: FileDiff,
+
    /// The set of [`Modification`]s to this file.
+
    pub diff: FileDiff<Modification>,
+
    /// Was there an EOF newline present.
    pub eof: Option<EofNewLine>,
}

-
/// A set of changes belonging to one file.
+
/// The set of changes for a given file.
#[cfg_attr(
    feature = "serde",
    derive(Serialize),
    serde(tag = "type", rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub enum FileDiff {
+
pub enum FileDiff<T> {
+
    /// The file is a binary file and so no set of changes can be provided.
    Binary,
+
    /// The set of changes, as [`Hunks`] for a plaintext file.
    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
-
    Plain {
-
        hunks: Hunks,
-
    },
+
    Plain { hunks: Hunks<T> },
}

/// Statistics describing a particular [`Diff`].
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
-
#[derive(Clone, Debug, Eq, PartialEq)]
+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Stats {
-
    /// Get the total number of files changed in a diff.
+
    /// Get the total number of files changed in a [`Diff`]
    pub files_changed: usize,
-
    /// Get the total number of insertions in a diff.
+
    /// Get the total number of insertions in a [`Diff`].
    pub insertions: usize,
-
    /// Get the total number of deletions in a diff.
+
    /// Get the total number of deletions in a [`Diff`].
    pub deletions: usize,
}

-
/// A set of line changes.
+
/// A set of changes across multiple lines.
+
///
+
/// The parameter `T` can be an [`Addition`], [`Deletion`], or
+
/// [`Modification`].
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct Hunk {
+
pub struct Hunk<T> {
    pub header: Line,
-
    pub lines: Vec<LineDiff>,
+
    pub lines: Vec<T>,
}

-
/// A set of [`Hunk`]s.
+
/// A set of [`Hunk`] changes.
#[cfg_attr(feature = "serde", derive(Serialize))]
-
#[derive(Clone, Debug, Default, PartialEq, Eq)]
-
pub struct Hunks(pub Vec<Hunk>);
-

-
pub struct IterHunks<'a> {
-
    inner: slice::Iter<'a, Hunk>,
-
}
-

-
impl Hunks {
-
    pub fn iter(&self) -> IterHunks<'_> {
-
        IterHunks {
-
            inner: self.0.iter(),
-
        }
-
    }
-
}
+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub struct Hunks<T>(pub Vec<Hunk<T>>);

-
impl From<Vec<Hunk>> for Hunks {
-
    fn from(hunks: Vec<Hunk>) -> Self {
-
        Self(hunks)
+
impl<T> Default for Hunks<T> {
+
    fn default() -> Self {
+
        Self(Default::default())
    }
}

-
impl<'a> Iterator for IterHunks<'a> {
-
    type Item = &'a Hunk;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.inner.next()
+
impl<T> Hunks<T> {
+
    pub fn iter(&self) -> impl Iterator<Item = &Hunk<T>> {
+
        self.0.iter()
    }
}

-
impl TryFrom<git2::Patch<'_>> for Hunks {
-
    type Error = git::error::Hunk;
-

-
    fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
-
        let mut hunks = Vec::new();
-
        for h in 0..patch.num_hunks() {
-
            let (hunk, hunk_lines) = patch.hunk(h)?;
-
            let header = Line(hunk.header().to_owned());
-
            let mut lines: Vec<LineDiff> = Vec::new();
-

-
            for l in 0..hunk_lines {
-
                let line = patch.line_in_hunk(h, l)?;
-
                let line = LineDiff::try_from(line)?;
-
                lines.push(line);
-
            }
-
            hunks.push(Hunk { header, lines });
-
        }
-
        Ok(Hunks(hunks))
+
impl<T> From<Vec<Hunk<T>>> for Hunks<T> {
+
    fn from(hunks: Vec<Hunk<T>>) -> Self {
+
        Self(hunks)
    }
}

@@ -199,155 +234,68 @@ impl Serialize for Line {
    }
}

-
/// Single line delta. Two of these are need to represented a modified line: one
-
/// addition and one deletion. Context is also represented with this type.
+
/// Either the modification of a single [`Line`], or just contextual
+
/// information.
#[cfg_attr(
    feature = "serde",
    derive(Serialize),
    serde(tag = "type", rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub enum LineDiff {
-
    /// Line added.
+
pub enum Modification {
+
    /// A lines is an addition in a file.
    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
-
    Addition { line: Line, line_num: u32 },
+
    Addition(Addition),

-
    /// Line deleted.
+
    /// A lines is a deletion in a file.
    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
-
    Deletion { line: Line, line_num: u32 },
+
    Deletion(Deletion),

-
    /// Line context.
+
    /// A contextual line in a file, i.e. there were no changes to the line.
    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
    Context {
        line: Line,
-
        line_num_old: u32,
-
        line_num_new: u32,
+
        line_no_old: u32,
+
        line_no_new: u32,
    },
}

-
impl LineDiff {
-
    pub fn addition(line: impl Into<Line>, line_num: u32) -> Self {
-
        Self::Addition {
+
/// A addition of a [`Line`] at the `line_no`.
+
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub struct Addition {
+
    pub line: Line,
+
    pub line_no: u32,
+
}
+

+
/// A deletion of a [`Line`] at the `line_no`.
+
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub struct Deletion {
+
    pub line: Line,
+
    pub line_no: u32,
+
}
+

+
impl Modification {
+
    pub fn addition(line: impl Into<Line>, line_no: u32) -> Self {
+
        Self::Addition(Addition {
            line: line.into(),
-
            line_num,
-
        }
+
            line_no,
+
        })
    }

-
    pub fn deletion(line: impl Into<Line>, line_num: u32) -> Self {
-
        Self::Deletion {
+
    pub fn deletion(line: impl Into<Line>, line_no: u32) -> Self {
+
        Self::Deletion(Deletion {
            line: line.into(),
-
            line_num,
-
        }
+
            line_no,
+
        })
    }

-
    pub fn context(line: impl Into<Line>, line_num_old: u32, line_num_new: u32) -> Self {
+
    pub fn context(line: impl Into<Line>, line_no_old: u32, line_no_new: u32) -> Self {
        Self::Context {
            line: line.into(),
-
            line_num_old,
-
            line_num_new,
-
        }
-
    }
-
}
-

-
impl Diff {
-
    pub fn new() -> Self {
-
        Diff {
-
            created: Vec::new(),
-
            deleted: Vec::new(),
-
            moved: Vec::new(),
-
            copied: Vec::new(),
-
            modified: Vec::new(),
-
        }
-
    }
-

-
    pub(crate) fn add_modified_file(
-
        &mut self,
-
        path: PathBuf,
-
        hunks: impl Into<Hunks>,
-
        eof: Option<EofNewLine>,
-
    ) {
-
        // TODO: file diff can be calculated at this point
-
        // Use pijul's transaction diff as an inspiration?
-
        // https://nest.pijul.com/pijul_org/pijul:master/1468b7281a6f3785e9#anesp4Qdq3V
-
        self.modified.push(ModifiedFile {
-
            path,
-
            diff: FileDiff::Plain {
-
                hunks: hunks.into(),
-
            },
-
            eof,
-
        });
-
    }
-

-
    pub(crate) fn add_moved_file(&mut self, old_path: PathBuf, new_path: PathBuf) {
-
        self.moved.push(MoveFile { old_path, new_path });
-
    }
-

-
    pub(crate) fn add_copied_file(&mut self, old_path: PathBuf, new_path: PathBuf) {
-
        self.copied.push(CopyFile { old_path, new_path });
-
    }
-

-
    pub(crate) fn add_modified_binary_file(&mut self, path: PathBuf) {
-
        self.modified.push(ModifiedFile {
-
            path,
-
            diff: FileDiff::Binary,
-
            eof: None,
-
        });
-
    }
-

-
    pub(crate) fn add_created_file(&mut self, path: PathBuf, diff: FileDiff) {
-
        self.created.push(CreateFile { path, diff });
-
    }
-

-
    pub(crate) fn add_deleted_file(&mut self, path: PathBuf, diff: FileDiff) {
-
        self.deleted.push(DeleteFile { path, diff });
-
    }
-

-
    pub fn stats(&self) -> Stats {
-
        let mut deletions = 0;
-
        let mut insertions = 0;
-

-
        for file in &self.modified {
-
            if let self::FileDiff::Plain { ref hunks } = file.diff {
-
                for hunk in hunks.iter() {
-
                    for line in &hunk.lines {
-
                        match line {
-
                            self::LineDiff::Addition { .. } => insertions += 1,
-
                            self::LineDiff::Deletion { .. } => deletions += 1,
-
                            _ => {},
-
                        }
-
                    }
-
                }
-
            }
-
        }
-

-
        for file in &self.created {
-
            if let self::FileDiff::Plain { ref hunks } = file.diff {
-
                for hunk in hunks.iter() {
-
                    for line in &hunk.lines {
-
                        if let self::LineDiff::Addition { .. } = line {
-
                            insertions += 1
-
                        }
-
                    }
-
                }
-
            }
-
        }
-

-
        for file in &self.deleted {
-
            if let self::FileDiff::Plain { ref hunks } = file.diff {
-
                for hunk in hunks.iter() {
-
                    for line in &hunk.lines {
-
                        if let self::LineDiff::Deletion { .. } = line {
-
                            deletions += 1
-
                        }
-
                    }
-
                }
-
            }
-
        }
-

-
        Stats {
-
            files_changed: self.modified.len() + self.created.len() + self.deleted.len(),
-
            insertions,
-
            deletions,
+
            line_no_old,
+
            line_no_new,
        }
    }
}
modified radicle-surf/src/diff/git.rs
@@ -17,16 +17,34 @@

use std::convert::TryFrom;

-
use crate::diff::{self, Diff, EofNewLine, Hunk, Hunks, Line, LineDiff};
+
use crate::diff::{self, Addition, Deletion, Diff, EofNewLine, Hunk, Hunks, Line, Modification};

pub mod error {
    use std::path::PathBuf;

    use thiserror::Error;

-
    #[derive(Debug, Error, PartialEq, Eq)]
+
    #[derive(Debug, Error)]
    #[non_exhaustive]
-
    pub enum LineDiff {
+
    pub enum Addition {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error("the new line number was missing for an added line")]
+
        MissingNewLineNo,
+
    }
+

+
    #[derive(Debug, Error)]
+
    #[non_exhaustive]
+
    pub enum Deletion {
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
        #[error("the new line number was missing for an deleted line")]
+
        MissingOldLineNo,
+
    }
+

+
    #[derive(Debug, Error)]
+
    #[non_exhaustive]
+
    pub enum Modification {
        /// A Git `DiffLine` is invalid.
        #[error(
            "invalid `git2::DiffLine` which contains no line numbers for either side of the diff"
@@ -34,19 +52,23 @@ pub mod error {
        Invalid,
    }

-
    #[derive(Debug, Error, PartialEq)]
+
    #[derive(Debug, Error)]
    #[non_exhaustive]
    pub enum Hunk {
        #[error(transparent)]
        Git(#[from] git2::Error),
        #[error(transparent)]
-
        Line(#[from] LineDiff),
+
        Line(#[from] Modification),
    }

    /// A Git diff error.
-
    #[derive(Debug, PartialEq, Error)]
+
    #[derive(Debug, Error)]
    #[non_exhaustive]
    pub enum Diff {
+
        #[error(transparent)]
+
        Addition(#[from] Addition),
+
        #[error(transparent)]
+
        Deletion(#[from] Deletion),
        /// A Git delta type isn't currently handled.
        #[error("git delta type is not handled")]
        DeltaUnhandled(git2::Delta),
@@ -55,7 +77,7 @@ pub mod error {
        #[error(transparent)]
        Hunk(#[from] Hunk),
        #[error(transparent)]
-
        Line(#[from] LineDiff),
+
        Line(#[from] Modification),
        /// A patch is unavailable.
        #[error("couldn't retrieve patch for {0}")]
        PatchUnavailable(PathBuf),
@@ -65,15 +87,116 @@ pub mod error {
    }
}

-
impl<'a> TryFrom<git2::DiffLine<'a>> for LineDiff {
-
    type Error = error::LineDiff;
+
impl TryFrom<git2::Patch<'_>> for Hunks<Modification> {
+
    type Error = error::Hunk;
+

+
    fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
+
        let mut hunks = Vec::new();
+
        for h in 0..patch.num_hunks() {
+
            let (hunk, hunk_lines) = patch.hunk(h)?;
+
            let header = Line(hunk.header().to_owned());
+
            let mut lines: Vec<Modification> = Vec::new();
+

+
            for l in 0..hunk_lines {
+
                let line = patch.line_in_hunk(h, l)?;
+
                let line = Modification::try_from(line)?;
+
                lines.push(line);
+
            }
+
            hunks.push(Hunk { header, lines });
+
        }
+
        Ok(Hunks(hunks))
+
    }
+
}
+

+
impl TryFrom<git2::Patch<'_>> for Hunks<Addition> {
+
    type Error = error::Addition;
+

+
    fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
+
        let mut hunks = Vec::with_capacity(patch.num_hunks());
+
        for h in 0..patch.num_hunks() {
+
            let (hunk, hunk_lines) = patch.hunk(h)?;
+
            let header = Line(hunk.header().to_owned());
+
            let mut lines: Vec<Addition> = Vec::with_capacity(hunk_lines);
+

+
            for l in 0..hunk_lines {
+
                let line = patch.line_in_hunk(h, l)?;
+
                lines.push(Addition::try_from(line)?);
+
            }
+
            hunks.push(Hunk { header, lines })
+
        }
+
        Ok(hunks.into())
+
    }
+
}
+

+
impl TryFrom<git2::Patch<'_>> for Hunks<Deletion> {
+
    type Error = git2::Error;
+

+
    fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
+
        let mut hunks = Vec::with_capacity(patch.num_hunks());
+
        for h in 0..patch.num_hunks() {
+
            let (hunk, hunk_lines) = patch.hunk(h)?;
+
            let header = Line(hunk.header().to_owned());
+
            let mut lines: Vec<Deletion> = Vec::with_capacity(hunk_lines);
+

+
            for l in 0..hunk_lines {
+
                let line = patch.line_in_hunk(h, l)?;
+
                lines.push(Deletion::try_from(line).expect("TODO"));
+
            }
+
            hunks.push(Hunk { header, lines })
+
        }
+
        Ok(hunks.into())
+
    }
+
}
+

+
impl<'a> TryFrom<git2::DiffLine<'a>> for Modification {
+
    type Error = error::Modification;

    fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
        match (line.old_lineno(), line.new_lineno()) {
            (None, Some(n)) => Ok(Self::addition(line.content().to_owned(), n)),
            (Some(n), None) => Ok(Self::deletion(line.content().to_owned(), n)),
            (Some(l), Some(r)) => Ok(Self::context(line.content().to_owned(), l, r)),
-
            (None, None) => Err(error::LineDiff::Invalid),
+
            (None, None) => Err(error::Modification::Invalid),
+
        }
+
    }
+
}
+

+
impl TryFrom<git2::DiffLine<'_>> for Addition {
+
    type Error = error::Addition;
+

+
    fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
+
        debug_assert!(
+
            line.old_lineno().is_none(),
+
            "trying to build Addition with a modified line"
+
        );
+
        Ok(Self {
+
            line: line.content().to_owned().into(),
+
            line_no: line.new_lineno().ok_or(error::Addition::MissingNewLineNo)?,
+
        })
+
    }
+
}
+

+
impl TryFrom<git2::DiffLine<'_>> for Deletion {
+
    type Error = error::Deletion;
+

+
    fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
+
        debug_assert!(
+
            line.new_lineno().is_none(),
+
            "trying to build Deletion with a modified line"
+
        );
+
        Ok(Self {
+
            line: line.content().to_owned().into(),
+
            line_no: line.old_lineno().ok_or(error::Deletion::MissingOldLineNo)?,
+
        })
+
    }
+
}
+

+
impl From<git2::DiffStats> for diff::Stats {
+
    fn from(stats: git2::DiffStats) -> Self {
+
        Self {
+
            files_changed: stats.files_changed(),
+
            insertions: stats.insertions(),
+
            deletions: stats.deletions(),
        }
    }
}
@@ -82,137 +205,18 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
    type Error = error::Diff;

    fn try_from(git_diff: git2::Diff) -> Result<Diff, Self::Error> {
-
        use git2::{Delta, Patch};
+
        use git2::Delta;

        let mut diff = Diff::new();
+
        diff.stats = git_diff.stats()?.into();

        for (idx, delta) in git_diff.deltas().enumerate() {
            match delta.status() {
-
                Delta::Added => {
-
                    let diff_file = delta.new_file();
-
                    let path = diff_file
-
                        .path()
-
                        .ok_or(error::Diff::PathUnavailable)?
-
                        .to_path_buf();
-

-
                    let patch = Patch::from_diff(&git_diff, idx)?;
-
                    if let Some(patch) = patch {
-
                        diff.add_created_file(
-
                            path,
-
                            diff::FileDiff::Plain {
-
                                hunks: Hunks::try_from(patch)?,
-
                            },
-
                        );
-
                    } else {
-
                        diff.add_created_file(
-
                            path,
-
                            diff::FileDiff::Plain {
-
                                hunks: Hunks::default(),
-
                            },
-
                        );
-
                    }
-
                },
-
                Delta::Deleted => {
-
                    let diff_file = delta.old_file();
-
                    let path = diff_file
-
                        .path()
-
                        .ok_or(error::Diff::PathUnavailable)?
-
                        .to_path_buf();
-
                    let patch = Patch::from_diff(&git_diff, idx)?;
-
                    if let Some(patch) = patch {
-
                        diff.add_deleted_file(
-
                            path,
-
                            diff::FileDiff::Plain {
-
                                hunks: Hunks::try_from(patch)?,
-
                            },
-
                        );
-
                    } else {
-
                        diff.add_deleted_file(
-
                            path,
-
                            diff::FileDiff::Plain {
-
                                hunks: Hunks::default(),
-
                            },
-
                        );
-
                    }
-
                },
-
                Delta::Modified => {
-
                    let diff_file = delta.new_file();
-
                    let path = diff_file
-
                        .path()
-
                        .ok_or(error::Diff::PathUnavailable)?
-
                        .to_path_buf();
-
                    let patch = Patch::from_diff(&git_diff, idx)?;
-

-
                    if let Some(patch) = patch {
-
                        let mut hunks: Vec<Hunk> = Vec::new();
-
                        let mut old_missing_eof = false;
-
                        let mut new_missing_eof = false;
-

-
                        for h in 0..patch.num_hunks() {
-
                            let (hunk, hunk_lines) = patch.hunk(h)?;
-
                            let header = Line(hunk.header().to_owned());
-
                            let mut lines: Vec<LineDiff> = Vec::new();
-

-
                            for l in 0..hunk_lines {
-
                                let line = patch.line_in_hunk(h, l)?;
-
                                match line.origin_value() {
-
                                    git2::DiffLineType::ContextEOFNL => {
-
                                        new_missing_eof = true;
-
                                        old_missing_eof = true;
-
                                        continue;
-
                                    },
-
                                    git2::DiffLineType::AddEOFNL => {
-
                                        old_missing_eof = true;
-
                                        continue;
-
                                    },
-
                                    git2::DiffLineType::DeleteEOFNL => {
-
                                        new_missing_eof = true;
-
                                        continue;
-
                                    },
-
                                    _ => {},
-
                                }
-
                                let line = LineDiff::try_from(line)?;
-
                                lines.push(line);
-
                            }
-
                            hunks.push(Hunk { header, lines });
-
                        }
-
                        let eof = match (old_missing_eof, new_missing_eof) {
-
                            (true, true) => Some(EofNewLine::BothMissing),
-
                            (true, false) => Some(EofNewLine::OldMissing),
-
                            (false, true) => Some(EofNewLine::NewMissing),
-
                            (false, false) => None,
-
                        };
-
                        diff.add_modified_file(path, hunks, eof);
-
                    } else if diff_file.is_binary() {
-
                        diff.add_modified_binary_file(path);
-
                    } else {
-
                        return Err(error::Diff::PatchUnavailable(path));
-
                    }
-
                },
-
                Delta::Renamed => {
-
                    let old = delta
-
                        .old_file()
-
                        .path()
-
                        .ok_or(error::Diff::PathUnavailable)?;
-
                    let new = delta
-
                        .new_file()
-
                        .path()
-
                        .ok_or(error::Diff::PathUnavailable)?;
-

-
                    diff.add_moved_file(old.to_path_buf(), new.to_path_buf());
-
                },
-
                Delta::Copied => {
-
                    let old = delta
-
                        .old_file()
-
                        .path()
-
                        .ok_or(error::Diff::PathUnavailable)?;
-
                    let new = delta
-
                        .new_file()
-
                        .path()
-
                        .ok_or(error::Diff::PathUnavailable)?;
-

-
                    diff.add_copied_file(old.to_path_buf(), new.to_path_buf());
-
                },
+
                Delta::Added => created(&mut diff, &git_diff, idx, &delta)?,
+
                Delta::Deleted => deleted(&mut diff, &git_diff, idx, &delta)?,
+
                Delta::Modified => modified(&mut diff, &git_diff, idx, &delta)?,
+
                Delta::Renamed => renamed(&mut diff, &delta)?,
+
                Delta::Copied => copied(&mut diff, &delta)?,
                status => {
                    return Err(error::Diff::DeltaUnhandled(status));
                },
@@ -222,3 +226,154 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
        Ok(diff)
    }
}
+

+
fn created(
+
    diff: &mut Diff,
+
    git_diff: &git2::Diff<'_>,
+
    idx: usize,
+
    delta: &git2::DiffDelta<'_>,
+
) -> Result<(), error::Diff> {
+
    let diff_file = delta.new_file();
+
    let path = diff_file
+
        .path()
+
        .ok_or(error::Diff::PathUnavailable)?
+
        .to_path_buf();
+

+
    let patch = git2::Patch::from_diff(git_diff, idx)?;
+
    if let Some(patch) = patch {
+
        diff.added(
+
            path,
+
            diff::FileDiff::Plain {
+
                hunks: Hunks::try_from(patch)?,
+
            },
+
        );
+
    } else {
+
        diff.added(
+
            path,
+
            diff::FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
        );
+
    }
+
    Ok(())
+
}
+

+
fn deleted(
+
    diff: &mut Diff,
+
    git_diff: &git2::Diff<'_>,
+
    idx: usize,
+
    delta: &git2::DiffDelta<'_>,
+
) -> Result<(), error::Diff> {
+
    let diff_file = delta.old_file();
+
    let path = diff_file
+
        .path()
+
        .ok_or(error::Diff::PathUnavailable)?
+
        .to_path_buf();
+
    let patch = git2::Patch::from_diff(git_diff, idx)?;
+
    if let Some(patch) = patch {
+
        diff.deleted(
+
            path,
+
            diff::FileDiff::Plain {
+
                hunks: Hunks::try_from(patch)?,
+
            },
+
        );
+
    } else {
+
        diff.deleted(
+
            path,
+
            diff::FileDiff::Plain {
+
                hunks: Hunks::default(),
+
            },
+
        );
+
    }
+
    Ok(())
+
}
+

+
fn modified(
+
    diff: &mut Diff,
+
    git_diff: &git2::Diff<'_>,
+
    idx: usize,
+
    delta: &git2::DiffDelta<'_>,
+
) -> Result<(), error::Diff> {
+
    let diff_file = delta.new_file();
+
    let path = diff_file
+
        .path()
+
        .ok_or(error::Diff::PathUnavailable)?
+
        .to_path_buf();
+
    let patch = git2::Patch::from_diff(git_diff, idx)?;
+

+
    if let Some(patch) = patch {
+
        let mut hunks: Vec<Hunk<Modification>> = Vec::new();
+
        let mut old_missing_eof = false;
+
        let mut new_missing_eof = false;
+

+
        for h in 0..patch.num_hunks() {
+
            let (hunk, hunk_lines) = patch.hunk(h)?;
+
            let header = Line(hunk.header().to_owned());
+
            let mut lines: Vec<Modification> = Vec::new();
+

+
            for l in 0..hunk_lines {
+
                let line = patch.line_in_hunk(h, l)?;
+
                match line.origin_value() {
+
                    git2::DiffLineType::ContextEOFNL => {
+
                        new_missing_eof = true;
+
                        old_missing_eof = true;
+
                        continue;
+
                    },
+
                    git2::DiffLineType::AddEOFNL => {
+
                        old_missing_eof = true;
+
                        continue;
+
                    },
+
                    git2::DiffLineType::DeleteEOFNL => {
+
                        new_missing_eof = true;
+
                        continue;
+
                    },
+
                    _ => {},
+
                }
+
                let line = Modification::try_from(line)?;
+
                lines.push(line);
+
            }
+
            hunks.push(Hunk { header, lines });
+
        }
+
        let eof = match (old_missing_eof, new_missing_eof) {
+
            (true, true) => Some(EofNewLine::BothMissing),
+
            (true, false) => Some(EofNewLine::OldMissing),
+
            (false, true) => Some(EofNewLine::NewMissing),
+
            (false, false) => None,
+
        };
+
        diff.modified(path, hunks, eof);
+
        Ok(())
+
    } else if diff_file.is_binary() {
+
        diff.modified_binary(path);
+
        Ok(())
+
    } else {
+
        Err(error::Diff::PatchUnavailable(path))
+
    }
+
}
+

+
fn renamed(diff: &mut Diff, delta: &git2::DiffDelta<'_>) -> Result<(), error::Diff> {
+
    let old = delta
+
        .old_file()
+
        .path()
+
        .ok_or(error::Diff::PathUnavailable)?;
+
    let new = delta
+
        .new_file()
+
        .path()
+
        .ok_or(error::Diff::PathUnavailable)?;
+

+
    diff.moved(old.to_path_buf(), new.to_path_buf());
+
    Ok(())
+
}
+

+
fn copied(diff: &mut Diff, delta: &git2::DiffDelta<'_>) -> Result<(), error::Diff> {
+
    let old = delta
+
        .old_file()
+
        .path()
+
        .ok_or(error::Diff::PathUnavailable)?;
+
    let new = delta
+
        .new_file()
+
        .path()
+
        .ok_or(error::Diff::PathUnavailable)?;
+

+
    diff.copied(old.to_path_buf(), new.to_path_buf());
+
    Ok(())
+
}
modified radicle-surf/src/git/repo.rs
@@ -171,15 +171,12 @@ impl Repository {
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
    }

-
    /// Get the [`Diff`] of a commit with no parents.
-
    pub fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
-
        let commit = self.get_git2_commit(self.object_id(&rev)?)?;
-
        self.diff_commits(None, None, &commit)
-
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
-
    }
-

-
    /// Get the diff introduced by a particlar rev.
-
    pub fn diff_from_parent<C: ToCommit>(&self, commit: C) -> Result<Diff, Error> {
+
    /// Get the [`Diff`] of a `commit`.
+
    ///
+
    /// If the `commit` has a parent, then it the diff will be a
+
    /// comparison between itself and that parent. Otherwise, the left
+
    /// hand side of the diff will pass nothing.
+
    pub fn diff_commit(&self, commit: impl ToCommit) -> Result<Diff, Error> {
        let commit = commit
            .to_commit(self)
            .map_err(|err| Error::ToCommit(err.into()))?;
@@ -371,6 +368,13 @@ impl Repository {
        Ok(contained_branches)
    }

+
    /// Get the [`Diff`] of a commit with no parents.
+
    fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
+
        let commit = self.get_git2_commit(self.object_id(&rev)?)?;
+
        self.diff_commits(None, None, &commit)
+
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
+
    }
+

    fn reachable_from(&self, reference: &git2::Reference, oid: &Oid) -> Result<bool, Error> {
        let git2_oid = (*oid).into();
        let other = reference.peel_to_commit()?.id();
modified radicle-surf/src/source/commit.rs
@@ -34,24 +34,12 @@ use crate::{

use radicle_git_ext::Oid;

-
/// Commit statistics.
-
#[cfg_attr(feature = "serde", derive(Serialize))]
-
#[derive(Clone)]
-
pub struct Stats {
-
    /// Additions.
-
    pub additions: u64,
-
    /// Deletions.
-
    pub deletions: u64,
-
}
-

/// Representation of a changeset between two revs.
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone)]
pub struct Commit {
    /// The commit header.
    pub header: Header,
-
    /// The change statistics for this commit.
-
    pub stats: Stats,
    /// The changeset introduced by this commit.
    pub diff: diff::Diff,
    /// The list of branches this commit belongs to.
@@ -150,48 +138,7 @@ pub fn commit<R: git::Revision>(repo: &Repository, rev: R) -> Result<Commit, Err
    let commit = repo.commit(rev)?;
    let sha1 = commit.id;
    let header = Header::from(&commit);
-
    let diff = repo.diff_from_parent(commit)?;
-

-
    let mut deletions = 0;
-
    let mut additions = 0;
-

-
    for file in &diff.modified {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    match line {
-
                        diff::LineDiff::Addition { .. } => additions += 1,
-
                        diff::LineDiff::Deletion { .. } => deletions += 1,
-
                        _ => {},
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    for file in &diff.created {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    if let diff::LineDiff::Addition { .. } = line {
-
                        additions += 1
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    for file in &diff.deleted {
-
        if let diff::FileDiff::Plain { ref hunks } = file.diff {
-
            for hunk in hunks.iter() {
-
                for line in &hunk.lines {
-
                    if let diff::LineDiff::Deletion { .. } = line {
-
                        deletions += 1
-
                    }
-
                }
-
            }
-
        }
-
    }
+
    let diff = repo.diff_commit(commit)?;

    let branches = repo
        .revision_branches(&sha1, Glob::all_heads().branches().and(Glob::all_remotes()))?
@@ -201,10 +148,6 @@ pub fn commit<R: git::Revision>(repo: &Repository, rev: R) -> Result<Commit, Err

    Ok(Commit {
        header,
-
        stats: Stats {
-
            additions,
-
            deletions,
-
        },
        diff,
        branches,
    })
deleted radicle-surf/t/src/diff.rs
@@ -1,129 +0,0 @@
-
// Copyright © 2022 The Radicle Git Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
//! Unit tests for radicle_surf::diff
-

-
use pretty_assertions::assert_eq;
-
use radicle_surf::diff::*;
-

-
/* TODO(fintan): Move is not detected yet
-
#[test]
-
fn test_moved_file() {
-
    let mut directory = Directory::root();
-
    directory.insert_file(&unsound::path::new("mod.rs"), File::new(b"use banana"));
-

-
    let mut new_directory = Directory::root();
-
    new_directory.insert_file(&unsound::path::new("banana.rs"), File::new(b"use banana"));
-

-
    let diff = Diff::diff(directory, new_directory).expect("diff failed");
-

-
    assert_eq!(diff, Diff::new())
-
}
-
*/
-

-
/* TODO(fintan): Tricky stuff
-
#[test]
-
fn test_disjoint_directories() {
-
    let mut directory = Directory::root();
-
    directory.insert_file(
-
        &unsound::path::new("foo/src/banana.rs"),
-
        File::new(b"use banana"),
-
    );
-

-
    let mut other_directory = Directory::root();
-
    other_directory.insert_file(
-
        &unsound::path::new("bar/src/pineapple.rs"),
-
        File::new(b"use pineapple"),
-
    );
-

-
    let diff = Diff::diff(directory, other_directory).expect("diff failed");
-

-
    let expected_diff = Diff {
-
        created: vec![CreateFile(Path::from_labels(
-
            unsound::label::new("bar"),
-
            &[
-
                unsound::label::new("src"),
-
                unsound::label::new("pineapple.rs"),
-
            ],
-
        ))],
-
        deleted: vec![DeleteFile(Path::from_labels(
-
            unsound::label::new("foo"),
-
            &[unsound::label::new("src"), unsound::label::new("banana.rs")],
-
        ))],
-
        moved: vec![],
-
        modified: vec![],
-
    };
-

-
    assert_eq!(diff, expected_diff)
-
}
-
*/
-

-
#[test]
-
fn test_both_missing_eof_newline() {
-
    let buf = r#"
-
diff --git a/.env b/.env
-
index f89e4c0..7c56eb7 100644
-
--- a/.env
-
+++ b/.env
-
@@ -1 +1 @@
-
-hello=123
-
\ No newline at end of file
-
+hello=1234
-
\ No newline at end of file
-
"#;
-
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
    let diff = Diff::try_from(diff).unwrap();
-
    assert_eq!(diff.modified[0].eof, Some(EofNewLine::BothMissing));
-
}
-

-
#[test]
-
fn test_none_missing_eof_newline() {
-
    let buf = r#"
-
diff --git a/.env b/.env
-
index f89e4c0..7c56eb7 100644
-
--- a/.env
-
+++ b/.env
-
@@ -1 +1 @@
-
-hello=123
-
+hello=1234
-
"#;
-
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
    let diff = Diff::try_from(diff).unwrap();
-
    assert_eq!(diff.modified[0].eof, None);
-
}
-

-
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
-
//#[test]
-
//     fn test_old_missing_eof_newline() {
-
//         let buf = r#"
-
// diff --git a/.env b/.env
-
// index f89e4c0..7c56eb7 100644
-
// --- a/.env
-
// +++ b/.env
-
// @@ -1 +1 @@
-
// -hello=123
-
// \ No newline at end of file
-
// +hello=1234
-
// "#;
-
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
//         let diff = Diff::try_from(diff).unwrap();
-
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::OldMissing));
-
//     }
-

-
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
-
//#[test]
-
//     fn test_new_missing_eof_newline() {
-
//         let buf = r#"
-
// diff --git a/.env b/.env
-
// index f89e4c0..7c56eb7 100644
-
// --- a/.env
-
// +++ b/.env
-
// @@ -1 +1 @@
-
// -hello=123
-
// +hello=1234
-
// \ No newline at end of file
-
// "#;
-
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
-
//         let diff = Diff::try_from(diff).unwrap();
-
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::NewMissing));
-
//     }
modified radicle-surf/t/src/git/diff.rs
@@ -2,7 +2,20 @@ use git_ref_format::refname;
use pretty_assertions::assert_eq;
use radicle_git_ext::Oid;
use radicle_surf::{
-
    diff::{CreateFile, Diff, FileDiff, Hunk, Line, LineDiff, ModifiedFile},
+
    diff::{
+
        Added,
+
        Addition,
+
        Diff,
+
        EofNewLine,
+
        FileDiff,
+
        Hunk,
+
        Hunks,
+
        Line,
+
        Modification,
+
        Modified,
+
        Moved,
+
        Stats,
+
    },
    git::{Branch, Error, Repository},
};
use std::{path::Path, str::FromStr};
@@ -16,19 +29,21 @@ fn test_initial_diff() -> Result<(), Error> {
    let commit = repo.commit(oid).unwrap();
    assert!(commit.parents.is_empty());

-
    let diff = repo.initial_diff(oid)?;
+
    let diff = repo.diff_commit(oid)?;

    let expected_diff = Diff {
-
        created: vec![CreateFile {
+
        added: vec![Added {
            path: Path::new("README.md").to_path_buf(),
            diff: FileDiff::Plain {
                hunks: vec![Hunk {
                    header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()),
-
                    lines: vec![LineDiff::addition(
-
                        b"This repository is a data source for the Upstream front-end tests.\n"
-
                            .to_vec(),
-
                        1,
-
                    )],
+
                    lines: vec![Addition {
+
                        line:
+
                            b"This repository is a data source for the Upstream front-end tests.\n"
+
                                .to_vec()
+
                                .into(),
+
                        line_no: 1,
+
                    }],
                }]
                .into(),
            },
@@ -37,6 +52,11 @@ fn test_initial_diff() -> Result<(), Error> {
        moved: vec![],
        copied: vec![],
        modified: vec![],
+
        stats: Stats {
+
            files_changed: 1,
+
            insertions: 1,
+
            deletions: 0,
+
        },
    };
    assert_eq!(expected_diff, diff);

@@ -46,8 +66,8 @@ fn test_initial_diff() -> Result<(), Error> {
#[test]
fn test_diff_of_rev() -> Result<(), Error> {
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let diff = repo.diff_from_parent("80bacafba303bf0cdf6142921f430ff265f25095")?;
-
    assert_eq!(diff.created.len(), 0);
+
    let diff = repo.diff_commit("80bacafba303bf0cdf6142921f430ff265f25095")?;
+
    assert_eq!(diff.added.len(), 0);
    assert_eq!(diff.deleted.len(), 0);
    assert_eq!(diff.moved.len(), 0);
    assert_eq!(diff.modified.len(), 1);
@@ -63,25 +83,30 @@ fn test_diff() -> Result<(), Error> {
    let diff = repo.diff(*parent_oid, oid)?;

    let expected_diff = Diff {
-
                created: vec![],
-
                deleted: vec![],
-
                moved: vec![],
-
                copied: vec![],
-
                modified: vec![ModifiedFile {
-
                    path: Path::new("README.md").to_path_buf(),
-
                    diff: FileDiff::Plain {
-
                        hunks: vec![Hunk {
-
                            header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
-
                            lines: vec![
-
                                LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
-
                                LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
-
                                LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
-
                            ]
-
                        }].into()
-
                    },
-
                    eof: None,
-
                }]
-
            };
+
        added: vec![],
+
        deleted: vec![],
+
        moved: vec![],
+
        copied: vec![],
+
        modified: vec![Modified {
+
            path: Path::new("README.md").to_path_buf(),
+
            diff: FileDiff::Plain {
+
                hunks: vec![Hunk {
+
                    header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
+
                    lines: vec![
+
                        Modification::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
+
                        Modification::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
+
                        Modification::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
+
                    ]
+
                }].into()
+
            },
+
            eof: None,
+
        }],
+
        stats: Stats {
+
            files_changed: 1,
+
            insertions: 2,
+
            deletions: 1,
+
        },
+
    };
    assert_eq!(expected_diff, diff);

    Ok(())
@@ -97,18 +122,18 @@ fn test_branch_diff() -> Result<(), Error> {

    println!("Diff two branches: master -> dev");
    println!(
-
        "result: created {} deleted {} moved {} modified {}",
-
        diff.created.len(),
+
        "result: added {} deleted {} moved {} modified {}",
+
        diff.added.len(),
        diff.deleted.len(),
        diff.moved.len(),
        diff.modified.len()
    );
-
    assert_eq!(diff.created.len(), 1);
+
    assert_eq!(diff.added.len(), 1);
    assert_eq!(diff.deleted.len(), 11);
    assert_eq!(diff.moved.len(), 1);
    assert_eq!(diff.modified.len(), 2);
-
    for c in diff.created.iter() {
-
        println!("created: {:?}", &c.path);
+
    for c in diff.added.iter() {
+
        println!("added: {:?}", &c.path);
    }
    for d in diff.deleted.iter() {
        println!("deleted: {:?}", &d.path);
@@ -124,39 +149,42 @@ fn test_branch_diff() -> Result<(), Error> {

#[test]
fn test_diff_serde() {
-
    use radicle_surf::diff::{Hunks, MoveFile};
-

    let diff = Diff {
-
        created: vec![ CreateFile {
+
        added: vec![ Added {
            path: Path::new("LICENSE").to_path_buf(),
            diff: FileDiff::Plain { hunks: Hunks::default() }
        }],
        deleted: vec![],
-
        moved: vec![ MoveFile {
+
        moved: vec![ Moved {
            old_path: Path::new("CONTRIBUTING").to_path_buf(),
            new_path: Path::new("CONTRIBUTING.md").to_path_buf(),
        }],
        copied: vec![],
-
        modified: vec![ ModifiedFile {
+
        modified: vec![ Modified {
            path: Path::new("README.md").to_path_buf(),
            diff: FileDiff::Plain {
                hunks: vec![Hunk {
                header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
                lines: vec![
-
                    LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
-
                    LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
-
                    LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
-
                    LineDiff::context(b"\n".to_vec(), 3, 4),
+
                    Modification::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1),
+
                    Modification::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1),
+
                    Modification::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2),
+
                    Modification::context(b"\n".to_vec(), 3, 4),
                ]
                }].into()
            },
            eof: None,
-
        }]
+
        }],
+
        stats: Stats {
+
            files_changed: 3,
+
            insertions: 2,
+
            deletions: 1,
+
        },
    };

    let eof: Option<u8> = None;
    let json = serde_json::json!({
-
        "created": [{"path": "LICENSE", "diff": {
+
        "added": [{"path": "LICENSE", "diff": {
                "type": "plain",
                "hunks": []
            },
@@ -171,19 +199,19 @@ fn test_diff_serde() {
                "hunks": [{
                    "header": "@@ -1 +1,2 @@\n",
                    "lines": [
-
                        { "lineNum": 1,
+
                        { "lineNo": 1,
                          "line": "This repository is a data source for the Upstream front-end tests.\n",
                          "type": "deletion"
                        },
-
                        { "lineNum": 1,
+
                        { "lineNo": 1,
                          "line": "This repository is a data source for the Upstream front-end tests and the\n",
                          "type": "addition"
                        },
-
                        { "lineNum": 2,
+
                        { "lineNo": 2,
                          "line": "[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n",
                          "type": "addition"
                        },
-
                        { "lineNumOld": 3, "lineNumNew": 4,
+
                        { "lineNoOld": 3, "lineNoNew": 4,
                          "line": "\n",
                          "type": "context"
                        }
@@ -191,7 +219,82 @@ fn test_diff_serde() {
                }]
            },
            "eof" : eof,
-
        }]
+
        }],
+
        "stats": {
+
            "deletions": 1,
+
            "filesChanged": 3,
+
            "insertions": 2,
+
        }
    });
    assert_eq!(serde_json::to_value(&diff).unwrap(), json);
}
+

+
#[test]
+
fn test_both_missing_eof_newline() {
+
    let buf = r#"
+
diff --git a/.env b/.env
+
index f89e4c0..7c56eb7 100644
+
--- a/.env
+
+++ b/.env
+
@@ -1 +1 @@
+
-hello=123
+
\ No newline at end of file
+
+hello=1234
+
\ No newline at end of file
+
"#;
+
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
    let diff = Diff::try_from(diff).unwrap();
+
    assert_eq!(diff.modified[0].eof, Some(EofNewLine::BothMissing));
+
}
+

+
#[test]
+
fn test_none_missing_eof_newline() {
+
    let buf = r#"
+
diff --git a/.env b/.env
+
index f89e4c0..7c56eb7 100644
+
--- a/.env
+
+++ b/.env
+
@@ -1 +1 @@
+
-hello=123
+
+hello=1234
+
"#;
+
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
    let diff = Diff::try_from(diff).unwrap();
+
    assert_eq!(diff.modified[0].eof, None);
+
}
+

+
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
+
//#[test]
+
//     fn test_old_missing_eof_newline() {
+
//         let buf = r#"
+
// diff --git a/.env b/.env
+
// index f89e4c0..7c56eb7 100644
+
// --- a/.env
+
// +++ b/.env
+
// @@ -1 +1 @@
+
// -hello=123
+
// \ No newline at end of file
+
// +hello=1234
+
// "#;
+
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
//         let diff = Diff::try_from(diff).unwrap();
+
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::OldMissing));
+
//     }
+

+
// TODO(xphoniex): uncomment once libgit2 has fixed the bug
+
//#[test]
+
//     fn test_new_missing_eof_newline() {
+
//         let buf = r#"
+
// diff --git a/.env b/.env
+
// index f89e4c0..7c56eb7 100644
+
// --- a/.env
+
// +++ b/.env
+
// @@ -1 +1 @@
+
// -hello=123
+
// +hello=1234
+
// \ No newline at end of file
+
// "#;
+
//         let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
+
//         let diff = Diff::try_from(diff).unwrap();
+
//         assert_eq!(diff.modified[0].eof, Some(EofNewLine::NewMissing));
+
//     }
modified radicle-surf/t/src/lib.rs
@@ -3,6 +3,3 @@

#[cfg(test)]
mod git;
-

-
#[cfg(test)]
-
mod diff;