# What Started as a Bugfix: Revision Graph 4.0.0

> One ring drawn in the wrong place. Chasing it turned into a rewrite — because Drupal stores no parent pointer for revisions, and "parent" means three different things on a translated node.

By Shibin Das · Drupal · Published 18 Aug 2026. Source: https://www.d34dman.com/posts/drupal/revision-graph-4-0-0-recorded-provenance/
Tags: Drupal, Content Creator Tools, Data Modelling, Engineering

[Revision Graph](https://www.drupal.org/project/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.

*Diagram: How 3.x showed a revision's facts, and how 4.0.0 shows them.* In 3.x the screen was two panels. The graph on the left could only draw two of a revision's facts, as a dark disc for the default revision and a green ring for the live one, both at the same radius on the same dot. The list on the right carried all five facts as text chips: Current, Latest revision, Published, Live and Default. In 4.0.0 there is one panel: the node-wide facts are stated in words above the rail and repeated as a single label on the row they are about.

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.

> **Warning:**
>
> **Upgrading?** Drupal 11 or 12 only — Drupal 10 sites stay on 3.1.1 — and run
> `drush updb`. There are breaking changes to the payload, the renderer and the CSS
> class names; the
> [changelog](https://git.drupalcode.org/project/revision_graph/-/blob/4.x/CHANGELOG.md)
> 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.

*Diagram: Three different right answers to &#8220;what is revision 5's parent?&#8221;.* A node with five revisions, listed newest first as the rail displays them: a German translation added at 5, an English draft at 4, English published at 3, an English draft at 2, and English published at 1. Revision 5 has three defensible parents. The revision it was built from is 3, not 4, because a draft never becomes the default revision. The language it was copied from is English, which no revision ID can express. And a visitor only ever saw revisions 1, 3 and 5, because 2 and 4 were never published.

Answer <span class="num">1</span> 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 <span class="num">2</span> 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 <span class="num">3</span> 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](https://www.drupal.org/i/3417493) with tstoeckler replaced thirteen
lines of that with the call that was always the right answer:

```php
$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 &mdash; 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.

*Diagram: A dot answers two independent questions: its ring and its centre.* The ring says whether the revision was ever the default revision: solid for yes, dashed for never. The filled centre says whether it is live right now, meaning it is both this language's current revision and published. Because a language has exactly one current revision, at most one dot per language carries a filled centre, and none does when that language's current revision is unpublished. Facts a shape cannot carry are printed on the row as text labels reading DEFAULT, PENDING or UNPUBLISHED.

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 &mdash;
`Default revision #9 · Live now: af #8 · bn #9 · en #5` &mdash; 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](https://www.drupal.org/project/revision_graph) on Drupal.org ·
  [source](https://git.drupalcode.org/project/revision_graph)
- [`docs/REVISION-MODEL.md`](https://git.drupalcode.org/project/revision_graph/-/blob/4.x/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](https://www.d34dman.com/posts/drupal/revision-graph-guide/)
  — the earlier, non-technical introduction
