Diff Checker
Paste two versions of anything and see exactly what changed — down to the individual word. Unified view for reading, split view for reviewing.
Difference
0 unchangedComparison options
Both ignore options change what counts as a match — they don't alter the text shown. The diff still displays your original lines word for word.
Line endings are the usual culprit. The same file saved on Windows and on macOS
differs on every line — CRLF against
LF — so a diff can show the whole file as changed when nothing
you can see has changed. Ignore whitespace collapses those runs, and the two
versions compare as identical again.
Two files, one alignment, and it may be the wrong one
A comparison is not a measurement; it is a story about how one text became another. The algorithm finds a shortest set of edits that turns the first into the second, and shortest is not the same as most readable. Move a function from the top of a file to the bottom and the shortest edit is usually to delete a few lines and add them back elsewhere — which is exactly what a reader wants. Re-indent a block by wrapping it in an extra condition and the shortest edit is every line in the block, because each one really did change.
const total = sum(items, 0); removed
if (ready) { added
const total = sum(items, 0); added
} added
The line is in both files, byte for byte. Indentation
is part of the line, so the comparison sees two strings
that differ and reports the change correctly.
That is the difference between a diff that is right and a diff that is useful, and it is decided before the tool is opened. A change that alters the shape of the code and a change that alters its behaviour belong in separate commits, because a reader working out which of forty marked lines is the behavioural one has a harder job than the edit required. Where a formatter runs on save, the machine has usually already made that decision, and the answer is to format the file once in a commit of its own and then leave it alone.
A diff of generated files is a diff of the generator. If a build writes a bundle, a lockfile or an index, comparing those outputs tells you what the build did and nothing about why. The change worth reviewing is the one in the source that produced them, and the generated files are usually better left out of the comparison entirely. Where they have to be committed, they deserve a commit of their own that says which build produced them.
Ignoring whitespace hides more than it fixes
The setting is a diagnostic, and it is worth being strict about that. Switching it on answers one question — is layout the only difference between these two texts — and leaving it on while reviewing answers a different and more dangerous one, because whitespace is not always insignificant. Indentation is syntax in Python, in YAML and in Makefiles, where a tab and four spaces are different programs or a broken file. Inside a string literal, leading space is part of the value, and a comparison that ignores it has just approved a change to a message, a template or a SQL fragment.
There is a second cost that has nothing to do with correctness. A team that compares with whitespace ignored never notices that half the repository uses tabs and the other half uses spaces, because the tool keeps covering for it — until someone diffs the same files with a different tool and sees the whole file as rewritten. The ignoring is treating the symptom. The cause is settled at the source: a formatting rule the editor applies, an .editorconfig so every editor agrees, and a line-ending rule so one convention lives in the repository rather than whatever the last person to save the file was running.
What a comparison cannot see
Two texts can differ in ways that are invisible on screen, and a tool that reports them as different is right even when the panes look identical. Character encoding is the largest of them: the same accented letter can be one code point or a base letter followed by a combining mark, so the strings are different bytes that render the same, and a comparison against either form fails on the other. Some editors write a byte-order mark at the front of a UTF-8 file; others do not, and it is a character at the start of the first line.
Non-breaking spaces are the other common one, and they are the worst of the set because they look exactly like spaces. A line copied from a web page or a document can carry them into source code where they are a syntax error that no amount of reading will reveal — the diagnostic points at a column and every character in it looks correct. Separately, a file that has no terminating newline on its last line differs from one that has, and the marker saying so sits underneath the last line rather than being a line of its own.
And a comparison of text can only compare text. Where the content is not line-oriented — an image, an archive, a compiled binary — there is no useful diff at all, and a tool reporting the files as different has told you everything it can. Changes to file modes and permissions are in the same position: real, invisible in the content, and better compared as metadata than as text.
Three versions, not two
Two-way comparison is the simple case, and the one that misleads during a merge. When two branches have both moved on, comparing them to each other shows every difference between the two lines of work — including the ones each of them inherited from a common ancestor, which are not changes at all. The comparison that answers the question is against the base, the version both branches started from, because that is what makes a difference a change.
That is why a three-way merge exists and why conflict markers are shaped the way they are: the region between the markers holds both versions of the same hunk, and the base is what tells the tool there is a conflict at all — if one side matches the base and the other does not, only one side changed and there is nothing to resolve. Reading a conflict by comparing the two sides to each other is how a line that one branch deliberately deleted gets quietly restored.
Reference
Unified diff notation, decoded
| Marker | Meaning | Example line |
|---|---|---|
| diff --git a/x b/x | Opens a section for one file and names it on both sides. The a/ and b/ prefixes exist so the old and the new copy can be written side by side without colliding. A rename shows two different paths here. | diff --git a/src/app.js b/src/app.js |
| --- a/x | The old file. Plain diff -u puts a timestamp after the path; git does not. /dev/null in this position means the file is being created rather than changed. | --- a/src/app.js |
| +++ b/x | The new file. This pair of header lines is what a patch tool reads to decide which file to edit. | +++ b/src/app.js |
| @@ -12,7 +12,9 @@ | A hunk header, read as a start line and a line count, first for the old side (the minus pair) and then for the new side (the plus pair). This hunk begins at line 12 of each file, covers 7 lines on the old side and 9 on the new, and those counts take in the context lines as well as the changed ones. A count of one is written with no comma, as in @@ -1 +1 @@. Text after the second @@ names the enclosing function, for orientation only. | @@ -12,7 +12,9 @@ export function parse(input) { |
| - | A line the patch removes. It is present in the old file and gone from the new one. | - const total = sum(items, 0); |
| + | A line the patch adds. It is absent from the old file and present in the new one. | + const total = sum(items); |
| leading space | An unchanged context line, kept so the patch knows where the hunk sits. Only the first character is the marker; the rest of the line is the file's own text, indentation and all. | return total; |
| \ No newline at end of file | A note about the line above it rather than a line of the file: that line has no terminating newline. It follows the - line, the + line, or both, depending on which side is missing it. | \ No newline at end of file |
| Binary files differ | git stopped before writing hunks because the content is not line-oriented. There is no usable text diff for an image, an archive or a compiled artefact, so compare checksums or dimensions instead. | Binary files a/logo.png and b/logo.png differ |
Every line of a unified diff opens with one of these markers, and the marker is the only thing that says whether the text after it exists in the old file, the new file, or both. Once the markers are read, a hunk header is arithmetic rather than guesswork.
The leading space is the part that catches people out. Every line inside a hunk carries exactly one marker character, so a context line is written with a space in front of it, and that space is the marker rather than indentation that got away. A line beginning with nothing would be indistinguishable from a line whose marker was eaten on the way through a mail client or a ticket tracker.
The numbers in a hunk header are counts, not positions.
@@ -12,7 +12,9 @@ starts the hunk at line 12 on both sides,
covers seven lines of the old file and nine of the new, and both totals take in the context
lines along with the changed ones. Three lines of context on each side is the default, and
-U0 strips context out completely — easier to read, impossible to
apply to a file that has moved on since.
To judge whether a change is safe, read the removals before the additions. A hunk that drops three lines and adds three is a rewrite of one region; a hunk that drops thirty and adds two is a deletion that has not admitted it yet. The added lines are what the file will contain afterwards, so any of them you cannot explain is a line you have not reviewed.
Questions
Diffs, answered plainly
Why is the whole line highlighted sometimes?
Word-level highlighting only pairs up removals and additions that sit next to each other in roughly equal numbers. If a block of four lines was deleted and three unrelated lines were added, there's nothing sensible to pair — so the tool marks them line by line instead of inventing a word match.
What's the difference between ignoring whitespace and ignoring case?
Both only affect the comparison, never the display. Ignoring whitespace means "a b" and "a b" count as the same line. Ignoring case means "Timeout" and "timeout" count as the same line. Either way, the text shown is exactly what you pasted.
Can I compare two files rather than pasting?
Open each file in a text editor, select all, and paste into the two panes — that's two keyboard shortcuts. Direct file loading is a reasonable addition and is on the list, but pasting works everywhere including from a remote shell where you can't browse to a file.
Is a diff evidence that nothing else changed?
Only for the text you pasted. Trailing newlines, invisible Unicode characters and encoding differences won't always be visible on screen even when the tool reports them as different. If a change is genuinely critical, compare checksums of the files themselves.
Why does re-indenting a block mark every line as changed?
Because every line really did change. Lines are compared as text, and a line that gained four leading spaces is not the line it was, even when the words on it are identical — so there is nothing for the word-level pass to pair up and the whole block is marked line by line. Switch on Ignore whitespace to confirm that layout is the only difference, then settle the question with a formatter or an editor config so the next commit is quiet.
The whole file shows as changed but I only edited one word. Why?
Line endings, almost every time. A file saved on Windows ends each line with CRLF; the same file saved on macOS or Linux ends its lines with LF. Every line then differs at its end while nothing visible has moved, so the file reads as rewritten from top to bottom. Turn on Ignore whitespace to check that this is the only difference, then fix it at the source — a .gitattributes rule such as * text=auto eol=lf keeps one ending in the repository and the next commit is clean.
Can I save the diff and apply it as a patch?
Not the text this tool copies. It gives you the -, + and space-prefixed lines without the ---, +++ and @@ headers that git apply and patch read to work out where each change belongs, so both will refuse it. Run git diff yourself for a file that will apply. Even then, a patch lands only when the target file still matches the context lines around each hunk, and git apply --check reports that without writing anything.