Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
add diff_file method for Repository.
Han Xu committed 3 years ago
commit 41a2cf5bdc181df654f8c0f4caea397cdb4fd3be
parent e7766d6
9 files changed +401 -213
modified radicle-surf/data/git-platinum.tgz
modified radicle-surf/examples/diff.rs
@@ -57,22 +57,22 @@ fn init_repository_or_exit(path_to_repo: &str) -> Repository {
}

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

    println!(
        "created {} / deleted {} / modified {} / total {}",
-
        diff.added.len(),
-
        diff.deleted.len(),
-
        diff.modified.len(),
-
        diff.added.len() + diff.deleted.len() + diff.modified.len()
+
        diff.added().count(),
+
        diff.deleted().count(),
+
        diff.modified().count(),
+
        diff.added().count() + diff.deleted().count() + diff.modified().count()
    );
    println!("diff took {elapsed_nanos} nanos ");
}
modified radicle-surf/scripts/update-git-platinum.sh
@@ -23,6 +23,7 @@ rm -rf "$PLATINUM_REPO"
# Clone an up-to-date version of git-platinum.
git clone https://github.com/radicle-dev/git-platinum.git "$PLATINUM_REPO"
git -C "$PLATINUM_REPO" checkout empty-branch
+
git -C "$PLATINUM_REPO" checkout diff-test
git -C "$PLATINUM_REPO" checkout dev

# Add the necessary refs.
modified radicle-surf/src/diff.rs
@@ -20,7 +20,7 @@
use std::path::PathBuf;

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

pub mod git;

@@ -29,59 +29,115 @@ pub mod git;
/// A [`Diff`] can be retrieved by the following functions:
///    * [`crate::Repository::diff`]
///    * [`crate::Repository::diff_commit`]
-
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Diff {
-
    pub added: Vec<Added>,
-
    pub deleted: Vec<Deleted>,
-
    pub moved: Vec<Moved>,
-
    pub copied: Vec<Copied>,
-
    pub modified: Vec<Modified>,
-
    pub stats: Stats,
+
    files: Vec<FileDiff>,
+
    stats: Stats,
}

impl Diff {
-
    pub fn new() -> Self {
+
    /// Creates an empty diff.
+
    pub(crate) fn new() -> Self {
        Diff::default()
    }

-
    fn modified(
+
    /// Returns an iterator of the file in the diff.
+
    pub fn files(&self) -> impl Iterator<Item = &FileDiff> {
+
        self.files.iter()
+
    }
+

+
    /// Returns owned files in the diff.
+
    pub fn into_files(self) -> Vec<FileDiff> {
+
        self.files
+
    }
+

+
    pub fn added(&self) -> impl Iterator<Item = &Added> {
+
        self.files().filter_map(|x| match x {
+
            FileDiff::Added(a) => Some(a),
+
            _ => None,
+
        })
+
    }
+

+
    pub fn deleted(&self) -> impl Iterator<Item = &Deleted> {
+
        self.files().filter_map(|x| match x {
+
            FileDiff::Deleted(a) => Some(a),
+
            _ => None,
+
        })
+
    }
+

+
    pub fn moved(&self) -> impl Iterator<Item = &Moved> {
+
        self.files().filter_map(|x| match x {
+
            FileDiff::Moved(a) => Some(a),
+
            _ => None,
+
        })
+
    }
+

+
    pub fn modified(&self) -> impl Iterator<Item = &Modified> {
+
        self.files().filter_map(|x| match x {
+
            FileDiff::Modified(a) => Some(a),
+
            _ => None,
+
        })
+
    }
+

+
    pub fn copied(&self) -> impl Iterator<Item = &Copied> {
+
        self.files().filter_map(|x| match x {
+
            FileDiff::Copied(a) => Some(a),
+
            _ => None,
+
        })
+
    }
+

+
    pub fn stats(&self) -> &Stats {
+
        &self.stats
+
    }
+

+
    fn insert_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,
-
        })
+
        let diff = DiffContent::Plain {
+
            hunks: hunks.into(),
+
        };
+
        let diff = FileDiff::Modified(Modified { path, eof, diff });
+
        self.files.push(diff)
    }

