-
Notifications
You must be signed in to change notification settings - Fork 3
System Overview en
A library for tracking the version history of a single document. That's
the whole scope. It's not a multi-file repository like Git — no branches,
no server, nothing like that. You point a SimpleRCS instance at a file
path, a BytesIO, or any file-like (BinaryIO) object, and it keeps
committing history onto that one stream.
The reason it exists: sometimes Git is overkill. If you want history for a
single wiki page, a config file, or a document sitting in a database BLOB
column, spinning up a .git directory and maintaining an object graph is
overhead you don't need. SimpleRCS just drops into that spot as one file
(or one BLOB column).
Same idea as classic RCS. HEAD (the latest version) is always stored as full text, and everything older than that is stored as a delta — "how do I get from the next newer version back to this one."
[V1 delta] [V2 delta] ... [Vn-1 delta] [Vn full text]
^^^^^^^^^^^^^
HEAD
Two things fall out of this naturally:
- Reading the latest version is always fast. That's the version people actually read most often in practice, so it's the one worth optimizing for. O(1).
- Reading an old version gets slower the further back you go. Five versions behind HEAD means applying five deltas in sequence. O(k), where k is the distance from HEAD.
When you call commit():
- Read the current HEAD (full text)
- Compute the delta that turns the new content back into the old HEAD
- Replace the existing HEAD block with that delta (it stops being full text, becomes a delta)
- Put the new content after it as full text — this is the new HEAD
So every commit changes two things: the last block, and a new one after it. Everything before that stays byte-identical. There's never a full-history rewrite.
On a store opened by path, that result is installed atomically: the
unchanged prefix is copied into a temp file beside the original, the new tail
is appended, and os.replace() swaps it in. Nothing live is overwritten, so
an interrupted commit costs the commit rather than the history — which matters
with reverse deltas, where every older version is anchored on HEAD and a
half-written HEAD takes all of them down at once. A BytesIO or a
caller-supplied stream has nothing to rename over and keeps the in-place
rewrite.
The byte-level details live in Storage Format; the reasoning and measurements are in Plan Atomic Commit.
Pass commit() a str and it uses an RCS-style line delta (the diff -n
family). Pass it bytes and it uses a BSDIFF40-compatible binary patch.
Callers just call the same method either way — the type alone decides
which delta algorithm runs underneath. Images, PDFs, archives, whatever —
they can all get history stored in the same .srcs format.
Starting with format v2, every block carries a hash of its own logical
content plus the hash of the block before it. Same basic idea as Git's
commit chain — tamper with anything in the past and every hash after it
stops matching, which verify() catches. On top of that there's optional
GPG-based multi-signer signing and verification (simple_rcs_gpg).
The codebase ships several diff algorithms — greedy hash matching
(StreamSequenceMatcher), pure-Python Myers implementations, and
Cython-compiled Myers SES/DMP variants. But the commit()/checkout()
path only ever uses StreamSequenceMatcher — it doesn't guarantee the
shortest edit script, but it's fast. The rest exist for benchmarking and
comparison. Why it ended up this way, and what the tradeoffs actually look
like, is covered in Diff Engines.
| SimpleRCS | Classic RCS | Git | Plain DB snapshot | |
|---|---|---|---|---|
| Storage unit | One stream (file) | One file (*,v) |
Object DB + working tree | DB row |
| Delta direction | Reverse (HEAD=full) | Reverse (HEAD=full) | Snapshot (delta at pack time) | No delta |
| Content types | Text + binary, same format | Text only | Anything (blob) | Anything |
| Integrity | v2 hash chain + GPG | None | Content-addressable | Depends on DB |
| Update atomicity | temp + rename (O(δ)) |
temp + rename (O(file)) |
Immutable objects + rename | Transaction |
| HEAD lookup | O(1) | O(1) | O(1) | O(1) |
| History lookup | O(k) backward scan | O(N) forward scan | O(log N) | O(1) |
The full comparison table lives in the repository's README.md.
- Not a multi-file repository — one instance per file
- No branching or merging — history is always linear
- No built-in concurrent-edit conflict detection or file locking. If you're putting this behind something multi-user (a wiki, say), you have to build that layer yourself — see Wiki Backend Design (Korean only) for what that actually involves.