Docsheaf
Blog

How change requests merge block by block

A look inside Docsheaf's three-way merge — content-addressed documents, field-by-field page merges and diff3 over blocks — and why two people can edit the same page without stepping on each other.

The Docsheaf team · · 3 min read

When two writers edit the same page in a wiki, the second save usually wins. In Docsheaf, both edits survive — as long as they touched different blocks. This post explains how.

Branches are cheap because documents are immutable#

A page body is a ProseMirror-style JSON document: a list of blocks such as paragraphs, headings, code blocks and hints. Docsheaf stores each document content-addressed: its id is the SHA-256 hash of its canonical JSON.

That one decision makes branching cheap. A branch is a working tree of page rows — title, slug, position, parent and a pointer to a document. Opening a change request copies those rows, not the content. Pages nobody edits keep pointing at the same document, on every branch and in every revision.

Revisions are immutable snapshots of a tree. Docsheaf creates one when a change request opens (its base), on every merge, and at most every ten minutes for live edits on main.

Three trees, not two#

A merge compares three versions of the space:

  • base — the revision the change request started from

  • ours — main as it is now

  • theirs — the change request branch

Comparing each side against the common base is what lets Docsheaf tell "you changed this" apart from "they changed this". A two-way diff can't.

Step 1: merge each page field by field#

For every page that exists in any of the three trees, Docsheaf compares each field — title, slug, emoji, description, parent, position, visibility and so on — against the base:

  • Changed on one side only: take that side.

  • Changed identically on both sides: fine, take it.

  • Changed differently on both sides: a conflict for that page.

The one exception is the page's document. Different documents on both sides don't fail yet — they fall through to a block-level merge.

Step 2: diff3 over blocks#

When both sides edited the same page, Docsheaf runs a three-way merge over its top-level blocks. Each block is keyed by its canonical JSON, so "the same block" means byte-for-byte identical content.

  1. Compute the changes (hunks) from base to ours and from base to theirs.

  2. Group hunks that overlap in the base.

  3. A group touched by only one side applies cleanly.

  4. A group touched by both sides merges only if both produce identical content; otherwise the page conflicts.

In practice: if one writer fixes a typo in the introduction and another rewrites the troubleshooting section further down, both changes land. If both rewrite the same paragraph differently, Docsheaf stops and asks.

Step 3: additions, deletions and tidy-up#

  • A page added on one side is kept.

  • A page deleted on one side and untouched on the other is deleted.

  • A page deleted on one side and modified on the other is a conflict — silently dropping someone's edit is the worst outcome.

Finally, pages whose parent disappeared are moved to the root, and sibling positions are normalised.

Resolving conflicts#

Conflicts block the merge. The API answers 409 merge_conflict with the list of affected pages and why each one conflicts. The reviewer picks a side per page — main or the change request — and merges again with those resolutions.

{
  "resolutions": {
    "pg_8fK2mQx1Lr0aVb7c": "theirs",
    "pg_Tz4nW9sYp3eHd6Ju": "ours"
  }
}

Update from main uses the same machinery in the other direction: it merges main into the change request branch and moves the base forward, so long-running change requests can catch up before review.

Saves that never overwrite silently#

Merging is one half; saving is the other. Every page row carries a version number. The editor sends the version it started from with each save, and if someone else saved in between, the API answers 409 version_conflict instead of overwriting their work.

Why blocks and not lines?#

Docs aren't code. A paragraph reflowed by the editor would show up as a dozen changed lines in a text diff. Blocks match how writers think about a page — this hint, that code sample, the table below it — so merges and review diffs line up with what actually changed.

The merge lives in @docsheaf/document, the same package the editor and the published site use, and it is covered by tests like any other code path that can lose work.

Accurate docs. Better answers.

Start writing for free, or talk to us about bringing your team and your existing docs.