Diffing is the process of comparing two pieces of text and showing exactly what changed between them. It's one of the most important tools in software development — git diff uses it, code review tools use it, and documentation systems use it. Understanding how diffs work makes you a better developer.
What a diff shows
A diff takes two inputs — usually called A (the original) and B (the modified version) — and produces output that marks every line as:
- Unchanged — present in both A and B
- Removed — only in A (deleted)
- Added — only in B (inserted)
Unified diff format
The most common format is unified diff, which is what git diff produces:
--- a/config.txt
+++ b/config.txt
@@ -1,6 +1,7 @@
server {
- listen 80;
+ listen 443 ssl;
+ ssl_certificate /etc/ssl/cert.pem;
server_name example.com;
- root /var/www/html;
+ root /var/www/public;
}
Reading the symbols:
| Symbol | Meaning |
|---|---|
--- |
Path to file A (original) |
+++ |
Path to file B (modified) |
@@ |
Hunk header — shows which lines from each file |
- |
Line removed from A |
+ |
Line added in B |
(space) |
Unchanged context line |
The @@ -1,6 +1,7 @@ header means: "starting at line 1, show 6 lines from A; starting at line 1, show 7 lines from B." The difference (6 vs 7) reflects the net change in line count.
The diff algorithm
The classic algorithm is Longest Common Subsequence (LCS), developed by Myers in 1986 and still used in most diff tools today. It works by finding the largest set of lines that appear in the same order in both files, then marking everything else as added or removed.
In practice, diff tools optimize for:
- Minimal edit distance — fewest additions and deletions
- Semantic quality — grouping related changes into hunks
- Performance — handling large files in milliseconds
Modern tools like git use the Myers algorithm by default, with options for patience diff (better for code) and histogram diff (better for copied code).
Diffing in JavaScript
The most popular library is diff by Ariya Hidayat:
import { diffLines, createPatch } from "diff";
const a = `line one\nline two\nline three\n`;
const b = `line one\nline 2\nline three\nline four\n`;
// Get structured diff
const changes = diffLines(a, b);
for (const part of changes) {
const prefix = part.added ? "+" : part.removed ? "-" : " ";
process.stdout.write(prefix + part.value);
}
// Output:
// line one
// -line two
// +line 2
// line three
// +line four
// Or generate a unified patch string
const patch = createPatch("file.txt", a, b);
console.log(patch);
For word-level diffs (useful for prose):
import { diffWords } from "diff";
const a = "The quick brown fox";
const b = "The slow brown fox jumps";
diffWords(a, b).forEach(part => {
if (part.added) process.stdout.write(`[+${part.value}]`);
else if (part.removed) process.stdout.write(`[-${part.value}]`);
else process.stdout.write(part.value);
});
// The [-quick][+slow] brown fox[+ jumps]
Diffing in Python
Python's standard library includes difflib — no install needed:
import difflib
a = "line one\nline two\nline three\n"
b = "line one\nline 2\nline three\nline four\n"
# Unified diff (same format as git diff)
diff = difflib.unified_diff(
a.splitlines(keepends=True),
b.splitlines(keepends=True),
fromfile="original.txt",
tofile="modified.txt",
)
print("".join(diff))
# HTML diff (for displaying in a browser)
html_diff = difflib.HtmlDiff()
html = html_diff.make_file(
a.splitlines(),
b.splitlines(),
fromdesc="Original",
todesc="Modified",
)
with open("diff.html", "w") as f:
f.write(html)
# Similarity ratio (0.0 = totally different, 1.0 = identical)
ratio = difflib.SequenceMatcher(None, a, b).ratio()
print(f"Similarity: {ratio:.0%}") # Similarity: 74%
Comparing two files on disk:
import difflib, pathlib, sys
file_a = pathlib.Path(sys.argv[1]).read_text()
file_b = pathlib.Path(sys.argv[2]).read_text()
diff = difflib.unified_diff(
file_a.splitlines(keepends=True),
file_b.splitlines(keepends=True),
fromfile=sys.argv[1],
tofile=sys.argv[2],
)
sys.stdout.writelines(diff)
Diffing in Go
The go-diff library implements the Myers algorithm:
package main
import (
"fmt"
"github.com/sergi/go-diff/diffmatchpatch"
)
func main() {
dmp := diffmatchpatch.New()
a := "line one\nline two\nline three\n"
b := "line one\nline 2\nline three\nline four\n"
// Character-level diff
diffs := dmp.DiffMain(a, b, false)
fmt.Println(dmp.DiffPrettyText(diffs))
// Line-level diff (better for most files)
aRunes, bRunes, lines := dmp.DiffLinesToRunes(a, b)
diffs = dmp.DiffMainRunes(aRunes, bRunes, false)
diffs = dmp.DiffCharsToLines(diffs, lines)
for _, d := range diffs {
switch d.Type {
case diffmatchpatch.DiffInsert:
fmt.Printf("+ %s", d.Text)
case diffmatchpatch.DiffDelete:
fmt.Printf("- %s", d.Text)
case diffmatchpatch.DiffEqual:
fmt.Printf(" %s", d.Text)
}
}
}
For a pure stdlib approach (simpler, line-level only):
package main
import (
"bufio"
"fmt"
"strings"
)
func splitLines(s string) []string {
var sc = bufio.NewScanner(strings.NewReader(s))
var lines []string
for sc.Scan() {
lines = append(lines, sc.Text())
}
return lines
}
// Minimal LCS-based diff — good for small files
func diff(a, b []string) {
// Build LCS table
m, n := len(a), len(b)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := m - 1; i >= 0; i-- {
for j := n - 1; j >= 0; j-- {
if a[i] == b[j] {
dp[i][j] = dp[i+1][j+1] + 1
} else if dp[i+1][j] > dp[i][j+1] {
dp[i][j] = dp[i+1][j]
} else {
dp[i][j] = dp[i][j+1]
}
}
}
// Trace back
i, j := 0, 0
for i < m && j < n {
if a[i] == b[j] {
fmt.Printf(" %s\n", a[i]); i++; j++
} else if dp[i+1][j] >= dp[i][j+1] {
fmt.Printf("- %s\n", a[i]); i++
} else {
fmt.Printf("+ %s\n", b[j]); j++
}
}
for ; i < m; i++ { fmt.Printf("- %s\n", a[i]) }
for ; j < n; j++ { fmt.Printf("+ %s\n", b[j]) }
}
Diffing in PHP
// PHP has no built-in diff, but similar_text() gives similarity
$a = "line one\nline two\nline three\n";
$b = "line one\nline 2\nline three\nline four\n";
similar_text($a, $b, $percent);
echo "Similarity: " . round($percent, 1) . "%\n"; // Similarity: 74.3%
// For a real diff, use the shell:
function unifiedDiff(string $fileA, string $fileB): string {
$a = escapeshellarg($fileA);
$b = escapeshellarg($fileB);
exec("diff -u {$a} {$b}", $output);
return implode("\n", $output);
}
// Or use a library like jfcherng/php-diff:
// composer require jfcherng/php-diff
use Jfcherng\Diff\Differ;
use Jfcherng\Diff\DiffHelper;
$result = DiffHelper::calculate($a, $b, "Unified");
echo $result;
Common diff options (command line)
When using the diff command on Linux/macOS:
| Flag | Effect |
|---|---|
-u |
Unified format (most readable, used by git) |
-c |
Context format (older, used by some patch tools) |
-i |
Ignore case differences |
-w |
Ignore all whitespace |
-b |
Ignore changes in amount of whitespace |
--word-diff |
Word-level diff instead of line-level |
-r |
Recurse into directories |
Git adds its own layer:
git diff HEAD~1 HEAD # changes in last commit
git diff --stat # summary of changed files
git diff --word-diff # word-level highlighting
git diff --ignore-all-space # skip whitespace-only changes
When to use line-level vs character-level diff
| Use case | Best level |
|---|---|
| Source code | Line |
| Prose / documents | Word or character |
| JSON / YAML | Line + collapse unchanged blocks |
| Binary files | Don't diff — compare hashes instead |
| Large config files | Line with context |
Compare text online
If you need to quickly compare two blocks of text without writing code, the Text Diff tool does it in your browser. Paste two texts, get a color-coded diff instantly — added lines in green, removed in red, unchanged lines in grey. No account needed, nothing is uploaded to a server.
FAQ
Q: What's the difference between a diff and a patch? A diff is the output that shows what changed. A patch is a diff that can be applied to a file to reproduce the changes. They use the same format — the difference is intent.
Q: Can I diff binary files? Not meaningfully. Binary diff tools exist, but they compare bytes rather than lines and produce output that's hard to interpret. For documents like Word or PDF, convert to text first, then diff.
Q: Why does my diff show huge changes when I only changed one line?
Usually a whitespace or line-ending issue. Try diff -b (ignore whitespace changes) or check that both files use the same line endings (CRLF vs LF). You can normalize endings with dos2unix or a text editor.
Q: What is a "merge conflict"?
When two people edit the same lines differently, git can't automatically apply both changes. It marks the conflict with <<<<<<<, =======, and >>>>>>> markers and asks you to manually choose which version (or combine them). The diff algorithm is what detects that two changes overlap.
Q: Is the Myers algorithm always best?
No. For code that has a lot of identical blocks (e.g., functions that look similar), the patience diff algorithm (git diff --patience) produces more readable output by preferring to align unique lines. Try both if the default output is confusing.