use std::error::Error;
use std::io::Result;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use diff::DiffItem;
use diff::DiffItem::*;
use diff::Side;
use owo_colors::OwoColorize;
use owo_colors::Style;
use crate::IgnoreWhitespace;
use crate::cluster::DiffCluster;
use crate::diff;
use crate::files::FilePairEvent;
use crate::hunks::Hunk;
use crate::parse_diff::ParsedFileDiff;
use crate::parse_diff::ParsedHunkItem;
use crate::styles::Styles;
use crate::tokenize_lines;
use crate::tokens::split_lines;
pub struct Printer<'a> {
styles: Styles,
writer: &'a mut dyn Write,
context: usize,
common_prefix: PathBuf,
pub ignore_whitespace: IgnoreWhitespace,
}
impl<'a> Printer<'a> {
pub fn default(
writer: &'a mut dyn Write,
context: usize,
common_prefix: PathBuf,
) -> Printer<'a> {
Printer {
styles: Styles::simple(),
writer,
context,
common_prefix,
ignore_whitespace: IgnoreWhitespace::default(),
}
}
pub fn new(
writer: &'a mut dyn Write,
context: usize,
common_prefix: PathBuf,
ignore_whitespace: IgnoreWhitespace,
) -> Printer<'a> {
Printer {
styles: Styles::simple(),
writer,
context,
common_prefix,
ignore_whitespace,
}
}
pub fn print_file_pair_event(&mut self, event: FilePairEvent) -> Result<()> {
match event {
FilePairEvent::TextDiff {
lhs_path,
rhs_path,
lhs_content,
rhs_content,
} => {
self.print_file_header(lhs_path.as_deref(), rhs_path.as_deref())?;
self.print_diff(true, &lhs_content, &rhs_content)?;
}
FilePairEvent::Binary { lhs_path, rhs_path } => {
self.print_binary_files_differ(
lhs_path.as_deref(),
rhs_path.as_deref(),
)?;
}
FilePairEvent::TypeMismatch { lhs_path, rhs_path } => {
self.print_directory_mismatch(&lhs_path, &rhs_path)?;
}
FilePairEvent::IoError { lhs_path, rhs_path, err } => {
writeln!(
self.writer,
"Error diffing {} and {}:\n{}",
self
.display_name(lhs_path.as_deref())
.style(self.styles.old),
self
.display_name(rhs_path.as_deref())
.style(self.styles.new),
err,
)?;
}
}
Ok(())
}
fn display_name(&self, p: Option<&Path>) -> String {
let Some(p) = p else {
return "/dev/null".into();
};
let stripped = p.strip_prefix(&self.common_prefix).unwrap_or(p);
if let Ok(link) = std::fs::read_link(p) {
let stripped_link =
link.strip_prefix(&self.common_prefix).unwrap_or(&link);
return format!("{} -> {}", stripped.display(), stripped_link.display());
}
stripped.display().to_string()
}
pub fn print_error(
&mut self,
lhs: Option<&Path>,
rhs: Option<&Path>,
err: Box<dyn Error>,
) -> Result<()> {
writeln!(
self.writer,
"Error diffing {} and {}:\n{}",
self.display_name(lhs).style(self.styles.old),
self.display_name(rhs).style(self.styles.new),
err
)
}
pub fn print_directory_mismatch(
&mut self,
lhs: &Path,
rhs: &Path,
) -> Result<()> {
fn ft(p: &Path) -> &str {
if p.metadata().unwrap().is_dir() { "directory" } else { "file" }
}
writeln!(
self.writer,
"File/directory mistmatch:\n {} is a {}\n {} is a {}",
self.display_name(Some(lhs)).style(self.styles.old),
ft(lhs),
self.display_name(Some(rhs)).style(self.styles.new),
ft(rhs),
)
}
pub fn print_binary_files_differ(
&mut self,
lhs: Option<&Path>,
rhs: Option<&Path>,
) -> Result<()> {
writeln!(
self.writer,
"Binary files {} and {} differ",
self.display_name(lhs).style(self.styles.old),
self.display_name(rhs).style(self.styles.new),
)?;
Ok(())
}
pub fn print_file_header(
&mut self,
lhs: Option<&Path>,
rhs: Option<&Path>,
) -> Result<()> {
writeln!(
self.writer,
"{} {}",
"---".style(self.styles.old),
self.display_name(lhs).style(self.styles.header),
)?;
writeln!(
self.writer,
"{} {}",
"+++".style(self.styles.new),
self.display_name(rhs).style(self.styles.header)
)?;
Ok(())
}
pub fn print_diff(
&mut self,
include_headers: bool,
lhs_all: &[u8],
rhs_all: &[u8],
) -> Result<()> {
let lhs = split_lines(lhs_all);
let rhs = split_lines(rhs_all);
let diffs = diff(&lhs, &rhs, self.ignore_whitespace);
let hunks = Hunk::build(self.context, &diffs);
for h in hunks {
if include_headers {
self.print_hunk_header(&h)?;
}
self.print_hunk_body(&lhs, &rhs, &h.diffs)?;
}
Ok(())
}
fn print_hunk_header(&mut self, h: &Hunk) -> Result<()> {
let (l, r) = (h.lhs(), h.rhs());
writeln!(
self.writer,
"{}",
format!(
"@@ -{},{} +{},{} @@",
l.start + 1,
l.end - l.start,
r.start + 1,
r.end - r.start
)
.style(self.styles.separator)
)?;
Ok(())
}
fn print_hunk_body(
&mut self,
lhs_lines: &[&[u8]],
rhs_lines: &[&[u8]],
diffs: &[DiffItem],
) -> Result<()> {
for d in diffs {
match &d {
Mutation { lhs, rhs } => {
self.print_mutation_block(
&lhs_lines[*lhs],
&rhs_lines[*rhs],
)?;
}
Match { lhs, .. } => {
self.print_lines(&lhs_lines[*lhs], " ", self.styles.both)?;
}
}
}
Ok(())
}
fn print_mutation_block(
&mut self,
old: &[&[u8]],
new: &[&[u8]],
) -> Result<()> {
if new.is_empty() {
self.print_lines(old, "-", self.styles.old)?;
} else if old.is_empty() {
self.print_lines(new, "+", self.styles.new)?;
} else {
self.print_mutation(old, new)?;
}
Ok(())
}
fn print_lines(
&mut self,
lines: &[&[u8]],
prefix: &str,
style: Style,
) -> Result<()> {
for line in lines {
let s = String::from_utf8_lossy(line);
writeln!(self.writer, "{}{}", prefix.style(style), s.style(style))?;
}
Ok(())
}
fn print_mutation(
&mut self,
lhs_lines: &[&[u8]],
rhs_lines: &[&[u8]],
) -> Result<()> {
let lhs_tokens = tokenize_lines(lhs_lines);
let rhs_tokens = tokenize_lines(rhs_lines);
let diffs = diff(&lhs_tokens, &rhs_tokens, self.ignore_whitespace);
self.print_mutation_side(
&lhs_tokens,
&diffs,
"-",
Side::Lhs,
self.styles.old,
self.styles.old_dim,
)?;
self.print_mutation_side(
&rhs_tokens,
&diffs,
"+",
Side::Rhs,
self.styles.new,
self.styles.new_dim,
)?;
Ok(())
}
fn print_mutation_side(
&mut self,
tokens: &[&[u8]],
diffs: &[DiffItem],
prefix: &str,
side: Side,
mutation: Style,
matching: Style,
) -> Result<()> {
write!(self.writer, "{}", prefix.style(mutation))?;
for d in diffs {
let style = if matches!(d, Match { .. }) { matching } else { mutation };
for &t in &tokens[d.side(side)] {
let s = String::from_utf8_lossy(t);
write!(self.writer, "{}", s.style(style))?;
if t == b"\n" {
write!(self.writer, "{}", prefix.style(mutation))?;
}
}
}
writeln!(self.writer)?;
Ok(())
}
pub fn print_parsed_file_diff(
&mut self,
diff: &ParsedFileDiff,
) -> Result<()> {
for line in &diff.preamble {
let s = String::from_utf8_lossy(line);
writeln!(self.writer, "{}", s.style(self.styles.header))?;
}
if let Some(old_path) = &diff.old_path {
writeln!(
self.writer,
"{} {}",
"---".style(self.styles.old),
old_path.display().style(self.styles.header),
)?;
}
if let Some(new_path) = &diff.new_path {
writeln!(
self.writer,
"{} {}",
"+++".style(self.styles.new),
new_path.display().style(self.styles.header),
)?;
}
for hunk in &diff.hunks {
if let Some(header) = &hunk.header {
let s = String::from_utf8_lossy(header);
writeln!(self.writer, "{}", s.style(self.styles.separator))?;
}
for item in &hunk.items {
match item {
ParsedHunkItem::Context(line) => {
self.print_lines(
std::slice::from_ref(line),
" ",
self.styles.both,
)?;
}
ParsedHunkItem::Mutation(m) => {
self.print_mutation_block(&m.old, &m.new)?;
}
}
}
}
Ok(())
}
pub fn print_clusters(&mut self, clusters: &[DiffCluster]) -> Result<()> {
for cluster in clusters {
self.print_cluster(cluster)?;
}
Ok(())
}
fn print_cluster(&mut self, cluster: &DiffCluster) -> Result<()> {
let total: usize = cluster.entries.values().sum();
let entry_count =
|n| format!("{} {}", n, if 1 == n { "entry" } else { "entries" });
writeln!(
self.writer,
"{}",
format!("=== cluster contains {}", entry_count(total))
.style(self.styles.header),
)?;
for (entry, &count) in &cluster.entries {
let lhs = self.display_name(entry.lhs_path.as_deref());
let rhs = self.display_name(entry.rhs_path.as_deref());
write!(self.writer, "{}", "= ".style(self.styles.separator))?;
write!(self.writer, "{}", lhs.style(self.styles.old))?;
write!(self.writer, "{}", " => ".style(self.styles.separator))?;
write!(self.writer, "{}", rhs.style(self.styles.new))?;
writeln!(
self.writer,
"{}",
format!(": {}", entry_count(count)).style(self.styles.separator)
)?;
}
writeln!(
self.writer,
"{}",
"=== example diff: ".style(self.styles.separator)
)?;
self.print_diff(false, &cluster.exemplar_lhs, &cluster.exemplar_rhs)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_diff;
fn printer_for(buf: &mut Vec<u8>) -> Printer<'_> {
Printer::new(buf, 3, PathBuf::new(), IgnoreWhitespace::No)
}
fn plain(buf: Vec<u8>) -> String {
let stripped = anstream::adapter::strip_bytes(&buf).into_vec();
String::from_utf8(stripped).unwrap()
}
#[test]
fn render_unified_roundtrip() {
let input = b"\
--- a/file.txt\n\
+++ b/file.txt\n\
@@ -1,3 +1,3 @@\n\
context before\n\
-old line\n\
+new line\n\
context after\n\
";
let diffs = parse_diff::parse(input);
let mut buf = Vec::new();
printer_for(&mut buf)
.print_parsed_file_diff(&diffs[0])
.unwrap();
let out = plain(buf);
assert!(out.contains("@@ -1,3 +1,3 @@"), "hunk header should be preserved");
assert!(out.contains(" context before"), "context line with space prefix");
assert!(out.contains(" context after"), "context line with space prefix");
assert!(out.contains("-old line"), "old line with minus prefix");
assert!(out.contains("+new line"), "new line with plus prefix");
}
#[test]
fn render_traditional_hunk_header_translated() {
let input = b"\
3c3\n\
< old line\n\
---\n\
> new line\n\
";
let diffs = parse_diff::parse(input);
let mut buf = Vec::new();
printer_for(&mut buf)
.print_parsed_file_diff(&diffs[0])
.unwrap();
let out = plain(buf);
assert!(
out.contains("@@ -3,1 +3,1 @@"),
"traditional 3c3 should be translated to unified header"
);
assert!(out.contains("-old line"));
assert!(out.contains("+new line"));
}
}