← Back to Blog

Text Diff Algorithms: How to Read the Output Differently

First answer: "why isn't diff smart?"

"I only moved the code on line 3 to line 9, why does git diff mark both blocks red?" Behind this complaint is the fact that diff algorithms don't try to understand 'moves' at all—they want one thing only:

Use the fewest editing steps (deletions, insertions) to turn the old text into the new text.

Once you understand that objective, most "counter-intuitive" output explains itself.

Grid model: every diff is a path

Put the old text on the horizontal axis and the new text on the vertical axis:

new text y
  ↑
  … ○──○──●   (diagonal = equal chars, free)
  …
  • One step right → delete one old character;
  • One step up → insert one new character;
  • A diagonal (both endpoints equal) → free, no step counted.

From start to end, every route corresponds to one way of turning the old text into the new. Diff wants the route with the fewest total steps—i.e. take as many free diagonals as possible, concentrating changes where they must happen.

Myers: approaching the shortest path with diagonal numbers

Myers' algorithm tracks progress by which diagonal (number k) you have reached and expands k round by round:

  1. Each round, try to reach every reachable diagonal;
  2. Prefer extending the diagonal with the greatest same-line progress—i.e. follow free diagonals to the end;
  3. When one step reaches the finish, that path is the shortest in edit steps.

Its elegance: among equally good answers, Myers tends toward concentrated edits (rather than many scattered inserts/deletes), so its output is usually compact and readable. That's why git diff and GNU diff implement it by default.

(The exact algorithm walks by "d steps" layers rather than strictly by k; the "eat as many free diagonals as possible" framing is enough for the intuition.)

Line-level vs character-level: the trade-off

Dimension Line-level diff Character-level diff
Granularity whole lines added/removed words/characters inside a line
Best for code review, config comparison a parameter change in a long line
Noise low high (large files turn red)
Data like JSON misjudged as whole-line changes more semantic (use a dedicated JSON diff)

Advice: read line-level first to grasp structure, then enable char-level highlight for the lines you care about. Prefer structured diffs for data.

Reading one real diff

Suppose a config raises timeout from 30 to 60 and moves a comment. git diff --no-index a.txt b.txt might output:

--- timeout: 30
+++ timeout: 60
@@ -1,5 +1,5 @@
  server { listen 80 }
- # TODO tune
- timeout: 30
+ timeout: 60
  • @@ -1,5 +1,5 @@: the context block's range;
  • - lines exist only in the old text, + lines only in the new;
  • identical lines stay untouched—the algorithm preserved as much unchanged context as possible around the edit.

Don't jump to "the comment was deleted": in the shortest-path view, deleting the comment plus adjusting the time costs less in edit distance than the imaginary operation "keep it and move it".

Practice: making the intuition concrete

Take two snippets of C code/config with small edits and run them through an online text diff tool (or local diff -u):

  1. Watch how the @@ context range is chosen;
  2. Deliberately move a large block up or down and see it become "all deleted + all added";
  3. Then compare two JSON blobs with a JSON diff tool—note the difference over line-diff when recognizing array moves.

When you can predict "this block will probably be flagged as all-delete/all-add", you've internalized the edit-distance view.

Summary

Diff isn't dumb; its objective function is minimal edit steps + maximizing free diagonals—rigorous, deterministic and reproducible. Once you read it that way, you step out of the confusion of re-reading commits: it's not that the content changed, but that the edit path changed.

Frequently Asked Questions

How are the '+' and '-' in diff output decided?

Diff finds the **minimal set of edit operations** (insert/delete/change) turning the old text into the new one. Line by line: a line present only in the new text gets '+', only in the old gets '-', lines equal on the minimal path stay untouched. When you move a block and see the whole block shown as deletions/additions, that's because the algorithm optimizes minimal edit cost rather than human 'move' intuition—Myers' algorithm explicitly balances edit steps against the number of inserted/deleted lines.

What does the 'diagonal' in Myers diff mean?

Set the old text on the x-axis and the new text on the y-axis to build a grid from bottom-left start to top-right end. Moving right deletes an old char, moving up inserts a new char, and moving along a **diagonal step is 'free' when the chars match**. Myers' algorithm tracks progress by 'diagonal number' (k) and expands k one round at a time; the first path to arrive at the end is the one with the fewest edit steps. Once you grasp 'walk free diagonals as much as possible', you understand why diffs grow long runs of unchanged content and concentrate changes into minimal hunks.

When should I use line-level vs character-level diff?

Line-level diff (with context) is enough to convey which lines were added/removed and how structure changed, so it is the default for code review and config comparison—low noise and readable. Character/word-level diff (supported by many visual tools) pins changes within a long line to individual tokens, handy for checking whether one parameter value changed. Rule: use line-level for structure, char-level highlight for in-line tweaks. For JSON, a dedicated JSON diff maps additions/deletions/moves more semantically than raw line diff.

Why does diff sometimes show all-delete/all-add for identical blocks?

Usually diff isn't wrong—it's that **identicalness only counts when blocks align on the same candidate path**. When two versions reorder large blocks or change nesting, those blocks don't sit on the same minimal edit path and get split into 'removed then added' even though the content is byte-identical. So `git diff` reports edit distance, not content similarity. To catch moves, use move-detection tools or a JSON diff that recognizes array relocation.

← Back to Blog