Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Remove pretty diffs
Fintan Halpenny committed 3 months ago
commit 5904c85973d9c08ed3258784b91f78078bcef6ba
parent f1a9ad9d0764ce67d035896f2db98be48a7dd9ad
5 files changed +19 -1739
modified crates/radicle-cli/src/commands/id.rs
@@ -12,10 +12,8 @@ use radicle::node::device::Device;
use radicle::node::NodeId;
use radicle::storage::{ReadStorage as _, WriteRepository};
use radicle::{cob, crypto, Profile};
-
use radicle_surf::diff::Diff;
use radicle_term::Element;

-
use crate::git::unified_diff::Encode as _;
use crate::git::Rev;
use crate::terminal as term;
use crate::terminal::args::Error;
@@ -443,44 +441,26 @@ fn print_diff(
    current: &RevisionId,
    repo: &radicle::storage::git::Repository,
) -> anyhow::Result<()> {
-
    let previous = if let Some(previous) = previous {
-
        let previous = Doc::load_at(*previous, repo)?;
-
        let previous = serde_json::to_string_pretty(&previous.doc)?;
-

-
        Some(previous)
-
    } else {
-
        None
-
    };
-
    let current = Doc::load_at(*current, repo)?;
-
    let current = serde_json::to_string_pretty(&current.doc)?;
-

-
    let tmp = tempfile::tempdir()?;
-
    let repo = radicle::git::raw::Repository::init_opts(
-
        tmp.path(),
-
        radicle::git::raw::RepositoryInitOptions::new()
-
            .external_template(false)
-
            .bare(true),
+
    let previous = previous
+
        .map(|id| Doc::load_at(*id, repo).map(|doc| doc.blob))
+
        .transpose()?;
+
    let current = Doc::load_at(*current, repo)?.blob;
+
    let old_blob = previous
+
        .map(|id| repo.raw().find_blob(id.into()))
+
        .transpose()?;
+
    let new_blob = Some(repo.raw().find_blob(current.into())?);
+
    let as_path = (*doc::PATH).to_string_lossy();
+
    repo.raw().diff_blobs(
+
        old_blob.as_ref(),
+
        Some(&as_path),
+
        new_blob.as_ref(),
+
        Some(&as_path),
+
        None,
+
        None,
+
        None,
+
        None,
+
        None,
    )?;
-

-
    let previous = if let Some(previous) = previous {
-
        let tree = radicle::git::write_tree(&doc::PATH, previous.as_bytes(), &repo)?;
-
        Some(tree)
-
    } else {
-
        None
-
    };
-
    let current = radicle::git::write_tree(&doc::PATH, current.as_bytes(), &repo)?;
-
    let mut opts = radicle::git::raw::DiffOptions::new();
-
    opts.context_lines(u32::MAX);
-

-
    let diff = repo.diff_tree_to_tree(previous.as_ref(), Some(&current), Some(&mut opts))?;
-
    let diff = Diff::try_from(diff)?;
-

-
    if let Some(modified) = diff.modified().next() {
-
        let diff = modified.diff.to_unified_string()?;
-
        print!("{diff}");
-
    } else {
-
        term::print(term::format::italic("No changes."));
-
    }
    Ok(())
}

modified crates/radicle-cli/src/git.rs
@@ -1,9 +1,5 @@
//! Git-related functions and types.

-
pub mod ddiff;
-
pub mod pretty_diff;
-
pub mod unified_diff;
-

use std::collections::HashSet;
use std::fmt::Display;
use std::fs::{File, OpenOptions};
deleted crates/radicle-cli/src/git/ddiff.rs
@@ -1,420 +0,0 @@
-
//! `DDiff` is a diff between diffs.  The type aids in the review of a `Patch` to a project by
-
//! providing useful context between `Patch` updates a regular `Diff` will miss.
-
//!
-
//! For example, lets start with a file containing a list of words.
-
//!
-
//! ```text
-
//! componentwise
-
//! reusing
-
//! simplest
-
//! crag
-
//! offended
-
//! omitting
-
//! ```
-
//! Where a change is proposed to the file replacing a set of lines.  The example includes the
-
//! `HunkHeader` "@ .. @" for completeness, but it can be mostly ignored.
-
//!
-
//! ```text
-
//! @@ -0,6 +0,6 @@
-
//! componentwise
-
//! reusing
-
//! -simplest
-
//! -crag
-
//! -offended
-
//! +interpreters
-
//! +soiled
-
//! +snuffing
-
//! omitting
-
//! ```
-
//!
-
//! The author updates the `Patch` to keep 'offended' and remove 'interpreters'.
-
//!
-
//! ```text
-
//! @@ -0,6 +0,6 @@
-
//! componentwise
-
//! reusing
-
//! -simplest
-
//! -crag
-
//!  offended
-
//! -interpreters
-
//! +soiled
-
//! +snuffing
-
//! omitting
-
//! ```
-
//! The `DDiff` will show the what changes are being made, overlaid on to the original diff and
-
//! the diff's original file as context.
-
//!
-
//! ```text
-
//! @@ -0,9 +0,8 @@
-
//!   componentwise
-
//!   reusing
-
//!  -simplest
-
//!  -crag
-
//! --offended
-
//! + offended
-
//! -+interpreters
-
//!  +soiled
-
//!  +snuffing
-
//!   omitting
-
//! ```
-
//!
-
//! An alternative is to review a `Diff` between the resulting files after the first and second
-
//! Patch versions were applied.  The first `Patch` changes and original file contents are one
-
//! making it unclear what are changes to the `Patch` or changes to the original file.
-
//!
-
//! ```text
-
//! @@ -0,9 +0,8 @@
-
//!  componentwise
-
//!  reusing
-
//! +offended
-
//! -interpreters
-
//!  soiled
-
//!  snuffing
-
//!  omitting
-
//! ```
-
use radicle_surf::diff::*;
-

-
use std::io;
-

-
use crate::git::unified_diff;
-
use crate::git::unified_diff::{Encode, Writer};
-
use crate::terminal as term;
-

-
/// Either the modification of a single diff [`Line`], or just contextual
-
/// information.
-
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub enum DiffModification {
-
    /// An addition line is to be added.
-
    AdditionAddition { line: Line, line_no: u32 },
-
    AdditionContext {
-
        line: Line,
-
        line_no_old: u32,
-
        line_no_new: u32,
-
    },
-
    /// An addition line is to be removed.
-
    AdditionDeletion { line: Line, line_no: u32 },
-
    /// A context line is to be added.
-
    ContextAddition { line: Line, line_no: u32 },
-
    /// A contextual line in a file, i.e. there were no changes to the line.
-
    ContextContext {
-
        line: Line,
-
        line_no_old: u32,
-
        line_no_new: u32,
-
    },
-
    /// A context line is to be removed.
-
    ContextDeletion { line: Line, line_no: u32 },
-
    /// A deletion line is to be added.
-
    DeletionAddition { line: Line, line_no: u32 },
-
    /// A deletion line in a diff, i.e. there were no changes to the line.
-
    DeletionContext {
-
        line: Line,
-
        line_no_old: u32,
-
        line_no_new: u32,
-
    },
-
    /// A deletion line is to be removed.
-
    DeletionDeletion { line: Line, line_no: u32 },
-
}
-

-
impl unified_diff::Decode for Hunk<DiffModification> {
-
    fn decode(r: &mut impl io::BufRead) -> Result<Self, unified_diff::Error> {
-
        let header = unified_diff::HunkHeader::decode(r)?;
-

-
        let mut lines = Vec::new();
-
        let mut new_line: u32 = 0;
-
        let mut old_line: u32 = 0;
-

-
        while old_line < header.old_size || new_line < header.new_size {
-
            if old_line > header.old_size {
-
                return Err(unified_diff::Error::syntax(format!(
-
                    "expected '{0}' old lines",
-
                    header.old_size,
-
                )));
-
            } else if new_line > header.new_size {
-
                return Err(unified_diff::Error::syntax(format!(
-
                    "expected '{0}' new lines",
-
                    header.new_size,
-
                )));
-
            }
-

-
            let mut line = DiffModification::decode(r).map_err(|e| {
-
                if e.is_eof() {
-
                    unified_diff::Error::syntax(format!(
-
                        "expected '{}' old lines and '{}' new lines, but found '{}' and '{}'",
-
                        header.old_size, header.new_size, old_line, new_line,
-
                    ))
-
                } else {
-
                    e
-
                }
-
            })?;
-

-
            match &mut line {
-
                DiffModification::AdditionAddition { line_no, .. } => {
-
                    *line_no = new_line;
-
                    new_line += 1;
-
                }
-
                DiffModification::AdditionContext {
-
                    line_no_old,
-
                    line_no_new,
-
                    ..
-
                } => {
-
                    *line_no_old = old_line;
-
                    *line_no_new = new_line;
-
                    old_line += 1;
-
                    new_line += 1;
-
                }
-
                DiffModification::AdditionDeletion { line_no, .. } => {
-
                    *line_no = old_line;
-
                    old_line += 1;
-
                }
-
                DiffModification::ContextAddition { line_no, .. } => {
-
                    *line_no = new_line;
-
                    new_line += 1;
-
                }
-
                DiffModification::ContextContext {
-
                    line_no_old,
-
                    line_no_new,
-
                    ..
-
                } => {
-
                    *line_no_old = old_line;
-
                    *line_no_new = new_line;
-
                    old_line += 1;
-
                    new_line += 1;
-
                }
-
                DiffModification::ContextDeletion { line_no, .. } => {
-
                    *line_no = old_line;
-
                    old_line += 1;
-
                }
-
                DiffModification::DeletionAddition { line_no, .. } => {
-
                    *line_no = new_line;
-
                    new_line += 1;
-
                }
-
                DiffModification::DeletionContext {
-
                    line_no_old,
-
                    line_no_new,
-
                    ..
-
                } => {
-
                    *line_no_old = old_line;
-
                    *line_no_new = new_line;
-
                    old_line += 1;
-
                    new_line += 1;
-
                }
-
                DiffModification::DeletionDeletion { line_no, .. } => {
-
                    *line_no = old_line;
-
                    old_line += 1;
-
                }
-
            };
-

-
            lines.push(line);
-
        }
-

-
        Ok(Hunk {
-
            header: Line::from(header.to_unified_string()?),
-
            lines,
-
            old: header.old_line_range(),
-
            new: header.new_line_range(),
-
        })
-
    }
-
}
-

-
impl unified_diff::Encode for Hunk<DiffModification> {
-
    fn encode(&self, w: &mut Writer) -> Result<(), unified_diff::Error> {
-
        // TODO: Remove trailing newlines accurately.
-
        // trim_end() will destroy diff information if the diff has a trailing whitespace on
-
        // purpose.
-
        w.magenta(self.header.from_utf8_lossy().trim_end())?;
-
        for l in &self.lines {
-
            l.encode(w)?;
-
        }
-
        Ok(())
-
    }
-
}
-

-
/// The DDiff version of `FileDiff`.
-
#[derive(Clone, Debug, PartialEq)]
-
pub struct FileDDiff {
-
    pub path: std::path::PathBuf,
-
    pub old: DiffFile,
-
    pub new: DiffFile,
-
    pub hunks: Hunks<DiffModification>,
-
    pub eof: EofNewLine,
-
}
-

-
impl From<&FileDDiff> for unified_diff::FileHeader {
-
    fn from(value: &FileDDiff) -> Self {
-
        unified_diff::FileHeader::Modified {
-
            path: value.path.clone(),
-
            old: value.old.clone(),
-
            new: value.new.clone(),
-
            binary: false,
-
        }
-
    }
-
}
-

-
impl unified_diff::Decode for DiffModification {
-
    fn decode(r: &mut impl std::io::BufRead) -> Result<Self, unified_diff::Error> {
-
        let mut line = String::new();
-
        if r.read_line(&mut line)? == 0 {
-
            return Err(unified_diff::Error::UnexpectedEof);
-
        }
-

-
        let mut chars = line.chars();
-

-
        let first = chars.next().ok_or(unified_diff::Error::UnexpectedEof)?;
-
        let second = chars.next().ok_or(unified_diff::Error::UnexpectedEof)?;
-

-
        let line = match (first, second) {
-
            ('+', '+') => DiffModification::AdditionAddition {
-
                line: chars.as_str().to_string().into(),
-
                line_no: 0,
-
            },
-
            ('+', '-') => DiffModification::DeletionAddition {
-
                line: chars.as_str().to_string().into(),
-
                line_no: 0,
-
            },
-
            ('+', ' ') => DiffModification::ContextAddition {
-
                line: chars.as_str().to_string().into(),
-
                line_no: 0,
-
            },
-
            ('-', '+') => DiffModification::AdditionDeletion {
-
                line: chars.as_str().to_string().into(),
-
                line_no: 0,
-
            },
-
            ('-', '-') => DiffModification::DeletionDeletion {
-
                line: chars.as_str().to_string().into(),
-
                line_no: 0,
-
            },
-
            ('-', ' ') => DiffModification::ContextDeletion {
-
                line: chars.as_str().to_string().into(),
-
                line_no: 0,
-
            },
-
            (' ', '+') => DiffModification::AdditionContext {
-
                line: chars.as_str().to_string().into(),
-
                line_no_old: 0,
-
                line_no_new: 0,
-
            },
-
            (' ', '-') => DiffModification::DeletionContext {
-
                line: chars.as_str().to_string().into(),
-
                line_no_old: 0,
-
                line_no_new: 0,
-
            },
-
            (' ', ' ') => DiffModification::ContextContext {
-
                line: chars.as_str().to_string().into(),
-
                line_no_old: 0,
-
                line_no_new: 0,
-
            },
-
            (v1, v2) => {
-
                return Err(unified_diff::Error::syntax(format!(
-
                    "indicator character expected, but got '{v1}{v2}'"
-
                )))
-
            }
-
        };
-

-
        Ok(line)
-
    }
-
}
-

-
impl unified_diff::Encode for DiffModification {
-
    fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
-
        match self {
-
            DiffModification::AdditionAddition { line, .. } => {
-
                let s = format!("++{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Green))?;
-
            }
-
            DiffModification::AdditionDeletion { line, .. } => {
-
                let s = format!("-+{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Red))?;
-
            }
-
            DiffModification::ContextAddition { line, .. } => {
-
                let s = format!("+ {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Green))?;
-
            }
-
            DiffModification::DeletionAddition { line, .. } => {
-
                let s = format!("+-{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Green))?;
-
            }
-
            DiffModification::DeletionDeletion { line, .. } => {
-
                let s = format!("--{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Red))?;
-
            }
-
            DiffModification::ContextDeletion { line, .. } => {
-
                let s = format!("- {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Red))?;
-
            }
-
            DiffModification::AdditionContext { line, .. } => {
-
                let s = format!(" +{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Green).dim())?
-
            }
-
            DiffModification::DeletionContext { line, .. } => {
-
                let s = format!(" -{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Red).dim())?;
-
            }
-
            DiffModification::ContextContext { line, .. } => {
-
                let s = format!("  {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::default().dim())?;
-
            }
-
        }
-

-
        Ok(())
-
    }
-
}
-

-
impl unified_diff::Encode for FileDDiff {
-
    fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
-
        w.encode(&unified_diff::FileHeader::from(self))?;
-
        for h in self.hunks.iter() {
-
            h.encode(w)?;
-
        }
-

-
        Ok(())
-
    }
-
}
-

-
/// A diff of a diff.
-
#[derive(Clone, Debug, PartialEq, Default)]
-
pub struct DDiff {
-
    files: Vec<FileDDiff>,
-
}
-

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

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

-
impl unified_diff::Encode for DDiff {
-
    fn encode(&self, w: &mut unified_diff::Writer) -> Result<(), unified_diff::Error> {
-
        for v in self.files() {
-
            v.encode(w)?;
-
        }
-
        Ok(())
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

-
    use crate::git::unified_diff::{Decode, Encode};
-

-
    #[test]
-
    fn diff_encode_decode_ddiff_hunk() {
-
        let ddiff = Hunk::<DiffModification>::parse(include_str!(concat!(
-
            env!("CARGO_MANIFEST_DIR"),
-
            "/tests/data/ddiff_hunk.diff"
-
        )))
-
        .unwrap();
-
        assert_eq!(
-
            include_str!(concat!(
-
                env!("CARGO_MANIFEST_DIR"),
-
                "/tests/data/ddiff_hunk.diff"
-
            )),
-
            ddiff.to_unified_string().unwrap()
-
        );
-
    }
-
}
deleted crates/radicle-cli/src/git/pretty_diff.rs
@@ -1,628 +0,0 @@
-
use std::fs;
-
use std::path::{Path, PathBuf};
-

-
use radicle::git;
-
use radicle::git::Oid;
-
use radicle_surf::diff;
-
use radicle_surf::diff::{Added, Copied, Deleted, FileStats, Hunks, Modified, Moved};
-
use radicle_surf::diff::{Diff, DiffContent, FileDiff, Hunk, Modification};
-
use radicle_term as term;
-
use term::cell::Cell;
-
use term::VStack;
-

-
use crate::git::unified_diff::FileHeader;
-
use crate::terminal::highlight::{Highlighter, Theme};
-

-
use super::unified_diff::{Decode, HunkHeader};
-

-
/// Blob returned by the [`Repo`] trait.
-
#[derive(PartialEq, Eq, Debug)]
-
pub enum Blob {
-
    Binary,
-
    Empty,
-
    Plain(Vec<u8>),
-
}
-

-
/// A repository of Git blobs.
-
pub trait Repo {
-
    /// Lookup a blob from the repo.
-
    fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error>;
-
    /// Lookup a file in the workdir.
-
    fn file(&self, path: &Path) -> Option<Blob>;
-
}
-

-
impl Repo for git::raw::Repository {
-
    fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error> {
-
        let blob = self.find_blob(oid.into())?;
-

-
        if blob.is_binary() {
-
            Ok(Blob::Binary)
-
        } else {
-
            let content = blob.content();
-

-
            if content.is_empty() {
-
                Ok(Blob::Empty)
-
            } else {
-
                Ok(Blob::Plain(blob.content().to_vec()))
-
            }
-
        }
-
    }
-

-
    fn file(&self, path: &Path) -> Option<Blob> {
-
        self.workdir()
-
            .and_then(|dir| fs::read(dir.join(path)).ok())
-
            .map(|content| {
-
                // A file is considered binary if there is a zero byte in the first 8 kilobytes
-
                // of the file. This is the same heuristic Git uses.
-
                let binary = content.iter().take(8192).any(|b| *b == 0);
-
                if binary {
-
                    Blob::Binary
-
                } else {
-
                    Blob::Plain(content)
-
                }
-
            })
-
    }
-
}
-

-
/// Blobs passed down to the hunk renderer.
-
#[derive(Debug)]
-
pub struct Blobs<T> {
-
    pub old: Option<T>,
-
    pub new: Option<T>,
-
}
-

-
impl<T> Blobs<T> {
-
    pub fn new(old: Option<T>, new: Option<T>) -> Self {
-
        Self { old, new }
-
    }
-
}
-

-
impl Blobs<(PathBuf, Blob)> {
-
    pub fn highlight(&self, hi: &mut Highlighter) -> Blobs<Vec<term::Line>> {
-
        let mut blobs = Blobs::default();
-
        if let Some((path, Blob::Plain(content))) = &self.old {
-
            blobs.old = hi.highlight(path, content).ok();
-
        }
-
        if let Some((path, Blob::Plain(content))) = &self.new {
-
            blobs.new = hi.highlight(path, content).ok();
-
        }
-
        blobs
-
    }
-

-
    pub fn from_paths<R: Repo>(
-
        old: Option<(&Path, Oid)>,
-
        new: Option<(&Path, Oid)>,
-
        repo: &R,
-
    ) -> Blobs<(PathBuf, Blob)> {
-
        Blobs::new(
-
            old.and_then(|(path, oid)| {
-
                repo.blob(oid)
-
                    .ok()
-
                    .or_else(|| repo.file(path))
-
                    .map(|blob| (path.to_path_buf(), blob))
-
            }),
-
            new.and_then(|(path, oid)| {
-
                repo.blob(oid)
-
                    .ok()
-
                    .or_else(|| repo.file(path))
-
                    .map(|blob| (path.to_path_buf(), blob))
-
            }),
-
        )
-
    }
-
}
-

-
impl<T> Default for Blobs<T> {
-
    fn default() -> Self {
-
        Self {
-
            old: None,
-
            new: None,
-
        }
-
    }
-
}
-

-
/// Types that can be rendered as pretty diffs.
-
pub trait ToPretty {
-
    /// The output of the render process.
-
    type Output: term::Element;
-
    /// Context that can be passed down from parent objects during rendering.
-
    type Context;
-

-
    /// Render to pretty diff output.
-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        context: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output;
-
}
-

-
impl ToPretty for Diff {
-
    type Output = term::VStack<'static>;
-
    type Context = ();
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        context: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        term::VStack::default()
-
            .padding(0)
-
            .children(self.files().flat_map(|f| {
-
                [
-
                    f.pretty(hi, context, repo).boxed(),
-
                    term::Line::blank().boxed(), // Blank line between files.
-
                ]
-
            }))
-
    }
-
}
-

-
impl ToPretty for FileHeader {
-
    type Output = term::Line;
-
    type Context = Option<FileStats>;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        _hi: &mut Highlighter,
-
        stats: &Self::Context,
-
        _repo: &R,
-
    ) -> Self::Output {
-
        let theme = Theme::default();
-
        let (mut header, badge, binary) = match self {
-
            FileHeader::Added { path, binary, .. } => (
-
                term::Line::new(path.display().to_string()),
-
                Some(term::format::badge_positive("created")),
-
                *binary,
-
            ),
-
            FileHeader::Moved {
-
                old_path, new_path, ..
-
            } => (
-
                term::Line::spaced([
-
                    term::label(old_path.display().to_string()),
-
                    term::label("->".to_string()),
-
                    term::label(new_path.display().to_string()),
-
                ]),
-
                Some(term::format::badge_secondary("moved")),
-
                false,
-
            ),
-
            FileHeader::Deleted { path, binary, .. } => (
-
                term::Line::new(path.display().to_string()),
-
                Some(term::format::badge_negative("deleted")),
-
                *binary,
-
            ),
-
            FileHeader::Modified {
-
                path,
-
                old,
-
                new,
-
                binary,
-
                ..
-
            } => {
-
                if old.mode != new.mode {
-
                    (
-
                        term::Line::spaced([
-
                            term::label(path.display().to_string()),
-
                            term::label(format!("{:o}", u32::from(old.mode.clone())))
-
                                .fg(term::Color::Blue),
-
                            term::label("->".to_string()),
-
                            term::label(format!("{:o}", u32::from(new.mode.clone())))
-
                                .fg(term::Color::Blue),
-
                        ]),
-
                        Some(term::format::badge_secondary("mode changed")),
-
                        *binary,
-
                    )
-
                } else {
-
                    (term::Line::new(path.display().to_string()), None, *binary)
-
                }
-
            }
-
            FileHeader::Copied {
-
                old_path, new_path, ..
-
            } => (
-
                term::Line::spaced([
-
                    term::label(old_path.display().to_string()),
-
                    term::label("->".to_string()),
-
                    term::label(new_path.display().to_string()),
-
                ]),
-
                Some(term::format::badge_secondary("copied")),
-
                false,
-
            ),
-
        };
-

-
        if binary {
-
            header.push(term::Label::space());
-
            header.push(term::label(term::format::badge_yellow("binary")));
-
        }
-

-
        let (additions, deletions) = if let Some(stats) = stats {
-
            (stats.additions, stats.deletions)
-
        } else {
-
            (0, 0)
-
        };
-
        if deletions > 0 {
-
            header.push(term::Label::space());
-
            header.push(term::label(format!("-{deletions}")).fg(theme.color("negative.light")));
-
        }
-
        if additions > 0 {
-
            header.push(term::Label::space());
-
            header.push(term::label(format!("+{additions}")).fg(theme.color("positive.light")));
-
        }
-
        if let Some(badge) = badge {
-
            header.push(term::Label::space());
-
            header.push(badge);
-
        }
-
        header
-
    }
-
}
-

-
impl ToPretty for FileDiff {
-
    type Output = term::VStack<'static>;
-
    type Context = ();
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        _context: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let header = FileHeader::from(self);
-

-
        match self {
-
            FileDiff::Added(f) => f.pretty(hi, &header, repo),
-
            FileDiff::Deleted(f) => f.pretty(hi, &header, repo),
-
            FileDiff::Modified(f) => f.pretty(hi, &header, repo),
-
            FileDiff::Moved(f) => f.pretty(hi, &header, repo),
-
            FileDiff::Copied(f) => f.pretty(hi, &header, repo),
-
        }
-
    }
-
}
-

-
impl ToPretty for DiffContent {
-
    type Output = term::VStack<'static>;
-
    type Context = Blobs<(PathBuf, Blob)>;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        blobs: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let mut vstack = term::VStack::default().padding(0);
-

-
        match self {
-
            DiffContent::Plain {
-
                hunks: Hunks(hunks),
-
                ..
-
            } => {
-
                let blobs = blobs.highlight(hi);
-

-
                for (i, h) in hunks.iter().enumerate() {
-
                    vstack.push(h.pretty(hi, &blobs, repo));
-
                    if i != hunks.len() - 1 {
-
                        vstack = vstack.divider();
-
                    }
-
                }
-
            }
-
            DiffContent::Empty => {}
-
            DiffContent::Binary => {}
-
        }
-
        vstack
-
    }
-
}
-

-
impl ToPretty for Moved {
-
    type Output = term::VStack<'static>;
-
    type Context = FileHeader;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        header: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let header = header.pretty(hi, &self.diff.stats().copied(), repo);
-

-
        term::VStack::default()
-
            .border(Some(term::colors::FAINT))
-
            .padding(1)
-
            .child(term::Line::default().extend(header))
-
    }
-
}
-

-
impl ToPretty for Added {
-
    type Output = term::VStack<'static>;
-
    type Context = FileHeader;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        header: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let old = None;
-
        let new = Some((self.path.as_path(), Oid::from(*self.new.oid)));
-

-
        pretty_modification(header, &self.diff, old, new, repo, hi)
-
    }
-
}
-

-
impl ToPretty for Deleted {
-
    type Output = term::VStack<'static>;
-
    type Context = FileHeader;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        header: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let old = Some((self.path.as_path(), Oid::from(*self.old.oid)));
-
        let new = None;
-

-
        pretty_modification(header, &self.diff, old, new, repo, hi)
-
    }
-
}
-

-
impl ToPretty for Modified {
-
    type Output = term::VStack<'static>;
-
    type Context = FileHeader;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        header: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let old = Some((self.path.as_path(), Oid::from(*self.old.oid)));
-
        let new = Some((self.path.as_path(), Oid::from(*self.new.oid)));
-

-
        pretty_modification(header, &self.diff, old, new, repo, hi)
-
    }
-
}
-

-
impl ToPretty for Copied {
-
    type Output = term::VStack<'static>;
-
    type Context = FileHeader;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        _context: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let header = FileHeader::Copied {
-
            old_path: self.old_path.clone(),
-
            new_path: self.old_path.clone(),
-
        }
-
        .pretty(hi, &self.diff.stats().copied(), repo);
-

-
        term::VStack::default()
-
            .border(Some(term::colors::FAINT))
-
            .padding(1)
-
            .child(header)
-
    }
-
}
-

