What Started as a Bugfix: Revision Graph 4.0.0
Revision Graph draws a node’s revision history the way a Git client draws history: one row per revision, a lane per language, and the branching shown rather than described.
4.0.0 is out, and it started as a bugfix.
A node with English, Afrikaans and Bengali translations was drawing wrong. The screen was two panels: a graph on the left, a list of revisions on the right. A revision’s facts — is this the current one, the latest, the published one, the live one, the default one — were text chips in the list, and the graph tried to echo them by ringing the dot.
It could only echo two of them. Live drew a green ring and Default drew a dark
disc; the other three had no styling at all and drew invisible circles. And both of
the marks that were drawn landed on one language’s dot — whichever language the
revision happened to touch. On this node that was Bengali, so the graph read as
though Bengali owned the default revision, while the English revision readers were
actually served sat there unmarked.
So: stop drawing that ring on a language’s dot. One afternoon, surely.
Except to know which dot a fact belongs to, you have to know what a dot is — and that question turns out to have no clean answer in Drupal.
drush updb. There are breaking changes to the payload, the renderer and the CSS
class names; the
changelog
lists them.
“Parent” means three different things
Here is the heart of it, and it is worth knowing even if you never install this module. Ask what a revision’s parent is, on a translated node with drafts, and there are three correct answers.
Answer ① is the surprising one in practice. When you open a translation form, Drupal builds it from the revision it loads normally — the default revision, the last published one. So a translation’s parent is usually not the newest revision; any draft sitting above it is not in its lineage at all.
Answer ② is why storing a revision number was never going to be enough. German didn’t come from revision 3, it came from English. A parent here is a (revision, language) pair, and half of it is a language.
Answer ③ is the one nobody stores, and the one most people picture. A lane in the graph is where content came from — not the sequence of versions the public saw.
Drupal doesn’t record any of this
There is no parent_vid column. node_revision gives you an ID, a timestamp,
an author and a log message, and that is the end of the topology. Revisions are a
list, not a tree.
So a module drawing the tree either keeps the answer itself or works it out afterwards. This one has now done both, in that order.
From 2019 we kept our own table, with a parent and a branch name per revision.
Getting the parent into it was the awkward part, and the commit messages from
that week are honest about it: “introduce a hackish way to store revision
parent”, then “Bugfix parent version hunt”. The code read the wrong property and
compensated by parsing the current URL for /node/X/revisions/Y/revert — a
storage layer reading a route to work out what it was storing, and blind to any
revert not done by hand in the UI. In 2024,
issue #3417493 with tstoeckler replaced thirteen
lines of that with the call that was always the right answer:
$parent_version_id = $entity->getLoadedRevisionId();
Then the 3.0.0 refactor dropped the table. The commit is called “Code cleanup”, and that is the whole recorded rationale. For the 3.x line the edges were inferred instead — and inference is fine until it isn’t: a revert looks exactly like an edit that happened to restore old values. Those are precisely the cases a history graph exists for.
4.0.0 records it again, as a base field written at save time. A field rather than a side table because core then manages its life: it travels with the revision and goes when the revision goes. The old table had no delete handling at all, so deleting a revision left its row behind, pointing at something that no longer existed. An edge the module recorded is then drawn a pixel heavier than one it merely inferred — a small thing, but it means the graph never claims to know more than it does.
Existing revisions are not backfilled. They keep their inferred edges. Reconstructing them would mean guessing, and a graph that draws a confidently wrong edge is worse than one that draws fewer.
What the fix actually was
Not a rendering tweak. The dot was carrying facts at three different scopes and had vocabulary for none of them, so each scope got its own channel.
The two channels are worth stating exactly, because they are easy to conflate. The
ring is trunk — was this revision ever the default revision, the one the site
served? The centre is state === 'live' — is this revision both its language’s
current revision and published?
The second has a consequence worth knowing before you read a graph: since a language has exactly one current revision, at most one dot per language can be filled — and none is, when that language’s current revision happens to be unpublished. If you are hunting for what a visitor gets right now, you are looking for one filled dot per lane, and its absence is information too.
Facts about the whole node are now said in words above the rail —
Default revision #9 · Live now: af #8 · bn #9 · en #5 — and repeated as a
text label on the row they are about, in a status column, rather than as a mark
on one language’s dot.
That last line is the annoying part. It would have prevented the original misreading on its own, it is the thing an editor opens the page to find out, and it needed none of the rest of this work. But it is only trustworthy because of the rest: a header stating what’s live, generated by a graph that couldn’t say what a dot was, is a sentence I’d have had no reason to believe.
References
- Revision Graph on Drupal.org · source
docs/REVISION-MODEL.md— how Drupal actually stores revisions and translations, and what can and cannot be recovered from it- A Content Creator’s Guide to Revision Graph — the earlier, non-technical introduction