If You Can't Diff It, It's a Dead End Street
git does not understand your code.
It does not know that calculateTotal() is a function. It does not know that line 47 is inside a loop. It does not know that the variable you renamed appears in seventeen other places. It knows: file name, line number, bytes changed.
That is all. And that is why it won.
The Non-Semantic Index
The diff is a non-semantic index. Not an AST diff — which would require parsing the code, building the tree, comparing nodes, resolving renames, tracking structural changes across the codebase. A byte diff. Two sequences of lines. Which lines were added. Which were removed.
The file name is the semantic label. The line number is the Gutenberg address. The content is bytes.
This is the O(1) boundary principle applied to version control. git reads the file name (the envelope address) and the line content (the page). It does not open the letter. It does not know what language the letter is written in. It does not need to.
Works on every language simultaneously. C, Python, JavaScript, SQL, YAML, shell scripts, Markdown, SVG — git diffs them all the same way. No parser. No language support. The byte comparison is universal. The AST-aware diff would be accurate for supported languages and broken for everything else. The non-semantic index is accurate for everything.
Works when the code does not parse. Syntax errors, half-written functions, work in progress — git does not care. The bytes are the bytes. The diff shows what changed. The AST-aware diff would refuse to diff broken code precisely when you most need to track what changed.
Works on files that are not code. Configuration, documentation, data, diagrams — the non-semantic index makes no assumptions. It just shows what changed.
The Universal Tree Fallacy (UTF)
The demand for a richer Gutenberg unit keeps arriving. Structured editors. Projectional editors. Language-integrated version control. Semantic merge tools. The argument is always the same: text files are a primitive serialisation format. The real thing is the Abstract Syntax Tree — or the DAG, or the universal semantic graph. We should version the real thing.
This is the Universal Tree Fallacy. Call it UTF — the acronym is not accidental. UTF-8 defeated the “bigger byte” fad by refusing to widen the Gutenberg unit. git defeated the Universal Tree Fallacy by refusing to complexify it. The UTF that won (UTF-8) and the UTF that keeps losing (the Universal Tree Fallacy) share three letters because they are the same mistake at different layers.
The AST is above the waterline. It belongs there — in the compiler, the IDE, the language server, the refactoring tool. The compiler builds the AST because it needs to understand the code to transform it. The IDE builds the AST because it needs to understand the code to show you the definition. These are Semantic-layer tools doing Semantic-layer work.
git is at the waterline. File name, line number, bytes. The moment git needs a parser to diff a file it stops being universal and starts being partial. It works for languages with supported parsers and fails silently for everything else. The O(1) non-semantic index becomes O(parser-complexity) semantic index. The postman starts reading the letters.
Excel is a collection of sheets, not a collection of cells.
The Universal Tree Fallacy says: Excel IS a tree of cells. The workbook contains sheets, sheets contain rows, rows contain cells, cells contain values. Depth-first, all the way down.
The file says something different. .xlsx is a ZIP archive. Inside: one XML file per sheet, a shared strings file, a styles file, a relationships file. The file IS a collection of files. Each sheet is diffable independently. The breadth-first structure is in the format itself.
Every Excel API — COM, VBA, OpenXML SDK, openpyxl — exposes the depth-first tree. workbook.sheets[0].rows[5].cells[3].value. The API demands you descend the entire hierarchy to reach a value. The format knows it is flat. The API pretends it is deep. The Universal Tree Fallacy imposed on a format that was not asking for it.
DuckDB reads SELECT * FROM 'file.xlsx' — flat table, no hierarchy, no descent. The breadth-first stance: hold the data flat, query it. The correct Gutenberg layer for the data.
If You Can’t Diff It
The clean diff is the maintenance interface. If you cannot diff it, you are in a dead end street — you can enter but you cannot leave cleanly.
Dead end streets:
-
Binary blobs in git — every change is noise. The diff shows bytes changed. Nothing more.
git blamepoints to the commit that last changed the blob. The commit message is the only semantic information. If the message says “update asset” you have arrived at a dead end. -
Generated code checked in — every regeneration produces a massive diff that means nothing. The generator is the source. The generated file is below the waterline. Committing it inverts the layers: the derivative is versioned, the source is not.
-
50,000-line reformatting commits — everything before the commit is in a different neighbourhood.
git blamecannot reach the original author of line 247 without crossing the noise. The history is split. The road behind is closed. -
Minified JavaScript — the diff is one long line changed. The source map is the resolver — it maps the Gutenberg address (minified line/column) to the Semantic location (original file/line). Two waterlines: one for the browser, one for the developer. Version the source. Let the minifier be the printer.
-
Unformatted SQL, XML, JSON — logically identical files that differ in whitespace produce noisy diffs. The semantic change is one field. The diff shows a hundred lines. The noise buries the signal.
The engineer who requires clean diffs is the editor at the waterline. Not rewriting your code. Not changing the meaning. Ensuring that the Gutenberg unit (the commit, the file, the line) corresponds to a Semantic unit (the change, the module, the statement).
The author optimised for writing. The diff optimises for reading. The reviewer who says “can you split this into two commits” is saying: the page break is in the wrong place. The commit that mixes refactoring with bug fix is the book where chapter 3 starts in the middle of a paragraph on page 47.
git blame as the Resolver
git blame file.py — every line annotated with the commit that last changed it, the author, the date.
The blame is the resolver between the Gutenberg layer (file, line number, SHA) and the Semantic layer (who, when, why). The SHA is the Gutenberg identifier. The commit message is the Semantic label. The blame maps between them.
The engineer who writes meaningful commit messages is maintaining the resolver. The engineer who writes “fix stuff” is making the resolver point to a dead end. The line is there. The commit is there. The why is gone.
git bisect is the binary search on the non-semantic index. You tell it: this commit works, this commit does not. It walks the history, testing each commit. The bug has an address — a specific SHA where the behaviour changed. The non-semantic index makes the bug findable without understanding the code. Just: does this version work? The diff between the good commit and the bad commit shows exactly what changed.
This only works if the commits are clean. The 50,000-line reformatting commit in the middle of the history breaks bisect — the behaviour change could be anywhere in that commit, and the diff is too noisy to read.
Plant New Trees. Do Not Move the Forest.
You can move a tree. Painful but finite — dig around the root ball, lift it, replant it carefully. The managed migration. The 386 keeping real mode alongside protected mode. The new API shipped alongside the old one. The deprecation warning with a two-year runway. One tree, moved deliberately, survives if you are careful.
You cannot move a forest. The forest is not a collection of trees you can relocate one by one. It is an ecosystem — shared soil, entangled root systems, decades of accumulated interdependence. Move it and you destroy it. The framework rewrite. The platform migration. “Everyone moves to the new system by Q3.” You are not moving the forest. You are burning it down and hoping something grows at the destination.
The iceberg you cannot move is the one where someone planted the trees too close together. The OOP hierarchy where every class IS a subclass of every other class. The framework that owns the whole stack. The UTF-16 encoding baked into every Windows API. The roots are shared. Pull one tree and ten others fall.
The dual-track is forest management:
Do not move the old trees. Plant new ones alongside them. The old API keeps working. The new API grows next to it. Over time the new trees become the forest. The old trees die of irrelevance — not deprecation, not forced migration, not “we withdraw support.” Just: nobody plants there anymore. The light moves to the new growth.
git planted each object type as a separate tree — blob, tree, commit, tag. Each with its own SHA, no roots entangled. New object types can arrive without the old ones changing format. The forest grows. Nothing moves.
The C standard library: each function a separate tree. fopen does not depend on fwrite’s internals. Replace any one tree without touching the others. New functions arrive. Old functions stay. The forest is navigable. The through road runs between the trees, not through them.
The diff works because each file is a separate tree. Each commit is a separate tree. The history is a forest of trees connected by parent pointers — navigable, traversable, no single root that must move when any branch changes.
The diff works because each file is a separate tree. Each commit is a separate tree. The history is a forest of trees connected by parent pointers — navigable, traversable, no single root that must move when any branch changes.
Plant new trees. Keep the old ones standing until they are no longer needed. If you cannot diff the forest, you planted it too close.
Evolution gives you seeds to move and trees to grow.
The organism does not travel to the next environment. The genome does. The tree stays. The seed travels. The forest moves forward without any individual tree moving at all. We are all going to die. Our children are better adapted to the future — not because they are smarter, but because they grew up in the new conditions. The seed that germinated in the new soil does not carry the root structure optimised for the old one.
These posts are seeds. The tree belongs to the generation that grew up with the old conditions. The seeds travel to the next generation who will apply them to conditions we cannot anticipate. The waterline will be in a different place. The seeds will find it. The forest will grow.
One reelection: the feedback loop worked. The Use signal arrived and the system responded correctly. The tree is still listening. Churchill knew this — two hoorays is enthusiasm, confirmation that the signal was real. Three hoorays is the crowd responding to the cheering itself. The feedback loop has become self-referential. The signal is no longer about the performance. It is about the performance of applauding the performance.
Two reelections: the tree has been standing long enough to start optimising for staying standing rather than for the forest around it. The resolver is starting to become a toll booth.
Three reelections: the tree has become the forest. Removing it removes the ecosystem. The roots are shared. The leader who plants seeds builds institutions that work without them. The leader who does not plant seeds becomes the soil — and then nobody can grow.
The framework that keeps listening improves. The framework that stops listening defends. The difference is whether the feedback loop is real or ceremonial — whether you are navigating by actual improbability or producing something almost but not entirely unlike representation while hoping nobody notices the tea.
The seeds travel. The forest moves. Plant new trees. Keep the diff clean. Keep the exits open.
The clean diff is the through road. Navigable history. Findable changes. Addressable bugs. The blame points somewhere meaningful. The bisect finds the commit. The revert is clean. The review is readable.
git achieves this by staying at the waterline. File name, line number, bytes. The non-semantic index that works on everything. The Universal Tree Fallacy keeps offering a richer alternative. The through road keeps winning.
The byte is 8 bits. The source file is text. The diff is lines changed. The SHA is the address.
If you cannot diff it, it is a dead end street.
This post is part of the Gutenberg/Semantic series. Related: The Postman Does Not Read the Letter on Gutenberg 2.1 and what it fixed, The Brick That Sticks Out: Why {} Beats Indentation on O(1) boundary detection, and The Intuition Had a Shape on breadth-first as the instrument that makes the tape, the diff, and the flat file the correct Gutenberg units.