//! This module provides unified diff functionality. //! //! It is available for as long as the `text` feature is enabled which //! is enabled by default: //! //! ```rust //! use similar::TextDiff; //! # let old_text = ""; //! # let new_text = ""; //! let text_diff = TextDiff::from_lines(old_text, new_text); //! print!("{}", text_diff //! .unified_diff() //! .context_radius(10) //! .header("old_file", "new_file")); //! ``` //! //! # Unicode vs Bytes //! //! The [`UnifiedDiff`] type supports both unicode and byte diffs for all //! types compatible with [`DiffableStr`]. You can pick between the two //! versions by using the [`Display`](std::fmt::Display) implementation or //! [`UnifiedDiff`] or [`UnifiedDiff::to_writer`]. //! //! The former uses [`DiffableStr::to_string_lossy`], the latter uses //! [`DiffableStr::as_bytes`] for each line. #[cfg(feature = "text")] use std::{fmt, io}; use crate::iter::AllChangesIter; use crate::text::{DiffableStr, TextDiff}; use crate::types::{Algorithm, DiffOp}; struct MissingNewlineHint(bool); impl fmt::Display for MissingNewlineHint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.0 { write!(f, "\n\\ No newline at end of file")?; } Ok(()) } } #[derive(Copy, Clone, Debug)] struct UnifiedDiffHunkRange(usize, usize); impl UnifiedDiffHunkRange { fn start(&self) -> usize { self.0 } fn end(&self) -> usize { self.1 } } impl fmt::Display for UnifiedDiffHunkRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut beginning = self.start() + 1; let len = self.end().saturating_sub(self.start()); if len == 1 { write!(f, "{}", beginning) } else { if len == 0 { // empty ranges begin at line just before the range beginning -= 1; } write!(f, "{},{}", beginning, len) } } } /// Unified diff hunk header formatter. pub struct UnifiedHunkHeader { old_range: UnifiedDiffHunkRange, new_range: UnifiedDiffHunkRange, } impl UnifiedHunkHeader { /// Creates a hunk header from a (non empty) slice of diff ops. pub fn new(ops: &[DiffOp]) -> UnifiedHunkHeader { let first = ops[0]; let last = ops[ops.len() - 1]; let old_start = first.old_range().start; let new_start = first.new_range().start; let old_end = last.old_range().end; let new_end = last.new_range().end; UnifiedHunkHeader { old_range: UnifiedDiffHunkRange(old_start, old_end), new_range: UnifiedDiffHunkRange(new_start, new_end), } } } impl fmt::Display for UnifiedHunkHeader { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "@@ -{} +{} @@", &self.old_range, &self.new_range) } } /// Unified diff formatter. /// /// ```rust /// use similar::TextDiff; /// # let old_text = ""; /// # let new_text = ""; /// let text_diff = TextDiff::from_lines(old_text, new_text); /// print!("{}", text_diff /// .unified_diff() /// .context_radius(10) /// .header("old_file", "new_file")); /// ``` /// /// ## Unicode vs Bytes /// /// The [`UnifiedDiff`] type supports both unicode and byte diffs for all /// types compatible with [`DiffableStr`]. You can pick between the two /// versions by using [`UnifiedDiff.to_string`] or [`UnifiedDiff.to_writer`]. /// The former uses [`DiffableStr::to_string_lossy`], the latter uses /// [`DiffableStr::as_bytes`] for each line. pub struct UnifiedDiff<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> { diff: &'diff TextDiff<'old, 'new, 'bufs, T>, context_radius: usize, missing_newline_hint: bool, header: Option<(String, String)>, } impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> UnifiedDiff<'diff, 'old, 'new, 'bufs, T> { /// Creates a formatter from a text diff object. pub fn from_text_diff(diff: &'diff TextDiff<'old, 'new, 'bufs, T>) -> Self { UnifiedDiff { diff, context_radius: 3, missing_newline_hint: true, header: None, } } /// Changes the context radius. /// /// The context radius is the number of lines between changes that should /// be emitted. This defaults to `3`. pub fn context_radius(&mut self, n: usize) -> &mut Self { self.context_radius = n; self } /// Sets a header to the diff. /// /// `a` and `b` are the file names that are added to the top of the unified /// file format. The names are accepted verbatim which lets you encode /// a timestamp into it when separated by a tab (`\t`). For more information /// see [the unified diff format specification](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/diff.html#tag_20_34_10_07) pub fn header(&mut self, a: &str, b: &str) -> &mut Self { self.header = Some((a.to_string(), b.to_string())); self } /// Controls the missing newline hint. /// /// By default a special `\ No newline at end of file` marker is added to /// the output when a file is not terminated with a final newline. This can /// be disabled with this flag. pub fn missing_newline_hint(&mut self, yes: bool) -> &mut Self { self.missing_newline_hint = yes; self } /// Iterates over all hunks as configured. pub fn iter_hunks(&self) -> impl Iterator> { let diff = self.diff; let missing_newline_hint = self.missing_newline_hint; self.diff .grouped_ops(self.context_radius) .into_iter() .filter(|ops| !ops.is_empty()) .map(move |ops| UnifiedDiffHunk::new(ops, diff, missing_newline_hint)) } /// Write the unified diff as bytes to the output stream. pub fn to_writer(&self, mut w: W) -> Result<(), io::Error> where 'diff: 'old + 'new + 'bufs, { let mut header = self.header.as_ref(); for hunk in self.iter_hunks() { if let Some((old_file, new_file)) = header.take() { writeln!(w, "--- {}", old_file)?; writeln!(w, "+++ {}", new_file)?; } write!(w, "{}", hunk)?; } Ok(()) } fn header_opt(&mut self, header: Option<(&str, &str)>) -> &mut Self { if let Some((a, b)) = header { self.header(a, b); } self } } /// Unified diff hunk formatter. /// /// The `Display` this renders out a single unified diff's hunk. pub struct UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> { diff: &'diff TextDiff<'old, 'new, 'bufs, T>, ops: Vec, missing_newline_hint: bool, } impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T> { /// Creates a new hunk for some operations. pub fn new( ops: Vec, diff: &'diff TextDiff<'old, 'new, 'bufs, T>, missing_newline_hint: bool, ) -> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T> { UnifiedDiffHunk { diff, ops, missing_newline_hint, } } /// Returns the header for the hunk. pub fn header(&self) -> UnifiedHunkHeader { UnifiedHunkHeader::new(&self.ops) } /// Returns all operations in the hunk. pub fn ops(&self) -> &[DiffOp] { &self.ops } /// Returns the value of the `missing_newline_hint` flag. pub fn missing_newline_hint(&self) -> bool { self.missing_newline_hint } /// Iterates over all changes in a hunk. pub fn iter_changes<'x, 'slf>(&'slf self) -> AllChangesIter<'slf, 'x, T> where 'x: 'slf + 'old + 'new, 'old: 'x, 'new: 'x, { AllChangesIter::new(self.diff.old_slices(), self.diff.new_slices(), self.ops()) } /// Write the hunk as bytes to the output stream. pub fn to_writer(&self, mut w: W) -> Result<(), io::Error> where 'diff: 'old + 'new + 'bufs, { for (idx, change) in self.iter_changes().enumerate() { if idx == 0 { writeln!(w, "{}", self.header())?; } write!(w, "{}", change.tag())?; w.write_all(change.value().as_bytes())?; if !self.diff.newline_terminated() { writeln!(w)?; } if self.diff.newline_terminated() && change.missing_newline() { writeln!(w, "{}", MissingNewlineHint(self.missing_newline_hint))?; } } Ok(()) } } impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display for UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T> where 'diff: 'old + 'new + 'bufs, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for (idx, change) in self.iter_changes().enumerate() { if idx == 0 { writeln!(f, "{}", self.header())?; } write!(f, "{}{}", change.tag(), change.to_string_lossy())?; if !self.diff.newline_terminated() { writeln!(f)?; } if self.diff.newline_terminated() && change.missing_newline() { writeln!(f, "{}", MissingNewlineHint(self.missing_newline_hint))?; } } Ok(()) } } impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display for UnifiedDiff<'diff, 'old, 'new, 'bufs, T> where 'diff: 'old + 'new + 'bufs, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut header = self.header.as_ref(); for hunk in self.iter_hunks() { if let Some((old_file, new_file)) = header.take() { writeln!(f, "--- {}", old_file)?; writeln!(f, "+++ {}", new_file)?; } write!(f, "{}", hunk)?; } Ok(()) } } /// Quick way to get a unified diff as string. /// /// `n` configures [`UnifiedDiff::context_radius`] and /// `header` configures [`UnifiedDiff::header`] when not `None`. pub fn unified_diff<'old, 'new>( alg: Algorithm, old: &'old str, new: &'new str, n: usize, header: Option<(&str, &str)>, ) -> String { TextDiff::configure() .algorithm(alg) .diff_lines(old, new) .unified_diff() .context_radius(n) .header_opt(header) .to_string() } #[test] fn test_unified_diff() { let diff = TextDiff::from_lines( "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\nS\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\no\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ", ); insta::assert_snapshot!(&diff.unified_diff().header("a.txt", "b.txt").to_string()); } #[test] fn test_empty_unified_diff() { let diff = TextDiff::from_lines("abc", "abc"); assert_eq!(diff.unified_diff().header("a.txt", "b.txt").to_string(), ""); } #[test] fn test_unified_diff_newline_hint() { let diff = TextDiff::from_lines("a\n", "b"); insta::assert_snapshot!(&diff.unified_diff().header("a.txt", "b.txt").to_string()); insta::assert_snapshot!(&diff .unified_diff() .missing_newline_hint(false) .header("a.txt", "b.txt") .to_string()); }