-
impl ToPretty for HunkHeader {
-
    type Output = term::Line;
-
    type Context = ();
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        _hi: &mut Highlighter,
-
        _context: &Self::Context,
-
        _repo: &R,
-
    ) -> Self::Output {
-
        term::Line::spaced([
-
            term::label(format!(
-
                "@@ -{},{} +{},{} @@",
-
                self.old_line_no, self.old_size, self.new_line_no, self.new_size,
-
            ))
-
            .fg(term::colors::fixed::FAINT),
-
            term::label(String::from_utf8_lossy(&self.text).to_string())
-
                .fg(term::colors::fixed::DIM),
-
        ])
-
    }
-
}
-

-
impl ToPretty for Hunk<Modification> {
-
    type Output = term::VStack<'static>;
-
    type Context = Blobs<Vec<term::Line>>;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        hi: &mut Highlighter,
-
        blobs: &Self::Context,
-
        repo: &R,
-
    ) -> Self::Output {
-
        let mut vstack = term::VStack::default().padding(0);
-
        let mut table = term::Table::<5, term::Filled<term::Line>>::new(term::TableOptions {
-
            overflow: false,
-
            spacing: 0,
-
            border: None,
-
        });
-
        let theme = Theme::default();
-

-
        if let Ok(header) = HunkHeader::from_bytes(self.header.as_bytes()) {
-
            vstack.push(header.pretty(hi, &(), repo));
-
        }
-

-
        table.extend(
-
            self.lines
-
                .iter()
-
                .map(|line| line_to_table_row(hi, blobs, repo, &theme, line)),
-
        );
-

-
        vstack.push(table);
-
        vstack
-
    }
