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:
- Each round, try to reach every reachable diagonal;
- Prefer extending the diagonal with the greatest same-line progress—i.e. follow free diagonals to the end;
- 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):
- Watch how the
@@context range is chosen; - Deliberately move a large block up or down and see it become "all deleted + all added";
- 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.