-
    fn moved(&mut self, old_path: PathBuf, new_path: PathBuf) {
-
        self.moved.push(Moved { old_path, new_path });
+
    fn insert_moved(&mut self, old_path: PathBuf, new_path: PathBuf) {
+
        let diff = FileDiff::Moved(Moved {
+
            old_path,
+
            new_path,
+
            diff: DiffContent::Empty,
+
        });
+
        self.files.push(diff);
    }

-
    fn copied(&mut self, old_path: PathBuf, new_path: PathBuf) {
-
        self.copied.push(Copied { old_path, new_path });
+
    fn insert_copied(&mut self, old_path: PathBuf, new_path: PathBuf) {
+
        let diff = FileDiff::Copied(Copied {
+
            old_path,
+
            new_path,
+
            diff: DiffContent::Empty,
+
        });
+
        self.files.push(diff);
    }

-
    fn modified_binary(&mut self, path: PathBuf) {
-
        self.modified.push(Modified {
+
    fn insert_modified_binary(&mut self, path: PathBuf) {
+
        let diff = FileDiff::Modified(Modified {
            path,
-
            diff: FileDiff::Binary,
            eof: None,
-
        })
+
            diff: DiffContent::Binary,
+
        });
+
        self.files.push(diff)
    }

-
    fn added(&mut self, path: PathBuf, diff: FileDiff<Addition>) {
-
        self.added.push(Added { path, diff })
+
    fn insert_added(&mut self, path: PathBuf, diff: DiffContent) {
+
        let diff = FileDiff::Added(Added { path, diff });
+
        self.files.push(diff);
    }

-
    fn deleted(&mut self, path: PathBuf, diff: FileDiff<Deletion>) {
-
        self.deleted.push(Deleted { path, diff })
+
    fn insert_deleted(&mut self, path: PathBuf, diff: DiffContent) {
+
        let diff = FileDiff::Deleted(Deleted { path, diff });
+
        self.files.push(diff);
    }
}

@@ -91,8 +147,7 @@ impl Diff {
pub struct Added {
    /// The path to this file, relative to the repository root.
    pub path: PathBuf,
-
    /// The set of [`Addition`]s to this file.
-
    pub diff: FileDiff<Addition>,
+
    pub diff: DiffContent,
}

/// A file that was deleted within a [`Diff`].
@@ -101,18 +156,32 @@ pub struct Added {
pub struct Deleted {
    /// The path to this file, relative to the repository root.
    pub path: PathBuf,
-
    /// The set of [`Deletion`]s to this file.
-
    pub diff: FileDiff<Deletion>,
+
    pub diff: DiffContent,
}

/// 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 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,
+
    pub diff: DiffContent,
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for Moved {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Moved", 2)?;
+
        state.serialize_field("oldPath", &self.old_path)?;
+
        state.serialize_field("newPath", &self.new_path)?;
+
        // `DiffContent` is not serialized yet for `Moved`, only
+
        // to keep the serialization same as before.
+
        state.end()
+
    }
}

/// A file that was copied within a [`Diff`].
@@ -123,6 +192,7 @@ pub struct Copied {
    pub old_path: PathBuf,
    /// The new path to this file, relative to the repository root.
    pub new_path: PathBuf,
+
    pub diff: DiffContent,
}

#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
@@ -139,8 +209,7 @@ pub enum EofNewLine {
pub struct Modified {
    /// The path to this file, relative to the repository root.
    pub path: PathBuf,
-
    /// The set of [`Modification`]s to this file.
-
    pub diff: FileDiff<Modification>,
+
    pub diff: DiffContent,
    /// Was there an EOF newline present.
    pub eof: Option<EofNewLine>,
}
@@ -152,12 +221,75 @@ pub struct Modified {
    serde(tag = "type", rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub enum FileDiff<T> {
+
pub enum DiffContent {
    /// 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<T> },
+
    Plain {
+
        hunks: Hunks<Modification>,
+
    },
+
    Empty,
+
}
+

+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub enum FileDiff {
+
    Added(Added),
+
    Deleted(Deleted),
+
    Modified(Modified),
+
    Moved(Moved),
+
    Copied(Copied),
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for FileDiff {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("FileDiff", 7)?;
+
        match &self {
+
            FileDiff::Added(x) => {
+
                state.serialize_field("path", &x.path)?;
+
                state.serialize_field("diff", &x.diff)?
+
            },
+
            FileDiff::Deleted(x) => {
+
                state.serialize_field("path", &x.path)?;
+
                state.serialize_field("diff", &x.diff)?
+
            },
+
            FileDiff::Modified(x) => {
+
                state.serialize_field("path", &x.path)?;
+
                state.serialize_field("diff", &x.diff)?;
+
                state.serialize_field("eof", &x.eof)?
+
            },
+
            FileDiff::Moved(x) => {
+
                state.serialize_field("oldPath", &x.old_path)?;
+
                state.serialize_field("newPath", &x.new_path)?
+
            },
+
            FileDiff::Copied(x) => {
+
                state.serialize_field("oldPath", &x.old_path)?;
+
                state.serialize_field("newPath", &x.new_path)?
+
            },
+
        }
+
        state.end()
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
impl Serialize for Diff {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        let mut state = serializer.serialize_struct("Diff", 6)?;
+
        state.serialize_field("added", &self.added().collect::<Vec<_>>())?;
+
        state.serialize_field("deleted", &self.deleted().collect::<Vec<_>>())?;
+
        state.serialize_field("moved", &self.moved().collect::<Vec<_>>())?;
+
        state.serialize_field("copied", &self.copied().collect::<Vec<_>>())?;
+
        state.serialize_field("modified", &self.modified().collect::<Vec<_>>())?;
+
        state.serialize_field("stats", &self.stats())?;
+
        state.end()
+
    }
}

/// Statistics describing a particular [`Diff`].
modified radicle-surf/src/diff/git.rs
@@ -241,16 +241,16 @@ fn created(

    let patch = git2::Patch::from_diff(git_diff, idx)?;
    if let Some(patch) = patch {
-
        diff.added(
+
        diff.insert_added(
            path,
-
            diff::FileDiff::Plain {
+
            diff::DiffContent::Plain {
                hunks: Hunks::try_from(patch)?,
            },
        );
    } else {
-
        diff.added(
+
        diff.insert_added(
            path,
-
            diff::FileDiff::Plain {
+
            diff::DiffContent::Plain {
                hunks: Hunks::default(),
            },
        );
@@ -271,16 +271,16 @@ fn deleted(
        .to_path_buf();
    let patch = git2::Patch::from_diff(git_diff, idx)?;
    if let Some(patch) = patch {
-
        diff.deleted(
+
        diff.insert_deleted(
            path,
-
            diff::FileDiff::Plain {
+
            diff::DiffContent::Plain {
                hunks: Hunks::try_from(patch)?,
            },
        );
    } else {
-
        diff.deleted(
+
        diff.insert_deleted(
            path,
-
            diff::FileDiff::Plain {
+
            diff::DiffContent::Plain {
                hunks: Hunks::default(),
            },
        );
@@ -340,10 +340,10 @@ fn modified(
            (false, true) => Some(EofNewLine::NewMissing),
            (false, false) => None,
        };
-
        diff.modified(path, hunks, eof);
+
        diff.insert_modified(path, hunks, eof);
        Ok(())
    } else if diff_file.is_binary() {
-
        diff.modified_binary(path);
+
        diff.insert_modified_binary(path);
        Ok(())
    } else {
        Err(error::Diff::PatchUnavailable(path))
@@ -360,7 +360,7 @@ fn renamed(diff: &mut Diff, delta: &git2::DiffDelta<'_>) -> Result<(), error::Di
        .path()
        .ok_or(error::Diff::PathUnavailable)?;

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

@@ -374,6 +374,6 @@ fn copied(diff: &mut Diff, delta: &git2::DiffDelta<'_>) -> Result<(), error::Dif
        .path()
        .ok_or(error::Diff::PathUnavailable)?;

-
    diff.copied(old.to_path_buf(), new.to_path_buf());
+
    diff.insert_copied(old.to_path_buf(), new.to_path_buf());
    Ok(())
}
modified radicle-surf/src/repo.rs
@@ -27,7 +27,7 @@ use radicle_git_ext::Oid;

use crate::{
    blob::{Blob, BlobRef},
-
    diff::Diff,
+
    diff::{Diff, FileDiff},
    fs::{Directory, File, FileContent},
    refs::{BranchNames, Branches, Categories, Namespaces, TagNames, Tags},
    tree::{Entry, Tree},
@@ -201,6 +201,28 @@ impl Repository {
        }
    }

+
    /// Get the [`FileDiff`] between two revisions for a file at `path`.
+
    ///
+
    /// If `path` is only a directory name, not a file, returns
+
    /// a [`FileDiff`] for any file under `path`.
+
    pub fn diff_file<P: AsRef<Path>, R: Revision>(
+
        &self,
+
        path: &P,
+
        from: R,
+
        to: R,
+
    ) -> Result<FileDiff, Error> {
+
        let from_commit = self.find_commit(self.object_id(&from)?)?;
+
        let to_commit = self.find_commit(self.object_id(&to)?)?;
+
        let diff = self
+
            .diff_commits(Some(path.as_ref()), Some(&from_commit), &to_commit)
+
            .and_then(|diff| Diff::try_from(diff).map_err(Error::from))?;
+
        let file_diff = diff
+
            .into_files()
+
            .pop()
+
            .ok_or(error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
+
        Ok(file_diff)
+
    }
+

    /// Parse an [`Oid`] from the given string.
    pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
        Ok(self.inner.revparse_single(oid)?.id().into())
modified radicle-surf/t/src/diff.rs
@@ -4,17 +4,14 @@ use radicle_git_ext::Oid;
use radicle_surf::{
    diff::{
        Added,
-
        Addition,
-
        Deleted,
-
        Deletion,
        Diff,
+
        DiffContent,
        EofNewLine,
        FileDiff,
        Hunk,
        Line,
        Modification,
        Modified,
-
        Moved,
        Stats,
    },
    Branch,
@@ -33,35 +30,32 @@ fn test_initial_diff() -> Result<(), Error> {
    assert!(commit.parents.is_empty());

    let diff = repo.diff_commit(oid)?;
+
    let diff_stats = *diff.stats();
+
    let diff_files = diff.into_files();

-
    let expected_diff = Diff {
-
        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![Addition {
-
                        line:
-
                            b"This repository is a data source for the Upstream front-end tests.\n"
-
                                .to_vec()
-
                                .into(),
-
                        line_no: 1,
-
                    }],
-
                }]
-
                .into(),
-
            },
-
        }],
-
        deleted: vec![],
-
        moved: vec![],
-
        copied: vec![],
-
        modified: vec![],
-
        stats: Stats {
-
            files_changed: 1,
-
            insertions: 1,
-
            deletions: 0,
+
    let expected_files = vec![FileDiff::Added(Added {
+
        path: Path::new("README.md").to_path_buf(),
+
        diff: DiffContent::Plain {
+
            hunks: vec![Hunk {
+
                header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()),
+
                lines: vec![Modification::addition(
+
                    b"This repository is a data source for the Upstream front-end tests.\n"
+
                        .to_vec(),
+
                    1,
+
                )],
+
            }]
+
            .into(),
        },
+
    })];
+

+
    let expected_stats = Stats {
+
        files_changed: 1,
+
        insertions: 1,
+
        deletions: 0,
    };
-
    assert_eq!(expected_diff, diff);
+

+
    assert_eq!(expected_files, diff_files);
+
    assert_eq!(expected_stats, diff_stats);

    Ok(())
}
@@ -70,10 +64,35 @@ fn test_initial_diff() -> Result<(), Error> {
fn test_diff_of_rev() -> Result<(), Error> {
    let repo = Repository::open(GIT_PLATINUM)?;
    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);
+
    assert_eq!(diff.files().count(), 1);
+
    Ok(())
+
}
+

+
#[test]
+
fn test_diff_file() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let path_buf = Path::new("README.md").to_path_buf();
+
    let diff = repo.diff_file(
+
        &path_buf,
+
        "d6880352fc7fda8f521ae9b7357668b17bb5bad5",
+
        "223aaf87d6ea62eef0014857640fd7c8dd0f80b5",
+
    )?;
+
    let expected_diff = FileDiff::Modified(Modified {
+
            path: path_buf,
+
            eof: None,
+
        diff: DiffContent::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()
+
        },
+
    });
+
    assert_eq!(expected_diff, diff);
+

    Ok(())
}

@@ -85,14 +104,10 @@ fn test_diff() -> Result<(), Error> {
    let parent_oid = commit.parents.get(0).unwrap();
    let diff = repo.diff(*parent_oid, oid)?;

-
    let expected_diff = Diff {
-
        added: vec![],
-
        deleted: vec![],
-
        moved: vec![],
-
        copied: vec![],
-
        modified: vec![Modified {
+
    let expected_files = vec![FileDiff::Modified(Modified {
            path: Path::new("README.md").to_path_buf(),
-
            diff: FileDiff::Plain {
+
            eof: None,
+
            diff: DiffContent::Plain {
                hunks: vec![Hunk {
                    header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()),
                    lines: vec![
@@ -102,15 +117,16 @@ fn test_diff() -> Result<(), Error> {
                    ]
                }].into()
            },
-
            eof: None,
-
        }],
-
        stats: Stats {
-
            files_changed: 1,
-
            insertions: 2,
-
            deletions: 1,
-
        },
+
        })];
+
    let expected_stats = Stats {
+
        files_changed: 1,
+
        insertions: 2,
+
        deletions: 1,
    };
-
    assert_eq!(expected_diff, diff);
+
    let diff_stats = *diff.stats();
+
    let diff_files = diff.into_files();
+
    assert_eq!(expected_files, diff_files);
+
    assert_eq!(expected_stats, diff_stats);

    Ok(())
}
@@ -118,89 +134,59 @@ fn test_diff() -> Result<(), Error> {
#[test]
fn test_branch_diff() -> Result<(), Error> {
    let repo = Repository::open(GIT_PLATINUM)?;
-
    let diff = repo.diff(
-
        Branch::local(refname!("master")),
-
        Branch::local(refname!("dev")),
-
    )?;
+
    let rev_from = Branch::local(refname!("master"));
+
    let rev_to = Branch::local(refname!("dev"));
+
    let diff = repo.diff(&rev_from, &rev_to)?;

    println!("Diff two branches: master -> dev");
    println!(
        "result: added {} deleted {} moved {} modified {}",
-
        diff.added.len(),
-
        diff.deleted.len(),
-
        diff.moved.len(),
-
        diff.modified.len()
+
        diff.added().count(),
+
        diff.deleted().count(),
+
        diff.moved().count(),
+
        diff.modified().count()
    );
-
    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.added.iter() {
+
    assert_eq!(diff.added().count(), 1);
+
    assert_eq!(diff.deleted().count(), 11);
+
    assert_eq!(diff.moved().count(), 1);
+
    assert_eq!(diff.modified().count(), 2);
+
    for c in diff.added() {
        println!("added: {:?}", &c.path);
    }
-
    for d in diff.deleted.iter() {
+
    for d in diff.deleted() {
        println!("deleted: {:?}", &d.path);
    }
-
    for m in diff.moved.iter() {
+
    for m in diff.moved() {
        println!("moved: {:?} -> {:?}", &m.old_path, &m.new_path);
    }
-
    for m in diff.modified.iter() {
+
    for m in diff.modified() {
        println!("modified: {:?}", &m.path);
    }
+

+
    // Verify moved.
+
    let diff_moved = diff.moved().next().unwrap();
+

+
    // We can find a `FileDiff` for the old_path in a move.
+
    let file_diff = repo.diff_file(&diff_moved.old_path, &rev_from, &rev_to)?;
+
    println!("old path file diff: {:?}", &file_diff);
+

+
    // We can find a `FileDiff` for the new_path in a move.
+
    let file_diff = repo.diff_file(&diff_moved.new_path, &rev_from, &rev_to)?;
+
    println!("new path file diff: {:?}", &file_diff);
+

+
    // We can find a `FileDiff` if given a directory name.
+
    let dir_diff = repo.diff_file(&"special/", &rev_from, &rev_to)?;
+
    println!("dir diff: {dir_diff:?}");
+

    Ok(())
}

#[test]
-
fn test_diff_serde() {
-
    let diff = Diff {
-
        added: vec![ Added {
-
            path: Path::new("LICENSE").to_path_buf(),
-
            diff: FileDiff::Plain {
-
                hunks: vec![Hunk {
-
                    header: Line::from(b"@@ -0,0 +1,1".to_vec()),
-
                    lines: vec![
-
                        Addition { line: Line::from(b"MIT".to_vec()), line_no: 1 }
-
                    ]
-
                }].into()
-
            }
-
        }],
-
        deleted: vec![ Deleted {
-
            path: Path::new("DCO").to_path_buf(),
-
            diff: FileDiff::Plain {
-
                hunks: vec![Hunk {
-
                    header: Line::from(b"@@ -0,0 +1,1".to_vec()),
-
                    lines: vec![
-
                        Deletion { line: Line::from(b"TODO".to_vec()), line_no: 1 }
-
                    ]
-
                }].into()
-
            }
-
        }],
-
        moved: vec![ Moved {
-
            old_path: Path::new("CONTRIBUTING").to_path_buf(),
-
            new_path: Path::new("CONTRIBUTING.md").to_path_buf(),
-
        }],
-
        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),
-
                    Modification::context(b"\n".to_vec(), 3, 4),
-
                ]
-
                }].into()
-
            },
-
            eof: None,
-
        }],
-
        stats: Stats {
-
            files_changed: 3,
-
            insertions: 2,
-
            deletions: 1,
-
        },
-
    };
+
fn test_diff_serde() -> Result<(), Error> {
+
    let repo = Repository::open(GIT_PLATINUM)?;
+
    let rev_from = Branch::local(refname!("master"));
+
    let rev_to = Branch::local(refname!("diff-test"));
+
    let diff = repo.diff(rev_from, rev_to)?;

    let eof: Option<u8> = None;
    let json = serde_json::json!({
@@ -209,66 +195,108 @@ fn test_diff_serde() {
            "diff": {
                "type": "plain",
                "hunks": [{
-
                    "header": "@@ -0,0 +1,1",
+
                    "header": "@@ -0,0 +1,2 @@\n",
                    "lines": [{
-
                        "line": "MIT",
+
                        "line": "This is a license file.\n",
                        "lineNo": 1,
                        "type": "addition",
+
                    },
+
                    {
+
                        "line": "\n",
+
                        "lineNo": 2,
+
                        "type": "addition",
                    }]
                }]
            },
        }],
        "deleted": [{
-
            "path": "DCO",
+
            "path": "text/arrows.txt",
            "diff": {
                "type": "plain",
                "hunks": [{
-
                    "header": "@@ -0,0 +1,1",
-
                    "lines": [{
-
                        "line": "TODO",
+
                    "header": "@@ -1,7 +0,0 @@\n",
+
                    "lines": [
+
                    {
+
                        "line": "  ;;;;;        ;;;;;        ;;;;;\n",
                        "lineNo": 1,
                        "type": "deletion",
-
                    }]
+
                    },
+
                    {
+
                        "line": "  ;;;;;        ;;;;;        ;;;;;\n",
+
                        "lineNo": 2,
+
                        "type": "deletion",
+
                    },
+
                    {
+
                        "line": "  ;;;;;        ;;;;;        ;;;;;\n",
+
                        "lineNo": 3,
+
                        "type": "deletion",
+
                    },
+
                    {
+
                        "line": "  ;;;;;        ;;;;;        ;;;;;\n",
+
                        "lineNo": 4,
+
                        "type": "deletion",
+
                    },
+
                    {
+
                        "line": "..;;;;;..    ..;;;;;..    ..;;;;;..\n",
+
                        "lineNo": 5,
+
                        "type": "deletion",
+
                    },
+
                    {
+
                        "line": " ':::::'      ':::::'      ':::::'\n",
+
                        "lineNo": 6,
+
                        "type": "deletion",
+
                    },
+
                    {
+
                        "line": "   ':`          ':`          ':`\n",
+
                        "lineNo": 7,
+
                        "type": "deletion",
+
                    },
+
                    ]
                }]
            },
        }],
-
        "moved": [{ "oldPath": "CONTRIBUTING", "newPath": "CONTRIBUTING.md" }],
+
        "moved": [{
+
            "oldPath": "text/emoji.txt",
+
            "newPath": "emoji.txt",
+
        }],
        "copied": [],
        "modified": [{
            "path": "README.md",
            "diff": {
                "type": "plain",
                "hunks": [{
-
                    "header": "@@ -1 +1,2 @@\n",
+
                    "header": "@@ -1,2 +1,2 @@\n",
                    "lines": [
                        { "lineNo": 1,
-
                          "line": "This repository is a data source for the Upstream front-end tests.\n",
+
                          "line": "This repository is a data source for the Upstream front-end tests and the\n",
+
                          "type": "deletion"
+
                        },
+
                        { "lineNo": 2,
+
                          "line": "[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n",
                          "type": "deletion"
                        },
                        { "lineNo": 1,
-
                          "line": "This repository is a data source for the Upstream front-end tests and the\n",
+
                          "line": "This repository is a data source for the upstream front-end tests and the\n",
                          "type": "addition"
                        },
                        { "lineNo": 2,
-
                          "line": "[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n",
+
                          "line": "[`radicle-surf`](https://github.com/radicle-dev/radicle-surf) unit tests.\n",
                          "type": "addition"
                        },
-
                        { "lineNoOld": 3, "lineNoNew": 4,
-
                          "line": "\n",
-
                          "type": "context"
-
                        }
                    ]
                }]
            },
-
            "eof" : eof,
+
            "eof": eof,
        }],
        "stats": {
-
            "deletions": 1,
-
            "filesChanged": 3,
-
            "insertions": 2,
+
            "deletions": 9,
+
            "filesChanged": 4,
+
            "insertions": 4,
        }
    });
-
    assert_eq!(serde_json::to_value(&diff).unwrap(), json);
+
    assert_eq!(serde_json::to_value(diff).unwrap(), json);
+

+
    Ok(())
}

#[test]
@@ -286,7 +314,10 @@ index f89e4c0..7c56eb7 100644
"#;
    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));
+
    assert_eq!(
+
        diff.modified().next().unwrap().eof,
+
        Some(EofNewLine::BothMissing)
+
    );
}

#[test]
@@ -302,7 +333,7 @@ index f89e4c0..7c56eb7 100644
"#;
    let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap();
    let diff = Diff::try_from(diff).unwrap();
-
    assert_eq!(diff.modified[0].eof, None);
+
    assert_eq!(diff.modified().next().unwrap().eof, None);
}

// TODO(xphoniex): uncomment once libgit2 has fixed the bug
modified radicle-surf/t/src/source.rs
@@ -209,23 +209,23 @@ fn commit_branches() {
    let glob = Glob::all_heads().branches().and(Glob::all_remotes());
    let branches = repo.revision_branches(init_commit, glob).unwrap();

-
    assert_eq!(branches.len(), 9);
-
    assert_eq!(branches[0].refname().as_str(), "refs/heads/dev");
-
    assert_eq!(branches[1].refname().as_str(), "refs/heads/empty-branch");
-
    assert_eq!(branches[2].refname().as_str(), "refs/heads/master");
-
    assert_eq!(
-
        branches[3].refname().as_str(),
-
        "refs/remotes/banana/orange/pineapple"
-
    );
-
    assert_eq!(
-
        branches[4].refname().as_str(),
-
        "refs/remotes/banana/pineapple"
-
    );
-
    assert_eq!(branches[5].refname().as_str(), "refs/remotes/origin/HEAD");
-
    assert_eq!(branches[6].refname().as_str(), "refs/remotes/origin/dev");
+
    assert_eq!(branches.len(), 11);
+

+
    let refnames: Vec<_> = branches.iter().map(|b| b.refname().to_string()).collect();
    assert_eq!(
-
        branches[7].refname().as_str(),
-
        "refs/remotes/origin/empty-branch"
+
        refnames,
+
        vec![
+
            "refs/heads/dev",
+
            "refs/heads/diff-test",
+
            "refs/heads/empty-branch",
+
            "refs/heads/master",
+
            "refs/remotes/banana/orange/pineapple",
+
            "refs/remotes/banana/pineapple",
+
            "refs/remotes/origin/HEAD",
+
            "refs/remotes/origin/dev",
+
            "refs/remotes/origin/diff-test",
+
            "refs/remotes/origin/empty-branch",
+
            "refs/remotes/origin/master"
+
        ]
    );
-
    assert_eq!(branches[8].refname().as_str(), "refs/remotes/origin/master");
}
modified radicle-surf/t/src/threading.rs
@@ -20,12 +20,14 @@ fn basic_test() -> Result<(), Error> {
        branches,
        vec![
            Branch::local(refname!("dev")),
+
            Branch::local(refname!("diff-test")),
            Branch::local(refname!("empty-branch")),
            Branch::local(refname!("master")),
            Branch::remote(banana.clone(), refname!("orange/pineapple")),
            Branch::remote(banana, refname!("pineapple")),
            Branch::remote(origin.clone(), refname!("HEAD")),
            Branch::remote(origin.clone(), refname!("dev")),
+
            Branch::remote(origin.clone(), refname!("diff-test")),
            Branch::remote(origin.clone(), refname!("empty-branch")),
            Branch::remote(origin, refname!("master")),
        ]