-
}
-

-
fn line_to_table_row<R: Repo>(
-
    hi: &mut Highlighter,
-
    blobs: &Blobs<Vec<radicle_term::Line>>,
-
    repo: &R,
-
    theme: &Theme,
-
    line: &Modification,
-
) -> [radicle_term::Filled<radicle_term::Line>; 5] {
-
    match line {
-
        Modification::Addition(a) => [
-
            term::Label::space()
-
                .pad(5)
-
                .bg(theme.color("positive"))
-
                .to_line()
-
                .filled(theme.color("positive")),
-
            term::label(a.line_no.to_string())
-
                .pad(5)
-
                .fg(theme.color("positive.light"))
-
                .to_line()
-
                .filled(theme.color("positive")),
-
            term::label(" + ")
-
                .fg(theme.color("positive.light"))
-
                .to_line()
-
                .filled(theme.color("positive.dark")),
-
            line.pretty(hi, blobs, repo)
-
                .filled(theme.color("positive.dark")),
-
            term::Line::blank().filled(term::Color::default()),
-
        ],
-
        Modification::Deletion(a) => [
-
            term::label(a.line_no.to_string())
-
                .pad(5)
-
                .fg(theme.color("negative.light"))
-
                .to_line()
-
                .filled(theme.color("negative")),
-
            term::Label::space()
-
                .pad(5)
-
                .fg(theme.color("dim"))
-
                .to_line()
-
                .filled(theme.color("negative")),
-
            term::label(" - ")
-
                .fg(theme.color("negative.light"))
-
                .to_line()
-
                .filled(theme.color("negative.dark")),
-
            line.pretty(hi, blobs, repo)
-
                .filled(theme.color("negative.dark")),
-
            term::Line::blank().filled(term::Color::default()),
-
        ],
-
        Modification::Context {
-
            line_no_old,
-
            line_no_new,
-
            ..
-
        } => [
-
            term::label(line_no_old.to_string())
-
                .pad(5)
-
                .fg(theme.color("dim"))
-
                .to_line()
-
                .filled(theme.color("faint")),
-
            term::label(line_no_new.to_string())
-
                .pad(5)
-
                .fg(theme.color("dim"))
-
                .to_line()
-
                .filled(theme.color("faint")),
-
            term::label("   ").to_line().filled(term::Color::default()),
-
            line.pretty(hi, blobs, repo).filled(term::Color::default()),
-
            term::Line::blank().filled(term::Color::default()),
-
        ],
-
    }
-
}
-

