A guide for junior engineers on how the contribution system works.
There are 3 main doctypes that power the contribution system:
| Doctype | Purpose |
|---|---|
| Wiki Document | The actual live wiki page (what visitors see) |
| Wiki Change Request (CR) | A "pull request" — a container for someone's proposed edits |
| Wiki Revision | A snapshot of the entire wiki tree at a point in time |
Think of it like Git: a Wiki Revision is a commit, a Wiki Change Request is a branch/PR, and Wiki Documents are the working tree.
When a user clicks "Edit" on a wiki page:
sequenceDiagram
participant U as User
participant API as Backend API
participant CR as Wiki Change Request
participant Rev as Wiki Revision
U->>API: Clicks "Edit Page"
API->>API: get_or_create_draft_change_request()
alt User already has a Draft CR
API->>CR: Return existing Draft CR
else No existing draft
API->>Rev: Create OVERLAY revision<br/>(empty, points to main_revision)
API->>CR: Create new CR<br/>base=main_revision, head=overlay
CR-->>U: Draft CR ready
end
Key concept — Overlay Revisions:
graph TD
subgraph "Before Refactor (slow)"
A1[main_revision<br/>500 items] -->|CLONE ALL 500| B1[head_revision<br/>500 items copied]
end
subgraph "After Refactor — Phase 4 (fast)"
A2[main_revision<br/>500 items] -->|points to| B2[head_revision OVERLAY<br/>0 items initially]
B2 -->|inherits all items<br/>from parent| A2
end
style B2 fill:#d4edda,stroke:#28a745
style B1 fill:#f8d7da,stroke:#dc3545
An overlay revision starts empty. It inherits everything from its parent (main_revision). Only when you actually edit a page does that page get copied into the overlay — this is called copy-on-write.
When a user edits, creates, or deletes a page inside their CR:
sequenceDiagram
participant U as User
participant API as Backend
participant Overlay as Head Revision (Overlay)
participant Base as Base Revision (Parent)
U->>API: Save page edit
API->>Overlay: ensure_overlay_item(doc_key)
alt Item NOT in overlay yet
Overlay->>Base: Copy item from parent into overlay
end
API->>Overlay: Update the item (new content, title, etc.)
API->>Overlay: mark_hashes_stale()
API-->>U: Page saved in draft
graph LR
subgraph "Base Revision (500 pages)"
P1[Page A]
P2[Page B]
P3[Page C]
P4[Page D]
P5[...]
end
subgraph "Overlay (only edits)"
O2[Page B modified]
O5[Page E new]
end
Overlay -->|inherits unchanged pages| P1
Overlay -->|inherits unchanged pages| P3
Overlay -->|inherits unchanged pages| P4
style O2 fill:#fff3cd,stroke:#ffc107
style O5 fill:#d4edda,stroke:#28a745
The overlay only stores what changed. Reading the full tree means: "take base items, then apply overlay items on top."
Content is stored in Wiki Content Blob — deduplicated by SHA-256 hash:
graph LR
RevA_Item1[Revision A: Page X] -->|content_blob| Blob1["Blob abc123<br/>(SHA-256 of content)"]
RevB_Item1[Revision B: Page X] -->|content_blob| Blob1
RevC_Item1[Revision C: Page X<br/>EDITED] -->|content_blob| Blob2["Blob def456<br/>(new content hash)"]
style Blob1 fill:#e2e3f1,stroke:#6c757d
style Blob2 fill:#d4edda,stroke:#28a745
If two revisions have the same content for a page, they share the same blob. Saves tons of storage.
stateDiagram-v2
[*] --> Draft: User creates CR
Draft --> InReview: request_review(reviewers)
InReview --> Approved: All reviewers approve
InReview --> ChangesRequested: Any reviewer requests changes
ChangesRequested --> InReview: Author edits & resubmits
Approved --> Merged: Wiki Manager merges
Draft --> Archived: Stale/abandoned
Merged --> [*]
Archived --> [*]
When a Wiki Manager clicks "Merge", the system decides between two paths:
flowchart TD
Start["merge_change_request(cr)"] --> Check{"Has main_revision<br/>advanced since CR<br/>was created?"}
Check -->|"No — base == main<br/>(no concurrent edits)"| FF["FAST-FORWARD MERGE"]
Check -->|"Yes — base != main<br/>(someone else merged)"| TW["THREE-WAY MERGE"]
FF --> Delta["Apply ONLY changed docs<br/>(delta-only, Phase 5)"]
TW --> Conflict{"Conflicts?"}
Conflict -->|No| AutoMerge["Auto-merge disjoint changes"]
Conflict -->|Yes| Block["Create Wiki Merge Conflict<br/>records, block merge"]
Delta --> Done["CR status = Merged<br/>space.main_revision updated<br/>Live wiki tree updated"]
AutoMerge --> Done
Block --> Resolve["User resolves conflicts"] --> TW
style FF fill:#d4edda,stroke:#28a745
style TW fill:#fff3cd,stroke:#ffc107
style Block fill:#f8d7da,stroke:#dc3545
style Done fill:#d4edda,stroke:#28a745
Nobody else merged anything while this CR was open, so we just apply the changes directly:
gitGraph
commit id: "main_revision"
branch change-request
commit id: "edit Page B"
commit id: "add Page E"
checkout main
merge change-request id: "fast-forward merge"
The Phase 5 optimization here: instead of re-saving all 500 pages, it computes the delta (what actually changed) and only touches those docs. 1 page changed = 1 page saved.
Someone else's CR was merged while this one was still open:
gitGraph
commit id: "base_revision"
branch cr-author
commit id: "edit Page B"
checkout main
commit id: "someone else merged (Page D)"
checkout cr-author
commit id: "add Page E"
checkout main
merge cr-author id: "three-way merge"
The algorithm compares three versions:
- Base: state when CR was created
- Ours: current main (includes other people's merges)
- Theirs: the CR author's changes
flowchart LR
Base["BASE<br/>Original state"] --> Ours["OURS<br/>Current main"]
Base --> Theirs["THEIRS<br/>CR's changes"]
Ours --> Merge["Merge"]
Theirs --> Merge
Merge --> Result["Merged revision"]
style Base fill:#e2e3f1
style Ours fill:#cce5ff
style Theirs fill:#fff3cd
style Result fill:#d4edda
If both sides edited different pages — auto-merge works fine. If both sides edited the same page on different lines — auto-merge tries line-by-line. If both sides edited the same lines — conflict, needs manual resolution.
sequenceDiagram
participant Merge as Merge Logic
participant Rev as New Merge Revision
participant Space as Wiki Space
participant Docs as Wiki Documents (live)
Merge->>Merge: Compute delta (changed doc_keys only)
loop For each changed doc
alt Content-only change
Merge->>Docs: frappe.db.set_value() (skip validation, fast)
else Structural change (parent, slug)
Merge->>Docs: Full doc.save() (with validation)
else New page
Merge->>Docs: Create new Wiki Document
else Deleted page
Merge->>Docs: Mark is_deleted=1
end
end
Merge->>Rev: Save merge revision
Merge->>Space: space.main_revision = merge_revision
flowchart TD
A["User clicks Edit"] --> B["Get/create Draft CR<br/>(overlay revision, O(1))"]
B --> C["User edits pages<br/>(copy-on-write into overlay)"]
C --> D["Submit for review"]
D --> E["Reviewers review"]
E -->|Changes requested| C
E -->|Approved| F["Wiki Manager merges"]
F --> G{"Concurrent changes?"}
G -->|No| H["Fast-forward<br/>(delta-only apply)"]
G -->|Yes| I["Three-way merge"]
I -->|Conflict| J["Resolve conflicts"] --> I
I -->|Clean| K["Apply merged changes"]
H --> K
K --> L["Live wiki updated!<br/>main_revision advanced"]
style A fill:#e2e3f1
style H fill:#d4edda
style J fill:#f8d7da
style L fill:#d4edda
- Drafts are cheap — overlay revisions start empty and only store what you change (copy-on-write)
- Content is deduplicated — same content = same blob, shared across revisions
- Merges are smart — fast-forward when possible, three-way merge when needed
- Only deltas are applied — merging 1 page change in a 500-page wiki only touches 1 page
- Conflicts are explicit — if two people edit the same lines, a
Wiki Merge Conflictrecord is created and must be resolved manually - The old
Wiki Page Patchsystem is deprecated — everything now goes through Change Requests + Revisions
| File | What it does |
|---|---|
wiki/wiki/doctype/wiki_change_request/wiki_change_request.py |
CR lifecycle, review actions |
wiki/wiki/doctype/wiki_revision/wiki_revision.py |
Revision creation, overlay logic, effective item maps |
wiki/wiki/doctype/wiki_document/wiki_document.py |
Live wiki page, desk-edit sync to revisions |
wiki/wiki/merge.py |
Merge logic (fast-forward, three-way, conflict detection) |
wiki/wiki/doctype/wiki_space/wiki_space.py |
Space management, main_revision tracking |
wiki/wiki/doctype/wiki_content_blob/wiki_content_blob.py |
Content deduplication via SHA-256 |