Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-cli src git ddiff.rs
//! `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.
    #[must_use]
    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()
        );
    }
}