-
impl ToPretty for Modification {
-
    type Output = term::Line;
-
    type Context = Blobs<Vec<term::Line>>;
-

-
    fn pretty<R: Repo>(
-
        &self,
-
        _hi: &mut Highlighter,
-
        blobs: &Blobs<Vec<term::Line>>,
-
        _repo: &R,
-
    ) -> Self::Output {
-
        match self {
-
            Modification::Deletion(diff::Deletion { line, line_no }) => {
-
                if let Some(lines) = &blobs.old.as_ref() {
-
                    lines[*line_no as usize - 1].clone()
-
                } else {
-
                    term::Line::new(String::from_utf8_lossy(line.as_bytes()).as_ref())
-
                }
-
            }
-
            Modification::Addition(diff::Addition { line, line_no }) => {
-
                if let Some(lines) = &blobs.new.as_ref() {
-
                    lines[*line_no as usize - 1].clone()
-
                } else {
-
                    term::Line::new(String::from_utf8_lossy(line.as_bytes()).as_ref())
-
                }
-
            }
-
            Modification::Context {
-
                line, line_no_new, ..
-
            } => {
-
                // Nb. we can check in the old or the new blob, we choose the new.
-
                if let Some(lines) = &blobs.new.as_ref() {
-
                    lines[*line_no_new as usize - 1].clone()
-
                } else {
-
                    term::Line::new(String::from_utf8_lossy(line.as_bytes()).as_ref())
-
                }
-
            }
-
        }
-
    }
-
}
-

