use radicle_surf::diff::*;
use std::io;
use crate::git::unified_diff;
use crate::git::unified_diff::{Encode, Writer};
use crate::terminal as term;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiffModification {
AdditionAddition { line: Line, line_no: u32 },
AdditionContext {
line: Line,
line_no_old: u32,
line_no_new: u32,
},
AdditionDeletion { line: Line, line_no: u32 },
ContextAddition { line: Line, line_no: u32 },
ContextContext {
line: Line,
line_no_old: u32,
line_no_new: u32,
},
ContextDeletion { line: Line, line_no: u32 },
DeletionAddition { line: Line, line_no: u32 },
DeletionContext {
line: Line,
line_no_old: u32,
line_no_new: u32,
},
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> {
w.magenta(self.header.from_utf8_lossy().trim_end())?;
for l in &self.lines {
l.encode(w)?;
}
Ok(())
}
}
#[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 '{0}{1}'",
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(())
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct DDiff {
files: Vec<FileDDiff>,
}
impl DDiff {
pub fn files(&self) -> impl Iterator<Item = &FileDDiff> {
self.files.iter()
}
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()
);
}
}