Skip to main content

diffy/patch/
format.rs

1use alloc::borrow::ToOwned;
2use core::fmt::Display;
3use core::fmt::Formatter;
4use core::fmt::Result;
5
6#[cfg(feature = "std")]
7use std::io;
8
9use super::Hunk;
10use super::Line;
11use super::NO_NEWLINE_AT_EOF;
12use super::Patch;
13#[cfg(feature = "color")]
14use super::style;
15
16/// Formats patches for display or writing into byte streams.
17///
18/// # Examples
19///
20/// ```
21/// use diffy::PatchFormatter;
22/// use diffy::create_patch;
23///
24/// let patch = create_patch("alpha\nbeta\n", "ALPHA\nbeta\n");
25/// let formatter = PatchFormatter::new().missing_newline_message(false);
26/// let mut output = Vec::new();
27///
28/// formatter.write_patch_into(&patch, &mut output).unwrap();
29///
30/// assert_eq!(
31///     String::from_utf8(output).unwrap(),
32///     "\
33/// --- original
34/// +++ modified
35/// @@ -1,2 +1,2 @@
36/// -alpha
37/// +ALPHA
38///  beta
39/// ",
40/// );
41/// ```
42#[derive(Debug)]
43pub struct PatchFormatter {
44    #[cfg(feature = "color")]
45    with_color: bool,
46    with_missing_newline_message: bool,
47    suppress_blank_empty: bool,
48}
49
50impl PatchFormatter {
51    /// Construct a new formatter
52    pub fn new() -> Self {
53        Self {
54            #[cfg(feature = "color")]
55            with_color: false,
56            with_missing_newline_message: true,
57
58            // TODO the default in git-diff and GNU diff is to have this set to false, on the next
59            // semver breaking release we should contemplate switching this to be false by default
60            suppress_blank_empty: true,
61        }
62    }
63
64    /// Enable formatting a patch with color
65    #[cfg(feature = "color")]
66    #[cfg_attr(docsrs, doc(cfg(feature = "color")))]
67    pub fn with_color(mut self) -> Self {
68        self.with_color = true;
69        self
70    }
71
72    /// Sets whether to format a patch with a "No newline at end of file" message.
73    ///
74    /// Default is `true`.
75    ///
76    /// Note: If this is disabled by setting to `false`, formatted patches will no longer contain
77    /// sufficient information to determine if a file ended with a newline character (`\n`) or not
78    /// and the patch will be formatted as if both the original and modified files ended with a
79    /// newline character (`\n`).
80    pub fn missing_newline_message(mut self, enable: bool) -> Self {
81        self.with_missing_newline_message = enable;
82        self
83    }
84
85    /// Sets whether to suppress printing of a space before empty lines.
86    ///
87    /// Defaults to `true`.
88    ///
89    /// For more information you can refer to the [Omitting trailing blanks] manual page of GNU
90    /// diff or the [diff.suppressBlankEmpty] config for `git-diff`.
91    ///
92    /// [Omitting trailing blanks]: https://www.gnu.org/software/diffutils/manual/html_node/Trailing-Blanks.html
93    /// [diff.suppressBlankEmpty]: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt-codediffsuppressBlankEmptycode
94    pub fn suppress_blank_empty(mut self, enable: bool) -> Self {
95        self.suppress_blank_empty = enable;
96        self
97    }
98
99    /// Returns a `Display` impl which can be used to print a Patch
100    pub fn fmt_patch<'a>(&'a self, patch: &'a Patch<'a, str>) -> impl Display + 'a {
101        PatchDisplay { f: self, patch }
102    }
103
104    /// Writes a formatted patch into a writer.
105    ///
106    /// This is the byte-oriented equivalent of [`fmt_patch`](Self::fmt_patch)
107    /// for callers that want to stream the formatted patch into an `io::Write`
108    /// sink instead of going through `Display`.
109    #[cfg(feature = "std")]
110    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
111    pub fn write_patch_into<T: ToOwned + AsRef<[u8]> + ?Sized, W: io::Write>(
112        &self,
113        patch: &Patch<'_, T>,
114        w: W,
115    ) -> io::Result<()> {
116        PatchDisplay { f: self, patch }.write_into(w)
117    }
118
119    fn fmt_hunk<'a>(&'a self, hunk: &'a Hunk<'a, str>) -> impl Display + 'a {
120        HunkDisplay { f: self, hunk }
121    }
122
123    #[cfg(feature = "std")]
124    fn write_hunk_into<T: AsRef<[u8]> + ?Sized, W: io::Write>(
125        &self,
126        hunk: &Hunk<'_, T>,
127        w: W,
128    ) -> io::Result<()> {
129        HunkDisplay { f: self, hunk }.write_into(w)
130    }
131
132    fn fmt_line<'a>(&'a self, line: &'a Line<'a, str>) -> impl Display + 'a {
133        LineDisplay { f: self, line }
134    }
135
136    #[cfg(feature = "std")]
137    fn write_line_into<T: AsRef<[u8]> + ?Sized, W: io::Write>(
138        &self,
139        line: &Line<'_, T>,
140        w: W,
141    ) -> io::Result<()> {
142        LineDisplay { f: self, line }.write_into(w)
143    }
144}
145
146impl Default for PatchFormatter {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152struct PatchDisplay<'a, T: ToOwned + ?Sized> {
153    f: &'a PatchFormatter,
154    patch: &'a Patch<'a, T>,
155}
156
157#[cfg(feature = "std")]
158impl<T: ToOwned + AsRef<[u8]> + ?Sized> PatchDisplay<'_, T> {
159    fn write_into<W: io::Write>(&self, mut w: W) -> io::Result<()> {
160        if self.patch.original.is_some() || self.patch.modified.is_some() {
161            #[cfg(feature = "color")]
162            if self.f.with_color {
163                write!(w, "{}", style::PATCH_HEADER)?;
164            }
165            if let Some(original) = &self.patch.original {
166                write!(w, "--- ")?;
167                original.write_into(&mut w)?;
168                writeln!(w)?;
169            }
170            if let Some(modified) = &self.patch.modified {
171                write!(w, "+++ ")?;
172                modified.write_into(&mut w)?;
173                writeln!(w)?;
174            }
175            #[cfg(feature = "color")]
176            if self.f.with_color {
177                write!(w, "{:#}", style::PATCH_HEADER)?;
178            }
179        }
180
181        for hunk in &self.patch.hunks {
182            self.f.write_hunk_into(hunk, &mut w)?;
183        }
184
185        Ok(())
186    }
187}
188
189impl Display for PatchDisplay<'_, str> {
190    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
191        if self.patch.original.is_some() || self.patch.modified.is_some() {
192            #[cfg(feature = "color")]
193            if self.f.with_color {
194                write!(f, "{}", style::PATCH_HEADER)?;
195            }
196            if let Some(original) = &self.patch.original {
197                writeln!(f, "--- {original}")?;
198            }
199            if let Some(modified) = &self.patch.modified {
200                writeln!(f, "+++ {modified}")?;
201            }
202            #[cfg(feature = "color")]
203            if self.f.with_color {
204                write!(f, "{:#}", style::PATCH_HEADER)?;
205            }
206        }
207
208        for hunk in &self.patch.hunks {
209            write!(f, "{}", self.f.fmt_hunk(hunk))?;
210        }
211
212        Ok(())
213    }
214}
215
216struct HunkDisplay<'a, T: ?Sized> {
217    f: &'a PatchFormatter,
218    hunk: &'a Hunk<'a, T>,
219}
220
221#[cfg(feature = "std")]
222impl<T: AsRef<[u8]> + ?Sized> HunkDisplay<'_, T> {
223    fn write_into<W: io::Write>(&self, mut w: W) -> io::Result<()> {
224        #[cfg(feature = "color")]
225        if self.f.with_color {
226            write!(w, "{}", style::HUNK_HEADER)?;
227        }
228        write!(w, "@@ -{} +{} @@", self.hunk.old_range, self.hunk.new_range)?;
229        #[cfg(feature = "color")]
230        if self.f.with_color {
231            write!(w, "{:#}", style::HUNK_HEADER)?;
232        }
233
234        if let Some(ctx) = self.hunk.function_context {
235            write!(w, " ")?;
236            #[cfg(feature = "color")]
237            if self.f.with_color {
238                write!(w, "{}", style::FUNCTION_CONTEXT)?;
239            }
240            write!(w, " ")?;
241            w.write_all(ctx.as_ref())?;
242            #[cfg(feature = "color")]
243            if self.f.with_color {
244                write!(w, "{:#}", style::FUNCTION_CONTEXT)?;
245            }
246        }
247        writeln!(w)?;
248
249        for line in &self.hunk.lines {
250            self.f.write_line_into(line, &mut w)?;
251        }
252
253        Ok(())
254    }
255}
256
257impl Display for HunkDisplay<'_, str> {
258    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
259        #[cfg(feature = "color")]
260        if self.f.with_color {
261            write!(f, "{}", style::HUNK_HEADER)?;
262        }
263        write!(f, "@@ -{} +{} @@", self.hunk.old_range, self.hunk.new_range)?;
264        #[cfg(feature = "color")]
265        if self.f.with_color {
266            write!(f, "{:#}", style::HUNK_HEADER)?;
267        }
268
269        if let Some(ctx) = self.hunk.function_context {
270            write!(f, " ")?;
271            #[cfg(feature = "color")]
272            if self.f.with_color {
273                write!(f, "{}", style::FUNCTION_CONTEXT)?;
274            }
275            write!(f, " {ctx}")?;
276            #[cfg(feature = "color")]
277            if self.f.with_color {
278                write!(f, "{:#}", style::FUNCTION_CONTEXT)?;
279            }
280        }
281        writeln!(f)?;
282
283        for line in &self.hunk.lines {
284            write!(f, "{}", self.f.fmt_line(line))?;
285        }
286
287        Ok(())
288    }
289}
290
291struct LineDisplay<'a, T: ?Sized> {
292    f: &'a PatchFormatter,
293    line: &'a Line<'a, T>,
294}
295
296#[cfg(feature = "std")]
297impl<T: AsRef<[u8]> + ?Sized> LineDisplay<'_, T> {
298    fn write_into<W: io::Write>(&self, mut w: W) -> io::Result<()> {
299        #[cfg(feature = "color")]
300        let (sign, line, style) = match self.line {
301            Line::Context(line) => (' ', line.as_ref(), style::CONTEXT),
302            Line::Delete(line) => ('-', line.as_ref(), style::DELETE),
303            Line::Insert(line) => ('+', line.as_ref(), style::INSERT),
304        };
305        #[cfg(not(feature = "color"))]
306        let (sign, line) = match self.line {
307            Line::Context(line) => (' ', line.as_ref()),
308            Line::Delete(line) => ('-', line.as_ref()),
309            Line::Insert(line) => ('+', line.as_ref()),
310        };
311
312        #[cfg(feature = "color")]
313        if self.f.with_color {
314            write!(w, "{style}")?;
315        }
316
317        if self.f.suppress_blank_empty && sign == ' ' && line == b"\n" {
318            w.write_all(line)?;
319        } else {
320            write!(w, "{sign}")?;
321            w.write_all(line)?;
322        }
323
324        #[cfg(feature = "color")]
325        if self.f.with_color {
326            write!(w, "{style:#}")?;
327        }
328
329        if !line.ends_with(b"\n") {
330            writeln!(w)?;
331            if self.f.with_missing_newline_message {
332                writeln!(w, "{NO_NEWLINE_AT_EOF}")?;
333            }
334        }
335
336        Ok(())
337    }
338}
339
340impl Display for LineDisplay<'_, str> {
341    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
342        #[cfg(feature = "color")]
343        let (sign, line, style) = match self.line {
344            Line::Context(line) => (' ', line, style::CONTEXT),
345            Line::Delete(line) => ('-', line, style::DELETE),
346            Line::Insert(line) => ('+', line, style::INSERT),
347        };
348        #[cfg(not(feature = "color"))]
349        let (sign, line) = match self.line {
350            Line::Context(line) => (' ', line),
351            Line::Delete(line) => ('-', line),
352            Line::Insert(line) => ('+', line),
353        };
354
355        #[cfg(feature = "color")]
356        if self.f.with_color {
357            write!(f, "{style}")?;
358        }
359
360        if self.f.suppress_blank_empty && sign == ' ' && *line == "\n" {
361            write!(f, "{line}")?;
362        } else {
363            write!(f, "{sign}{line}")?;
364        }
365
366        #[cfg(feature = "color")]
367        if self.f.with_color {
368            write!(f, "{style:#}")?;
369        }
370
371        if !line.ends_with('\n') {
372            writeln!(f)?;
373            if self.f.with_missing_newline_message {
374                writeln!(f, "{NO_NEWLINE_AT_EOF}")?;
375            }
376        }
377
378        Ok(())
379    }
380}