-
/// Render a file added, deleted or modified.
-
fn pretty_modification<R: Repo>(
-
    header: &FileHeader,
-
    diff: &DiffContent,
-
    old: Option<(&Path, Oid)>,
-
    new: Option<(&Path, Oid)>,
-
    repo: &R,
-
    hi: &mut Highlighter,
-
) -> VStack<'static> {
-
    let blobs = Blobs::from_paths(old, new, repo);
-
    let header = header.pretty(hi, &diff.stats().copied(), repo);
-
    let vstack = term::VStack::default()
-
        .border(Some(term::colors::FAINT))
-
        .padding(1)
-
        .child(header);
-

-
    let body = diff.pretty(hi, &blobs, repo);
-
    if body.is_empty() {
-
        vstack
-
    } else {
-
        vstack.divider().merge(body)
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use std::ffi::OsStr;
-

-
    use term::Constraint;
-
    use term::Element;
-

-
    use super::*;
-
    use git::raw::RepositoryOpenFlags;
-
    use git::raw::{Oid, Repository};
-

-
    #[test]
-
    #[ignore]
-
    fn test_pretty() {
-
        let repo = Repository::open_ext::<_, _, &[&OsStr]>(
-
            env!("CARGO_MANIFEST_DIR"),
-
            RepositoryOpenFlags::all(),
-
            &[],
-
        )
-
        .unwrap();
-
        let commit = repo
-
            .find_commit(Oid::from_str("5078396028e2ec5660aa54a00208f6e11df84aa9").unwrap())
-
            .unwrap();
-
        let parent = commit.parents().next().unwrap();
-
        let old_tree = parent.tree().unwrap();
-
        let new_tree = commit.tree().unwrap();
-
        let diff = repo
-
            .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)
-
            .unwrap();
-
        let diff = Diff::try_from(diff).unwrap();
-

-
        let mut hi = Highlighter::default();
-
        let pretty = diff.pretty(&mut hi, &(), &repo);
-

-
        pretty
-
            .write(Constraint::from_env().unwrap_or_default())
-
            .unwrap();
-
    }
-
}
deleted crates/radicle-cli/src/git/unified_diff.rs
@@ -1,648 +0,0 @@
-
//! Formatting support for Git's [diff format](https://git-scm.com/docs/diff-format).
-
use std::fmt;
-
use std::io;
-
use std::path::PathBuf;
-

