diffofdiffs compares two versions of a patch side by side. Its report shows both what each patch changed and how the surrounding source differs after those changes. This helps you review a backport, a revised fix, or a rebase without losing track of the original additions and removals.
See the README for installation and a quick introduction. The
examples using samples/ below run from this repository's root after building
diffofdiffs. Run diffofdiffs --help for the complete option list.
- Choose the inputs
- Read the report
- Adjust the context
- Read highlights and whitespace
- Use terminal output
- Use a standalone HTML report
- Understand the limits
Each of the two inputs represents one patch, possibly changing several files. You can supply patch files directly or ask diffofdiffs to read commits from a local Git repository. The first input appears on the left, and the second appears on the right.
To compare two patch files:
diffofdiffs first.patch second.patchPatch mode accepts unified diffs from git diff or diff -u, including Git
metadata for renames, mode changes, and binary changes. A single
git format-patch email can include its preamble. Context diffs (diff -c)
and combined diffs (git diff --cc) aren't supported.
Supply one patch per input. The reader doesn't detect a concatenated email
series as separate commits; split it into individual patches before comparing
them. Either input can be - to read a patch from standard input, but only
one of them can use it at a time.
Patch mode knows only the source lines quoted in the patches. Code omitted between hunks or beyond their edges remains unknown. A larger context setting cannot recover those missing lines.
To compare commits using the complete files before and after each change:
diffofdiffs commit1 commit2Run this from the repository's root. When both path lookups report that the
operands are missing, diffofdiffs assumes --git-tree=.. Standard input
(-), existing paths, and other lookup errors, such as denied access, keep
patch mode. Use --git-tree=DIR to select the repository explicitly. With
this option, the two arguments are treated as Git revisions, even if their
names match local files:
diffofdiffs --git-tree=/path/to/repository commit1 commit2In a bare repository, HEAD is an actual file. Pass --git-tree=. to compare
it as a revision.
Misspelling both patch filenames also selects tree mode. If the repository or a revision cannot be found, the error names the current directory's Git repository and asks whether you meant to compare patch files. In that case, check that both paths are correct.
Tree mode reads Git objects without checking out revisions or changing the
index. The repository can be a working tree or a bare repository. Use commit
IDs, unambiguous abbreviated IDs, branch names, tags, or HEAD. Revision
expressions such as HEAD~2 aren't supported; resolve them to commit IDs with
Git first.
Each revision is compared with its first parent; a root commit uses an empty
tree. To select another parent, use BASE..TIP as an operand, where BASE
must be a parent of TIP. This notation selects one commit's change against
that parent, not a series of commits.
Shallow repositories are supported. Comparisons need the selected commits,
their chosen parents, and the trees and blobs containing their files; older
history is not needed. Fetching each input commit with git fetch --depth=2
includes its parents. If a required object is missing, diffofdiffs reports an
error.
Tree mode handles regular text files, symbolic links, and submodule entries.
It stops with an error on changed files containing NUL bytes, paths containing
tabs or newlines, and unsupported entry modes. To compare binary changes, use
patch mode with patches produced by git diff --binary.
The arguments sample provides a small comparison to follow:
./diffofdiffs --backport-labels \
samples/arguments/backport.patch samples/arguments/upstream.patchBoth patches add sequence-count calls around an assignment in record.c.
The upstream calls use record->state.seq, while the backport uses
record->seq. Both add record->generation++;, and the upstream source also
contains a guard that was already present before its patch.
The report separates these facts into DELTA DIFFERENCES, for the original edits, and CONTEXT DIFFERENCES, for the nearby resulting source. You can also follow the sample in its HTML report.
A nonempty text report begins with centered input labels between horizontal
borders. The labels identify each input as a patch or commit and appear only
at the top. --backport-labels names the left side Backport and the right
side Upstream, as in the sample.
Below the labels, a note explains which source is available: complete files before and after each commit in tree mode, or only the lines quoted in the patches in patch mode. Section banners then introduce the comparisons.
A thick horizontal line starts each file, and thinner lines separate groups
of changes. A group's @@ label gives the available function name or original
hunk comment; a bare @@ means neither is available. These labels appear in
cyan when terminal color is enabled.
A blank source line has a line number. An alignment gap has neither source nor a number, and HTML marks these gaps with hatching by default. File creation, deletion, rename, copy, and mode differences also appear when relevant. Patch mode can also report binary differences.
Only sections with output appear in a text report. Identical inputs produce no text output. A report describes the comparison; it isn't a patch to apply to either input.
This section keeps each patch's original additions and removals, with its
own line numbers: old means before that patch, and new means after it.
In text output, the sign and numbers tell you what happened to a line:
| Sign | Numbers shown | Meaning |
|---|---|---|
+ |
new |
An addition without a matching addition in the other patch |
- |
old |
A removal without a matching removal in the other patch |
| Sign-less | old and new |
Source retained by this patch |
| Sign-less | new only |
An addition both patches make |
| Sign-less | old only |
A removal both patches make |
With terminal color enabled, differing additions have green text, and differing removals have red text. Sign-less lines use the same text color as unchanged source. A differing blank line still has a colored sign and line number.
Shared edits appear when needed as context around differing edits. In HTML,
differing additions and removals have green and red row backgrounds. Shared
edits keep their original + or - signs and have yellow shading by default.
An edit can be shared even when its counterpart appears at another position.
In the sample, both write_seqcount_begin() calls have a +: each patch
adds a call, but their arguments differ. Their new line numbers are 3 on the
left and 5 on the right. The write_seqcount_end() calls likewise differ.
The shared record->generation++; addition has no sign in the text report,
only new line numbers 5 and 7. The retained record->value = value; has both
old and new numbers: 3 and 4 on the left, and 5 and 6 on the right. These
numbers distinguish a shared addition from code neither patch changed.
This section compares the resulting source near the edits. Only result line
numbers appear, under new. Here - marks left-only source, and + marks
right-only source. With terminal color enabled, left differences have red
text, and right differences have green text. HTML uses the same red and green
assignment for row backgrounds.
In the sample, the upstream if (!record->active) and return; lines face
an empty backport cell. They have + signs in this section because they exist
only in the upstream result. They were sign-less retained source in the delta
section: the upstream patch didn't add them.
The differing sequence-count arguments also appear in the resulting source,
so the context section can show those calls with - on the left and +
on the right. The signs now describe the difference between the two results,
rather than each patch's original edit.
Matching final code doesn't mean two patches made the same change. For
example, save this patch as first.patch:
--- a/task.c
+++ b/task.c
@@ -1,2 +1,2 @@
-legacy_check();
+check();
run();Save the following patch as second.patch:
--- a/task.c
+++ b/task.c
@@ -1,3 +1,2 @@
check();
-obsolete_step();
run();Compare them with diffofdiffs first.patch second.patch. Both leave exactly
the same two lines, check(); followed by run();. However, only the first
patch adds check();; the second already had it. The patches also remove
different calls: legacy_check(); and obsolete_step();.
The delta section shows that addition opposite retained source, along with both differing removals. There is no context section for these inputs because the resulting source agrees. Comparing only the final files would hide all three edit differences.
Like GNU diff's unified-context option, -U N (or --unified=N) requests
N context lines before and after differences. The default is three. In the
delta section, N counts each patch's original rows, including removals and
shared edits; in the context section, it counts result lines. Alignment gaps
don't count. Nearby excerpts merge when their margins meet, and file
boundaries or missing patch quotations can leave fewer lines.
For a compact version of the arguments sample, omit surrounding matching
lines with -U0:
./diffofdiffs -U0 --backport-labels \
samples/arguments/backport.patch samples/arguments/upstream.patchThe differing calls remain in the delta section, while the shared generation increment drops out. The upstream guard still appears in the context section: it is a difference within the quoted source, not matching context.
The context section is a bounded excerpt. A differing stretch must include retained source near an edit; differences involving only added lines are already covered by delta. Here, "near" includes the original quoted result lines and N result lines around each edit. Each initial selection then gets up to N source lines on either side; an empty side gets the same margin around its corresponding gap. Newly exposed differences don't trigger another expansion, so an excerpt can stop inside a longer source difference.
In tree mode, the complete files are compared before excerpts are selected.
Each commit's derived patch initially quotes three context lines,
independently of -U. In patch mode, enough quoted source must be available to
establish a difference. Unquoted gaps and quotation edges can leave context
differences unreported, including a final-newline mismatch at an uncertain
file boundary. Increasing -U cannot recover unknown source. Neither mode
prints every difference between two releases.
Line colors describe whole rows. Inline highlights draw attention to changed text within an aligned pair:
--highlight=words, the default, emphasizes changed words, punctuation, and whitespace.--highlight=charactersnarrows the emphasis to changed characters within those word ranges.--highlight=nonedisables inline emphasis while keeping line colors.
For example, when comparing check_old(sp); with check_new(sp);, word mode
emphasizes the entire names check_old and check_new. Character mode
emphasizes only old and new, leaving check_ unmarked in both names.
Inline red marks changed left-side text, and inline green marks changed right-side text. This is independent of the original edit signs: both sequence-count calls in the sample are additions, yet their differing arguments receive left-red and right-green emphasis.
In terminal output, highlighting requires color. Changed text uses bright red or green. Changed whitespace receives cyan emphasis only when it touches an unchanged space or tab on the same source line; its displayed spaces are underlined. A separate run of changed spaces stays unmarked so it doesn't look like underscores. Word highlighting can mark a whole whitespace run as changed, leaving it without an unchanged neighbor; character highlighting can isolate the extra spaces within that run. Wrapping doesn't change the adjacency decision, and code characters aren't underlined or made bold.
HTML uses stronger red and green backgrounds behind changed text. Ordinary
spaces stay inside the surrounding highlight. Separate space blocks appear
when spaces and tabs share a whitespace run, or when corresponding whitespace
uses tabs on one side and spaces on the other. Each highlighted tab fills its
tab stop. --tab-width=N sets the tab stops, with eight columns by default.
Hover over a separate whitespace block to see whether it marks a space or a
tab. These blocks decorate the existing source; they don't replace tabs or
spaces with symbols.
An HTML "Indentation only" label means the nonblank text agrees exactly after removing leading spaces and tabs. Internal whitespace, trailing whitespace, and the final-newline state must also agree. Leading tabs and spaces can differ even when they occupy the same display width: a tab and eight spaces can receive this label while remaining different source bytes.
A No newline at end of file note follows a source line whose final line
terminator is missing. It doesn't mean an extra blank line should be added.
Lines that differ only in this final-newline state aren't equal.
On a terminal, source columns can grow to fit the available width. Use
--max-column-width=N to cap each source column at N display columns,
excluding the signs, numbers, and separators. Redirected output uses a
default cap of 100, as does a terminal whose width cannot be read. Both
columns use the same width throughout the report, measured from the source;
longer paths, labels, and notes wrap without widening it.
TTY output expands source tabs to spaces, using tab stops measured from each
source line's start. --tab-width=N accepts a positive integer and defaults
to eight; --tab-width=4, for example, uses four-column stops. This gives
identical source the same indentation in both columns, regardless of the
signs and line numbers before it.
Source columns have a minimum width of two. Redirected text keeps literal tabs and needs at least eight columns when tabs occur, even if a smaller cap is requested. A very narrow terminal may be too small for the two-column layout.
Long source lines wrap without repeating their signs or line numbers on
continuation lines. UTF-8 characters remain intact, and other nonprinting
bytes are escaped as \xNN. Both text and HTML output show invisible Unicode
format characters, direction overrides, and line separators as byte escapes
so they cannot silently alter the report's appearance.
Borders use box-drawing characters in an effective UTF-8 locale and ASCII
otherwise; in a single-byte locale, the program first tries C.UTF-8.
Redirected text keeps literal source tabs. Its layout assumes eight-column
terminal stops measured from the whole output row, so gutters and column
positions affect their displayed width if you later print that file with
cat. --tab-width affects TTY rendering and HTML display; it doesn't expand
tabs in saved text. Trailing source whitespace is preserved, and layout
padding doesn't add trailing spaces.
Use -o FILE or --output=FILE to save the report. Output otherwise goes to
standard output, and -o - explicitly selects it. Terminal color is automatic
and is disabled when output is redirected. To retain color in saved output,
use --color=always; use --color=never for plain text.
Tab expansion and terminal width detection follow the report's actual
destination. Saving with -o report.txt keeps literal tabs even when standard
output is a terminal; writing to a terminal device with -o uses the terminal
layout.
TTY output contains expanded spaces, and terminal selection cannot recover the original tabs. Even when printing a saved file containing tabs, terminals such as Konsole can copy the resulting screen cells as spaces. To retain tabs, read saved text output from a file, select source in HTML, or use the HTML report's Copy report and Save text controls. Saving a report includes its numbers and layout; it doesn't produce a replacement source file.
Generate an HTML version of the same sample:
./diffofdiffs --html --theme=light --backport-labels -o report.html \
samples/arguments/backport.patch samples/arguments/upstream.patchOpen report.html directly in a browser. This single file embeds its styles,
scripts, and viewing controls, so it needs no server or network connection.
The HTML and text reports use the same paired source rows. For Git inputs,
HTML also shows each result commit's subject beneath its identifier and in
the browser tab title.
Input text, filenames, and commit subjects are escaped so the browser displays them as text, even if they were written to alter the page or run code. Every report also includes a Content Security Policy that restricts scripts to its own viewer and blocks network access. These safeguards also help when you use diffofdiffs in your own CI pipeline to generate HTML reports as build artifacts for reviewers.
The seven controls under Viewing options change the presentation:
| Control | Choices or effect |
|---|---|
| Layout | Review or Console |
| Theme | Dark or Light |
| Highlight | Words, Exact characters, or Off |
| Yellow shared edits | Toggle yellow shading on shared additions and removals |
| Mark gaps | Toggle hatching where no source line is present |
| Wrap lines | Toggle wrapping of long source lines |
| Text size | Small, Medium, or Large |
Review uses rounded panels and a reading layout; Console uses the full page
width, square corners, and monospace headings. Reports start in Review with
the Dark theme and Words highlighting. Use --theme=dark|light and
--highlight to choose the initial theme and highlighting, or change them
in the viewer. Turning off shading or hatching doesn't change which edits
are shared or which cells have no source.
Use Show to select both report sections or just delta or context differences. File jumps to a file, and Previous and Next move between difference groups. Tooltips explain line numbers, gaps, shared edits, and whitespace; the legends explain edit signs.
Copy report and Save text include the complete plain-text report, including sections hidden by Show. Source selection and these controls preserve literal tabs and spaces. All source remains visible without JavaScript; the viewing controls require it.
diffofdiffs compares source and original edits without interpreting what the code does. Text similarity and indentation can help align rows for reading, but they don't make unequal source into a shared edit. Highlighting decorates the selected pairs; it doesn't establish that two changes mean the same thing. Unrelated pairs and very long lines can appear without inline highlights.
Resource limits apply to every comparison to help keep unusually large or deeply nested inputs from exhausting memory or the stack. Each version of a file is limited to 64 MiB, and process limits constrain memory growth. Tree mode also stops directory walks at 256 levels to protect the stack. Exceeding a limit returns an error; it never means that the inputs match. These safeguards are part of diffofdiffs' design for safe use in CI pipelines that generate HTML artifacts from untrusted patches or repositories.
Files with different basenames aren't paired, though differing leading directories can still match. Identical edits can be shared across separate files, so a change of path alone doesn't guarantee a report. When the edits also differ, unpaired files appear in separate sections with an empty side.
Repeated or reordered source and missing patch quotations can leave identical edits unpaired and visible. Ambiguous overlapping or conflicting quotations suppress a file's context comparison; its original edits remain available for delta comparison. Tree mode supplies more source evidence, but complete source doesn't remove every ambiguity.
A successful comparison returns exit status 0 even when it finds differences, so status alone isn't a changed-or-unchanged test. When no differences are found, both text and HTML output are empty. Check for both empty output and status 0 to distinguish that result from an error, which returns a nonzero status.
Use the report to inspect the edits and their surroundings. Deciding whether a backport is correct still requires reviewing what those changes do.