// Modified for gitoxide from the upstream imara-diff crate.// Upstream source: git cat-file -p 32d1e45d3df061e6ccba6db7fdce92db29e345d8:src/unified_diff.rs use std::fmt::{self, Display};use std::hash::Hash; use crate::Diff;use crate::intern::{InternedInput, Interner, Token}; impl Diff { /// Creates a unified diff output that can be formatted as a string. /// /// This is a convenience method that extracts the token sequences from the `input`. /// /// # Parameters /// /// * `printer` - A printer implementation that controls how tokens are displayed /// * `config` - Configuration options for the unified diff format /// * `input` - The interned input containing the token sequences pub fn unified_diff<'a, P: UnifiedDiffPrinter, T: Hash + Eq>( &'a self, printer: &'a P, config: UnifiedDiffConfig, input: &'a InternedInput<T>, ) -> UnifiedDiff<'a, P> { self.unified_diff_with(printer, config, &input.before, &input.after) } /// Creates a unified diff output with explicit token sequences. /// /// # Parameters /// /// * `printer` - A printer implementation that controls how tokens are displayed /// * `config` - Configuration options for the unified diff format /// * `before` - The token sequence from the first file, before changes. /// * `after` - The token sequence from the second file, after changes. pub fn unified_diff_with<'a, P: UnifiedDiffPrinter>( &'a self, printer: &'a P, config: UnifiedDiffConfig, before: &'a [Token], after: &'a [Token], ) -> UnifiedDiff<'a, P> { UnifiedDiff { printer, diff: self, config, before, after, } }} /// A trait for customizing the output format of unified diffs.////// Implementations of this trait control how different parts of a unified diff are displayed,/// including headers, context lines, and changed hunks.pub trait UnifiedDiffPrinter { /// Displays the header for a hunk in the unified diff format. /// /// The header typically includes the line numbers and lengths for both files. /// /// # Parameters /// /// * `f` - The formatter to write to /// * `start_before` - The starting line number in the first file (0-indexed) /// * `start_after` - The starting line number in the second file (0-indexed) /// * `len_before` - The number of lines from the first file in this hunk /// * `len_after` - The number of lines from the second file in this hunk fn display_header( &self, f: impl fmt::Write, start_before: u32, start_after: u32, len_before: u32, len_after: u32, ) -> fmt::Result; /// Displays a context token (an unchanged line) in the unified diff. /// /// # Parameters /// /// * `f` - The formatter to write to /// * `token` - The token to display fn display_context_token(&self, f: impl fmt::Write, token: Token) -> fmt::Result; /// Displays a hunk showing the changes between before and after tokens. /// /// # Parameters /// /// * `f` - The formatter to write to /// * `before` - The tokens from the first file that were removed /// * `after` - The tokens from the second file that were added fn display_hunk(&self, f: impl fmt::Write, before: &[Token], after: &[Token]) -> fmt::Result;} /// Configuration options for unified diff output.////// Controls aspects of the unified diff format such as the number of context lines.#[derive(Debug, Clone, PartialEq, Eq)]pub struct UnifiedDiffConfig { /// The number of unchanged lines to show around each hunk for context. context_len: u32,} impl Default for UnifiedDiffConfig { fn default() -> Self { UnifiedDiffConfig { context_len: 3 } }} impl UnifiedDiffConfig { /// Sets the number of context lines to display around each hunk. /// /// # Parameters /// /// * `len` - The number of unchanged lines to show before and after each change /// /// # Returns /// /// A mutable reference to self for method chaining pub fn context_len(&mut self, len: u32) -> &mut Self { self.context_len = len; self }} /// A helper trait for determining if a token ends with a newline.////// This is used by the unified diff printer to decide whether to add newlines/// when displaying tokens.pub trait EndsWithNewline { /// Returns `true` if the token ends with a newline character. fn ends_with_newline(&self) -> bool;} impl<T: AsRef<[u8]> + ?Sized> EndsWithNewline for T { fn ends_with_newline(&self) -> bool { self.as_ref().ends_with(b"\n") }} /// A basic implementation of [`UnifiedDiffPrinter`] for line-based diffs.////// This printer formats diffs in the standard unified diff format commonly used by/// tools like `git diff` and `diff -u`. It displays removed lines with a `-` prefix/// and added lines with a `+` prefix.pub struct BasicLineDiffPrinter<'a, T: EndsWithNewline + ?Sized + Hash + Eq + Display>( /// A reference to the interner containing the line data. pub &'a Interner<&'a T>,); impl<T: EndsWithNewline + Hash + Eq + Display + ?Sized> UnifiedDiffPrinter for BasicLineDiffPrinter<'_, T> { fn display_header( &self, mut f: impl fmt::Write, start_before: u32, start_after: u32, len_before: u32, len_after: u32, ) -> fmt::Result { writeln!( f, "@@ -{},{} +{},{} @@", start_before + 1, len_before, start_after + 1, len_after ) } fn display_context_token(&self, mut f: impl fmt::Write, token: Token) -> fmt::Result { write!(f, " {}", self.0[token])?; if !&self.0[token].ends_with_newline() { writeln!(f)?; } Ok(()) } fn display_hunk(&self, mut f: impl fmt::Write, before: &[Token], after: &[Token]) -> fmt::Result { if let Some(&last) = before.last() { for &token in before { let token = self.0[token]; write!(f, "-{token}")?; } if !self.0[last].ends_with_newline() { writeln!(f)?; } } if let Some(&last) = after.last() { for &token in after { let token = self.0[token]; write!(f, "+{token}")?; } if !self.0[last].ends_with_newline() { writeln!(f)?; } } Ok(()) }} /// A formatted unified diff that can be displayed as a string.////// This structure is created by [`Diff::unified_diff`] or [`Diff::unified_diff_with`]/// and implements [`Display`] to produce standard unified diff output.pub struct UnifiedDiff<'a, P: UnifiedDiffPrinter> { /// The printer that controls output formatting. printer: &'a P, /// The computed diff to display. diff: &'a Diff, /// Configuration for the unified diff format. config: UnifiedDiffConfig, /// The token sequence from the first file, before changes. before: &'a [Token], /// The token sequence from the second file, after changes. after: &'a [Token],} impl<P: UnifiedDiffPrinter> Display for UnifiedDiff<'_, P> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let first_hunk = self.diff.hunks().next().unwrap_or_default(); let context_len = self.config.context_len.min(1024 * 1024); let mut pos = first_hunk.before.start.saturating_sub(context_len); let mut before_context_start = pos; let mut after_context_start = first_hunk.after.start.saturating_sub(context_len); let mut before_context_len = 0; let mut after_context_len = 0; let mut buffer = String::new(); for hunk in self.diff.hunks() { if hunk.before.start - pos > 2 * context_len { if !buffer.is_empty() { let end = (pos + context_len).min(self.before.len() as u32); self.printer.display_header( &mut *f, before_context_start, after_context_start, before_context_len + end - pos, after_context_len + end - pos, )?; write!(f, "{buffer}")?; for &token in &self.before[pos as usize..end as usize] { self.printer.display_context_token(&mut *f, token)?; } buffer.clear(); } pos = hunk.before.start - context_len; before_context_start = pos; after_context_start = hunk.after.start - context_len; before_context_len = 0; after_context_len = 0; } for &token in &self.before[pos as usize..hunk.before.start as usize] { self.printer.display_context_token(&mut buffer, token)?; } let context_len = hunk.before.start - pos; before_context_len += hunk.before.len() as u32 + context_len; after_context_len += hunk.after.len() as u32 + context_len; self.printer.display_hunk( &mut buffer, &self.before[hunk.before.start as usize..hunk.before.end as usize], &self.after[hunk.after.start as usize..hunk.after.end as usize], )?; pos = hunk.before.end; } if !buffer.is_empty() { let end = (pos + context_len).min(self.before.len() as u32); self.printer.display_header( &mut *f, before_context_start, after_context_start, before_context_len + end - pos, after_context_len + end - pos, )?; write!(f, "{buffer}")?; for &token in &self.before[pos as usize..end as usize] { self.printer.display_context_token(&mut *f, token)?; } buffer.clear(); } Ok(()) }}
// Modified for gitoxide from the upstream imara-diff crate.
// Upstream source: git cat-file -p 32d1e45d3df061e6ccba6db7fdce92db29e345d8:src/unified_diff.rs
use std::fmt::{self, Display};
use std::hash::Hash;
use crate::Diff;
use crate::intern::{InternedInput, Interner, Token};
impl Diff {
/// Creates a unified diff output that can be formatted as a string.
///
/// This is a convenience method that extracts the token sequences from the `input`.
///
/// # Parameters
///
/// * `printer` - A printer implementation that controls how tokens are displayed
/// * `config` - Configuration options for the unified diff format
/// * `input` - The interned input containing the token sequences
pub fn unified_diff<'a, P: UnifiedDiffPrinter, T: Hash + Eq>(
&'a self,
printer: &'a P,
config: UnifiedDiffConfig,
input: &'a InternedInput<T>,
) -> UnifiedDiff<'a, P> {
self.unified_diff_with(printer, config, &input.before, &input.after)
}
/// Creates a unified diff output with explicit token sequences.
///
/// # Parameters
///
/// * `printer` - A printer implementation that controls how tokens are displayed
/// * `config` - Configuration options for the unified diff format
/// * `before` - The token sequence from the first file, before changes.
/// * `after` - The token sequence from the second file, after changes.
pub fn unified_diff_with<'a, P: UnifiedDiffPrinter>(
&'a self,
printer: &'a P,
config: UnifiedDiffConfig,
before: &'a [Token],
after: &'a [Token],
) -> UnifiedDiff<'a, P> {
UnifiedDiff {
printer,
diff: self,
config,
before,
after,
}
}
}
/// A trait for customizing the output format of unified diffs.
///
/// Implementations of this trait control how different parts of a unified diff are displayed,
/// including headers, context lines, and changed hunks.
pub trait UnifiedDiffPrinter {
/// Displays the header for a hunk in the unified diff format.
///
/// The header typically includes the line numbers and lengths for both files.
///
/// # Parameters
///
/// * `f` - The formatter to write to
/// * `start_before` - The starting line number in the first file (0-indexed)
/// * `start_after` - The starting line number in the second file (0-indexed)
/// * `len_before` - The number of lines from the first file in this hunk
/// * `len_after` - The number of lines from the second file in this hunk
fn display_header(
&self,
f: impl fmt::Write,
start_before: u32,
start_after: u32,
len_before: u32,
len_after: u32,
) -> fmt::Result;
/// Displays a context token (an unchanged line) in the unified diff.
///
/// # Parameters
///
/// * `f` - The formatter to write to
/// * `token` - The token to display
fn display_context_token(&self, f: impl fmt::Write, token: Token) -> fmt::Result;
/// Displays a hunk showing the changes between before and after tokens.
///
/// # Parameters
///
/// * `f` - The formatter to write to
/// * `before` - The tokens from the first file that were removed
/// * `after` - The tokens from the second file that were added
fn display_hunk(&self, f: impl fmt::Write, before: &[Token], after: &[Token]) -> fmt::Result;
}
/// Configuration options for unified diff output.
///
/// Controls aspects of the unified diff format such as the number of context lines.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnifiedDiffConfig {
/// The number of unchanged lines to show around each hunk for context.
context_len: u32,
}
impl Default for UnifiedDiffConfig {
fn default() -> Self {
UnifiedDiffConfig { context_len: 3 }
}
}
impl UnifiedDiffConfig {
/// Sets the number of context lines to display around each hunk.
///
/// # Parameters
///
/// * `len` - The number of unchanged lines to show before and after each change
///
/// # Returns
///
/// A mutable reference to self for method chaining
pub fn context_len(&mut self, len: u32) -> &mut Self {
self.context_len = len;
self
}
}
/// A helper trait for determining if a token ends with a newline.
///
/// This is used by the unified diff printer to decide whether to add newlines
/// when displaying tokens.
pub trait EndsWithNewline {
/// Returns `true` if the token ends with a newline character.
fn ends_with_newline(&self) -> bool;
}
impl<T: AsRef<[u8]> + ?Sized> EndsWithNewline for T {
fn ends_with_newline(&self) -> bool {
self.as_ref().ends_with(b"\n")
}
}
/// A basic implementation of [`UnifiedDiffPrinter`] for line-based diffs.
///
/// This printer formats diffs in the standard unified diff format commonly used by
/// tools like `git diff` and `diff -u`. It displays removed lines with a `-` prefix
/// and added lines with a `+` prefix.
pub struct BasicLineDiffPrinter<'a, T: EndsWithNewline + ?Sized + Hash + Eq + Display>(
/// A reference to the interner containing the line data.
pub &'a Interner<&'a T>,
);
impl<T: EndsWithNewline + Hash + Eq + Display + ?Sized> UnifiedDiffPrinter for BasicLineDiffPrinter<'_, T> {
fn display_header(
&self,
mut f: impl fmt::Write,
start_before: u32,
start_after: u32,
len_before: u32,
len_after: u32,
) -> fmt::Result {
writeln!(
f,
"@@ -{},{} +{},{} @@",
start_before + 1,
len_before,
start_after + 1,
len_after
)
}
fn display_context_token(&self, mut f: impl fmt::Write, token: Token) -> fmt::Result {
write!(f, " {}", self.0[token])?;
if !&self.0[token].ends_with_newline() {
writeln!(f)?;
}
Ok(())
}
fn display_hunk(&self, mut f: impl fmt::Write, before: &[Token], after: &[Token]) -> fmt::Result {
if let Some(&last) = before.last() {
for &token in before {
let token = self.0[token];
write!(f, "-{token}")?;
}
if !self.0[last].ends_with_newline() {
writeln!(f)?;
}
}
if let Some(&last) = after.last() {
for &token in after {
let token = self.0[token];
write!(f, "+{token}")?;
}
if !self.0[last].ends_with_newline() {
writeln!(f)?;
}
}
Ok(())
}
}
/// A formatted unified diff that can be displayed as a string.
///
/// This structure is created by [`Diff::unified_diff`] or [`Diff::unified_diff_with`]
/// and implements [`Display`] to produce standard unified diff output.
pub struct UnifiedDiff<'a, P: UnifiedDiffPrinter> {
/// The printer that controls output formatting.
printer: &'a P,
/// The computed diff to display.
diff: &'a Diff,
/// Configuration for the unified diff format.
config: UnifiedDiffConfig,
/// The token sequence from the first file, before changes.
before: &'a [Token],
/// The token sequence from the second file, after changes.
after: &'a [Token],
}
impl<P: UnifiedDiffPrinter> Display for UnifiedDiff<'_, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let first_hunk = self.diff.hunks().next().unwrap_or_default();
let context_len = self.config.context_len.min(1024 * 1024);
let mut pos = first_hunk.before.start.saturating_sub(context_len);
let mut before_context_start = pos;
let mut after_context_start = first_hunk.after.start.saturating_sub(context_len);
let mut before_context_len = 0;
let mut after_context_len = 0;
let mut buffer = String::new();
for hunk in self.diff.hunks() {
if hunk.before.start - pos > 2 * context_len {
if !buffer.is_empty() {
let end = (pos + context_len).min(self.before.len() as u32);
self.printer.display_header(
&mut *f,
before_context_start,
after_context_start,
before_context_len + end - pos,
after_context_len + end - pos,
)?;
write!(f, "{buffer}")?;
for &token in &self.before[pos as usize..end as usize] {
self.printer.display_context_token(&mut *f, token)?;
}
buffer.clear();
}
pos = hunk.before.start - context_len;
before_context_start = pos;
after_context_start = hunk.after.start - context_len;
before_context_len = 0;
after_context_len = 0;
}
for &token in &self.before[pos as usize..hunk.before.start as usize] {
self.printer.display_context_token(&mut buffer, token)?;
}
let context_len = hunk.before.start - pos;
before_context_len += hunk.before.len() as u32 + context_len;
after_context_len += hunk.after.len() as u32 + context_len;
self.printer.display_hunk(
&mut buffer,
&self.before[hunk.before.start as usize..hunk.before.end as usize],
&self.after[hunk.after.start as usize..hunk.after.end as usize],
)?;
pos = hunk.before.end;
}
if !buffer.is_empty() {
let end = (pos + context_len).min(self.before.len() as u32);
self.printer.display_header(
&mut *f,
before_context_start,
after_context_start,
before_context_len + end - pos,
after_context_len + end - pos,
)?;
write!(f, "{buffer}")?;
for &token in &self.before[pos as usize..end as usize] {
self.printer.display_context_token(&mut *f, token)?;
}
buffer.clear();
}
Ok(())
}
}