-
use radicle_surf::diff::FileStats;
-
use thiserror::Error;
-

-
use radicle::git;
-
use radicle_surf::diff;
-
use radicle_surf::diff::{Diff, DiffContent, DiffFile, FileDiff, Hunk, Hunks, Line, Modification};
-

-
use crate::terminal as term;
-

-
#[derive(Debug, Error)]
-
pub enum Error {
-
    /// Attempt to decode from a source with no data left.
-
    #[error("unexpected end of file")]
-
    UnexpectedEof,
-
    #[error(transparent)]
-
    Io(#[from] io::Error),
-
    /// Catchall for syntax error messages.
-
    #[error("{0}")]
-
    Syntax(String),
-
    #[error(transparent)]
-
    ParseInt(#[from] std::num::ParseIntError),
-
    #[error(transparent)]
-
    Utf8(#[from] std::string::FromUtf8Error),
-
}
-

-
impl Error {
-
    pub fn syntax(msg: impl ToString) -> Self {
-
        Self::Syntax(msg.to_string())
-
    }
-

-
    pub fn is_eof(&self) -> bool {
-
        match self {
-
            Self::UnexpectedEof => true,
-
            Self::Io(e) => e.kind() == io::ErrorKind::UnexpectedEof,
-
            _ => false,
-
        }
-
    }
-
}
-

-
/// The kind of FileDiff Header which can be used to print the FileDiff information which precedes
-
/// `Hunks`.
-
#[derive(Debug, Clone, PartialEq)]
-
pub enum FileHeader {
-
    Added {
-
        path: PathBuf,
-
        new: DiffFile,
-
        binary: bool,
-
    },
-
    Copied {
-
        old_path: PathBuf,
-
        new_path: PathBuf,
-
    },
-
    Deleted {
-
        path: PathBuf,
-
        old: DiffFile,
-
        binary: bool,
-
    },
-
    Modified {
-
        path: PathBuf,
-
        old: DiffFile,
-
        new: DiffFile,
-
        binary: bool,
-
    },
-
    Moved {
-
        old_path: PathBuf,
-
        new_path: PathBuf,
-
    },
-
}
-

-
impl std::convert::From<&FileDiff> for FileHeader {
-
    // TODO: Pathnames with 'unusual names' need to be quoted.
-
    fn from(value: &FileDiff) -> Self {
-
        match value {
-
            FileDiff::Modified(v) => FileHeader::Modified {
-
                path: v.path.clone(),
-
                old: v.old.clone(),
-
                new: v.new.clone(),
-
                binary: matches!(v.diff, DiffContent::Binary),
-
            },
-
            FileDiff::Added(v) => FileHeader::Added {
-
                path: v.path.clone(),
-
                new: v.new.clone(),
-
                binary: matches!(v.diff, DiffContent::Binary),
-
            },
-
            FileDiff::Copied(c) => FileHeader::Copied {
-
                old_path: c.old_path.clone(),
-
                new_path: c.new_path.clone(),
-
            },
-
            FileDiff::Deleted(v) => FileHeader::Deleted {
-
                path: v.path.clone(),
-
                old: v.old.clone(),
-
                binary: matches!(v.diff, DiffContent::Binary),
-
            },
-
            FileDiff::Moved(v) => FileHeader::Moved {
-
                old_path: v.old_path.clone(),
-
                new_path: v.new_path.clone(),
-
            },
-
        }
-
    }
-
}
-

-
/// Meta data which precedes a `Hunk`s content.
-
///
-
/// For example:
-
/// @@ -24,8 +24,6 @@ use radicle_surf::diff::*;
-
#[derive(Clone, Debug, Default, PartialEq)]
-
pub struct HunkHeader {
-
    /// Line the hunk started in the old file.
-
    pub old_line_no: u32,
-
    /// Number of removed and context lines.
-
    pub old_size: u32,
-
    /// Line the hunk started in the new file.
-
    pub new_line_no: u32,
-
    /// Number of added and context lines.
-
    pub new_size: u32,
-
    /// Trailing text for the Hunk Header.
-
    ///
-
    /// From Git's documentation "Hunk headers mention the name of the function to which the hunk
-
    /// applies. See "Defining a custom hunk-header" in gitattributes for details of how to tailor
-
    /// to this to specific languages.".  It is likely best to leave this empty when generating
-
    /// diffs.
-
    pub text: Vec<u8>,
-
}
-

-
impl TryFrom<&Hunk<Modification>> for HunkHeader {
-
    type Error = Error;
-

-
    fn try_from(hunk: &Hunk<Modification>) -> Result<Self, Self::Error> {
-
        let mut r = io::BufReader::new(hunk.header.as_bytes());
-
        Self::decode(&mut r)
-
    }
-
}
-

-
impl HunkHeader {
-
    pub fn old_line_range(&self) -> std::ops::Range<u32> {
-
        let start: u32 = self.old_line_no;
-
        let end: u32 = self.old_line_no + self.old_size;
-
        start..end + 1
-
    }
-

-
    pub fn new_line_range(&self) -> std::ops::Range<u32> {
-
        let start: u32 = self.new_line_no;
-
        let end: u32 = self.new_line_no + self.new_size;
-
        start..end + 1
-
    }
-
}
-

-
/// Diff-related types that can be decoded from the unified diff format.
-
pub trait Decode: Sized {
-
    /// Decode, and fail if we reach the end of the stream.
-
    fn decode(r: &mut impl io::BufRead) -> Result<Self, Error>;
-

-
    /// Decode, and return a `None` if we reached the end of the stream.
-
    fn try_decode(r: &mut impl io::BufRead) -> Result<Option<Self>, Error> {
-
        match Self::decode(r) {
-
            Ok(v) => Ok(Some(v)),
-
            Err(Error::UnexpectedEof) => Ok(None),
-
            Err(e) => Err(e),
-
        }
-
    }
-

-
    /// Decode from a string input.
-
    fn parse(s: &str) -> Result<Self, Error> {
-
        Self::from_bytes(s.as_bytes())
-
    }
-

-
    /// Decode from a string input.
-
    fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
-
        let mut r = io::BufReader::new(bytes);
-
        Self::decode(&mut r)
-
    }
-
}
-

-
/// Diff-related types that can be encoded intro the unified diff format.
-
pub trait Encode: Sized {
-
    /// Encode type into diff writer.
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error>;
-

-
    /// Encode into unified diff string.
-
    fn to_unified_string(&self) -> Result<String, Error> {
-
        let mut buf = Vec::new();
-
        let mut w = Writer::new(&mut buf);
-

-
        w.encode(self)?;
-
        drop(w);
-

-
        String::from_utf8(buf).map_err(Error::from)
-
    }
-
}
-

-
impl Decode for Diff {
-
    /// Decode from git's unified diff format, consuming the entire input.
-
    fn decode(r: &mut impl io::BufRead) -> Result<Self, Error> {
-
        let mut s = String::new();
-

-
        r.read_to_string(&mut s)?;
-

-
        let d = git::raw::Diff::from_buffer(s.as_ref())
-
            .map_err(|e| Error::syntax(format!("decoding unified diff: {e}")))?;
-
        let d =
-
            Diff::try_from(d).map_err(|e| Error::syntax(format!("decoding unified diff: {e}")))?;
-

-
        Ok(d)
-
    }
-
}
-

-
impl Encode for Diff {
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error> {
-
        for fdiff in self.files() {
-
            fdiff.encode(w)?;
-
        }
-
        Ok(())
-
    }
-
}
-

-
impl Decode for DiffContent {
-
    fn decode(r: &mut impl io::BufRead) -> Result<Self, Error> {
-
        let mut hunks = Vec::default();
-
        let mut additions = 0;
-
        let mut deletions = 0;
-

-
        while let Some(h) = Hunk::try_decode(r)? {
-
            for l in &h.lines {
-
                match l {
-
                    Modification::Addition(_) => additions += 1,
-
                    Modification::Deletion(_) => deletions += 1,
-
                    _ => {}
-
                }
-
            }
-
            hunks.push(h);
-
        }
-

-
        if hunks.is_empty() {
-
            Ok(DiffContent::Empty)
-
        } else {
-
            // TODO: Handle case for binary.
-
            Ok(DiffContent::Plain {
-
                hunks: Hunks::from(hunks),
-
                stats: FileStats {
-
                    additions,
-
                    deletions,
-
                },
-
                // TODO: Properly handle EndOfLine field
-
                eof: diff::EofNewLine::NoneMissing,
-
            })
-
        }
-
    }
-
}
-

-
impl Encode for DiffContent {
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error> {
-
        match self {
-
            DiffContent::Plain { hunks, .. } => {
-
                for h in hunks.iter() {
-
                    h.encode(w)?;
-
                }
-
            }
-
            DiffContent::Empty => {}
-
            DiffContent::Binary => todo!("DiffContent::Binary encoding not implemented"),
-
        }
-
        Ok(())
-
    }
-
}
-

-
impl Encode for FileDiff {
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error> {
-
        w.encode(&FileHeader::from(self))?;
-
        match self {
-
            FileDiff::Modified(f) => {
-
                w.encode(&f.diff)?;
-
            }
-
            FileDiff::Added(f) => {
-
                w.encode(&f.diff)?;
-
            }
-
            FileDiff::Copied(f) => {
-
                w.encode(&f.diff)?;
-
            }
-
            FileDiff::Deleted(f) => {
-
                w.encode(&f.diff)?;
-
            }
-
            FileDiff::Moved(f) => {
-
                // Nb. We only display diffs as moves when the file was not changed.
-
                w.encode(&f.diff)?;
-
            }
-
        }
-

-
        Ok(())
-
    }
-
}
-

-
impl Encode for FileHeader {
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error> {
-
        match self {
-
            FileHeader::Modified { path, old, new, .. } => {
-
                w.meta(format!(
-
                    "diff --git a/{} b/{}",
-
                    path.display(),
-
                    path.display()
-
                ))?;
-

-
                if old.mode == new.mode {
-
                    w.meta(format!(
-
                        "index {}..{} {:o}",
-
                        term::format::oid(*old.oid),
-
                        term::format::oid(*new.oid),
-
                        u32::from(old.mode.clone()),
-
                    ))?;
-
                } else {
-
                    w.meta(format!("old mode {:o}", u32::from(old.mode.clone())))?;
-
                    w.meta(format!("new mode {:o}", u32::from(new.mode.clone())))?;
-
                    w.meta(format!(
-
                        "index {}..{}",
-
                        term::format::oid(*old.oid),
-
                        term::format::oid(*new.oid)
-
                    ))?;
-
                }
-

-
                w.meta(format!("--- a/{}", path.display()))?;
-
                w.meta(format!("+++ b/{}", path.display()))?;
-
            }
-
            FileHeader::Added { path, new, .. } => {
-
                w.meta(format!(
-
                    "diff --git a/{} b/{}",
-
                    path.display(),
-
                    path.display()
-
                ))?;
-

-
                w.meta(format!("new file mode {:o}", u32::from(new.mode.clone())))?;
-
                w.meta(format!(
-
                    "index {}..{}",
-
                    term::format::oid(git::Oid::sha1_zero()),
-
                    term::format::oid(*new.oid),
-
                ))?;
-

-
                w.meta("--- /dev/null")?;
-
                w.meta(format!("+++ b/{}", path.display()))?;
-
            }
-
            FileHeader::Copied { .. } => todo!(),
-
            FileHeader::Deleted { path, old, .. } => {
-
                w.meta(format!(
-
                    "diff --git a/{} b/{}",
-
                    path.display(),
-
                    path.display()
-
                ))?;
-

-
                w.meta(format!(
-
                    "deleted file mode {:o}",
-
                    u32::from(old.mode.clone())
-
                ))?;
-
                w.meta(format!(
-
                    "index {}..{}",
-
                    term::format::oid(*old.oid),
-
                    term::format::oid(git::Oid::sha1_zero())
-
                ))?;
-

-
                w.meta(format!("--- a/{}", path.display()))?;
-
                w.meta("+++ /dev/null".to_string())?;
-
            }
-
            FileHeader::Moved { old_path, new_path } => {
-
                w.meta(format!(
-
                    "diff --git a/{} b/{}",
-
                    old_path.display(),
-
                    new_path.display()
-
                ))?;
-
                w.meta("similarity index 100%")?;
-
                w.meta(format!("rename from {}", old_path.display()))?;
-
                w.meta(format!("rename to {}", new_path.display()))?;
-
            }
-
        };
-
        Ok(())
-
    }
-
}
-

-
impl Decode for HunkHeader {
-
    fn decode(r: &mut impl io::BufRead) -> Result<Self, Error> {
-
        let mut line = String::default();
-
        if r.read_line(&mut line)? == 0 {
-
            return Err(Error::UnexpectedEof);
-
        };
-

-
        let mut header = HunkHeader::default();
-
        let s = line
-
            .strip_prefix("@@ -")
-
            .ok_or(Error::syntax("missing '@@ -'"))?;
-

-
        let (old, s) = s
-
            .split_once(" +")
-
            .ok_or(Error::syntax("missing new line information"))?;
-
        let (line_no, size) = old.split_once(',').unwrap_or((old, "1"));
-

-
        header.old_line_no = line_no.parse()?;
-
        header.old_size = size.parse()?;
-

-
        let (new, s) = s
-
            .split_once(" @@")
-
            .ok_or(Error::syntax("closing '@@' is missing"))?;
-
        let (line_no, size) = new.split_once(',').unwrap_or((new, "1"));
-

-
        header.new_line_no = line_no.parse()?;
-
        header.new_size = size.parse()?;
-

-
        let s = s.strip_prefix(' ').unwrap_or(s);
-
        header.text = s.as_bytes().to_vec();
-

-
        Ok(header)
-
    }
-
}
-

-
impl Encode for HunkHeader {
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error> {
-
        let old = if self.old_size == 1 {
-
            format!("{}", self.old_line_no)
-
        } else {
-
            format!("{},{}", self.old_line_no, self.old_size)
-
        };
-
        let new = if self.new_size == 1 {
-
            format!("{}", self.new_line_no)
-
        } else {
-
            format!("{},{}", self.new_line_no, self.new_size)
-
        };
-
        let text = if self.text.is_empty() {
-
            "".to_string()
-
        } else {
-
            format!(" {}", String::from_utf8_lossy(&self.text))
-
        };
-
        w.meta(format!("@@ -{old} +{new} @@{text}"))?;
-

-
        Ok(())
-
    }
-
}
-

-
impl Decode for Hunk<Modification> {
-
    fn decode(r: &mut impl io::BufRead) -> Result<Self, Error> {
-
        let header = HunkHeader::decode(r)?;
-

-
        let mut lines = Vec::new();
-
        let mut new_line: u32 = 0;
-
        let mut old_line: u32 = 0;
-

-
        while old_line < header.old_size || new_line < header.new_size {
-
            if old_line > header.old_size {
-
                return Err(Error::syntax(format!(
-
                    "expected '{}' old lines",
-
                    header.old_size
-
                )));
-
            } else if new_line > header.new_size {
-
                return Err(Error::syntax(format!(
-
                    "expected '{0}' new lines",
-
                    header.new_size
-
                )));
-
            }
-

-
            let Some(line) = Modification::try_decode(r)? else {
-
                return Err(Error::syntax(format!(
-
                    "expected '{}' old lines and '{}' new lines, but found '{}' and '{}'",
-
                    header.old_size, header.new_size, old_line, new_line,
-
                )));
-
            };
-

-
            let line = match line {
-
                Modification::Addition(v) => {
-
                    let l = Modification::addition(v.line, header.new_line_no + new_line);
-
                    new_line += 1;
-
                    l
-
                }
-
                Modification::Deletion(v) => {
-
                    let l = Modification::deletion(v.line, header.old_line_no + old_line);
-
                    old_line += 1;
-
                    l
-
                }
-
                Modification::Context { line, .. } => {
-
                    let l = Modification::Context {
-
                        line,
-
                        line_no_old: header.old_line_no + old_line,
-
                        line_no_new: header.new_line_no + new_line,
-
                    };
-
                    new_line += 1;
-
                    old_line += 1;
-
                    l
-
                }
-
            };
-

-
            lines.push(line);
-
        }
-

-
        Ok(Hunk {
-
            header: Line::from(header.to_unified_string()?),
-
            lines,
-
            old: header.old_line_range(),
-
            new: header.new_line_range(),
-
        })
-
    }
-
}
-

-
impl Encode for Hunk<Modification> {
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error> {
-
        // TODO: Remove trailing newlines accurately.
-
        // `trim_end()` will destroy diff information if the diff has a trailing whitespace on
-
        // purpose.
-
        w.magenta(self.header.from_utf8_lossy().trim_end())?;
-
        for l in &self.lines {
-
            l.encode(w)?;
-
        }
-

-
        Ok(())
-
    }
-
}
-

-
impl Decode for Modification {
-
    fn decode(r: &mut impl io::BufRead) -> Result<Self, Error> {
-
        let mut line = String::new();
-
        if r.read_line(&mut line)? == 0 {
-
            return Err(Error::UnexpectedEof);
-
        };
-

-
        let mut chars = line.chars();
-
        let l = match chars.next() {
-
            Some('+') => Modification::addition(chars.as_str().to_string(), 0),
-
            Some('-') => Modification::deletion(chars.as_str().to_string(), 0),
-
            Some(' ') => Modification::Context {
-
                line: chars.as_str().to_string().into(),
-
                line_no_old: 0,
-
                line_no_new: 0,
-
            },
-
            Some(c) => {
-
                return Err(Error::syntax(format!(
-
                    "indicator character expected, but got '{c}'",
-
                )))
-
            }
-
            None => return Err(Error::UnexpectedEof),
-
        };
-

-
        Ok(l)
-
    }
-
}
-

-
impl Encode for Modification {
-
    fn encode(&self, w: &mut Writer) -> Result<(), Error> {
-
        match self {
-
            Modification::Deletion(radicle_surf::diff::Deletion { line, .. }) => {
-
                let s = format!("-{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Red))?;
-
            }
-
            Modification::Addition(radicle_surf::diff::Addition { line, .. }) => {
-
                let s = format!("+{}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::new(term::Color::Green))?;
-
            }
-
            Modification::Context { line, .. } => {
-
                let s = format!(" {}", String::from_utf8_lossy(line.as_bytes()).trim_end());
-
                w.write(s, term::Style::default().dim())?;
-
            }
-
        }
-

-
        Ok(())
-
    }
-
}
-

-
/// An IO Writer with color printing to the terminal.
-
pub struct Writer<'a> {
-
    styled: bool,
-
    stream: Box<dyn io::Write + 'a>,
-
}
-

-
impl<'a> Writer<'a> {
-
    pub fn new(w: impl io::Write + 'a) -> Self {
-
        Self {
-
            styled: false,
-
            stream: Box::new(w),
-
        }
-
    }
-

-
    pub fn encode<T: Encode>(&mut self, arg: &T) -> Result<(), Error> {
-
        arg.encode(self)?;
-
        Ok(())
-
    }
-

-
    pub fn styled(mut self, value: bool) -> Self {
-
        self.styled = value;
-
        self
-
    }
-

-
    pub fn write(&mut self, s: impl fmt::Display, style: term::Style) -> io::Result<()> {
-
        #[cfg(windows)]
-
        const EOL: &str = "\r\n";
-

-
        #[cfg(not(windows))]
-
        const EOL: &str = "\n";
-

-
        if self.styled {
-
            write!(
-
                self.stream,
-
                "{}{EOL}",
-
                term::Paint::new(s).with_style(style)
-
            )
-
        } else {
-
            write!(self.stream, "{s}{EOL}")
-
        }
-
    }
-

-
    pub fn meta(&mut self, s: impl fmt::Display) -> io::Result<()> {
-
        self.write(s, term::Style::new(term::Color::Yellow))
-
    }
-

-
    pub fn magenta(&mut self, s: impl fmt::Display) -> io::Result<()> {
-
        self.write(s, term::Style::new(term::Color::Magenta))
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use super::*;
-

-
    #[test]
-
    fn test_diff_encode_decode_diff() {
-
        let diff_a = diff::Diff::parse(include_str!(concat!(
-
            env!("CARGO_MANIFEST_DIR"),
-
            "/tests/data/diff.diff"
-
        )))
-
        .unwrap();
-
        assert_eq!(
-
            include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/diff.diff")),
-
            diff_a.to_unified_string().unwrap()
-
        );
-
    }
-

-
    #[test]
-
    fn test_diff_content_encode_decode_content() {
-
        let diff_content = diff::DiffContent::parse(include_str!(concat!(
-
            env!("CARGO_MANIFEST_DIR"),
-
            "/tests/data/diff_body.diff"
-
        )))
-
        .unwrap();
-
        assert_eq!(
-
            include_str!(concat!(
-
                env!("CARGO_MANIFEST_DIR"),
-
                "/tests/data/diff_body.diff"
-
            )),
-
            diff_content.to_unified_string().unwrap()
-
        );
-
    }
-

-
    // TODO: Test parsing a real diff from this repository.
-
}