This document specifies the FVS repository format and the guarantees that come
with it. The current format is 3. Everything under .fvs2/ is covered;
anything not described here is an implementation detail.
<repo>/.fvs2/
config.json repo configuration (format version, chunking parameters)
index.json ordered list of states (id, time, message)
HEAD.json current head: a branch name or a detached state id
refs/heads/<name> one file per branch, containing a state id
commits/<id>.json one document per state
blocks/<blake3> content-addressed blocks, named by their BLAKE3-256 hex
lock advisory lock file (flock) for mutating operations
{
"format": 2,
"block_size": 4096,
"chunking": { "min_size": 4096, "avg_size": 16384, "max_size": 65536 }
}formatis the repo format version. Absent means 1. Readers must refuse formats newer than they support.- Format 1 repos chunk files into fixed
block_sizeblocks. Format 2 repos use content-defined chunking with thechunkingparameters.
Files are split with a FastCDC-style gear rolling hash with normalized
chunking. The 256-entry gear table is generated with splitmix64 from the fixed
seed 0x3779b97f4a7c15f6 and is part of the format: it determines every
chunk boundary, so it can never change within format 2. The same applies to a
repo's chunking parameters: changing them re-chunks everything and defeats
dedup, so they are fixed at init.
min == avg == max degenerates to fixed-size chunking, which is how format 1
repos are processed by the same code path.
Any parameter that influences chunk boundaries is part of the format,
versioned, and recorded in the state document (chunking_policy). Policy 0
uses the repository parameters for every file. Policy 1 (default for format
3 repositories) sniffs the first 8 KiB of each file: content with no NUL
byte and at least 70% printable bytes chunks with a fine text target
(min 1 KiB, avg 4 KiB, max 16 KiB); everything else keeps the repository
parameters. The rule depends only on content, never on the file name, so
identical content chunks identically everywhere and dedup survives renames.
A file that changes classification between versions re-chunks from scratch
for that transition; determinism is worth that rare, single-file cost.
Blocks are identified by the BLAKE3 of their uncompressed content, but stored zstd-compressed when that wins (readers sniff the zstd magic, so raw legacy blocks stay readable and incompressible content stays raw). Ids, dedup and the remote protocol never see compression: it is purely a storage detail.
Format 3 moves a state's file list out of the commit document and into
tree objects: one content-addressed object per directory, stored in the
same block store as file content. A tree object is a JSON array of entries
(n name, k kind: f/d/l, m mode, s size, t mtime, b blocks,
z block sizes, l link target, d child tree id), sorted by name. An
unchanged directory hashes to the same object across states, so metadata
deduplicates exactly like content, and gc and sync treat tree objects as
ordinary blocks. The commit document keeps only root_tree, file_count and
total_size.
Format 2 repositories (inline files in the commit document) remain fully
readable and can still be created with fvs2 init --format 2 for consumers
that pin older tooling.
A state (commit) document:
{
"id": "<blake3 of time, message and file list>",
"format": 2,
"time_utc": 1783689683,
"message": "before installer",
"block_size": 4096,
"files": [
{
"path": "drive_c/game.exe",
"mode": 493,
"size": 123456,
"mod_time": 1783689600,
"blocks": ["<blake3>", "..."],
"block_sizes": [16384, 8123]
},
{ "path": "link", "mode": 511, "mod_time": 1783689600, "link": "target" }
]
}blockslists the chunks of the file in order;block_sizestheir byte lengths (present in format 2, absent in format 1 where offsets derive from the fixedblock_size).- Symlinks carry
linkand no blocks. Empty files carry an empty block list. - States are independent: there are no parent links, and any state can be
deleted (
drop) without affecting the others.
- Integrity on read. Blocks are content-addressed; every read re-hashes and fails with a corruption error instead of returning bad data.
- Crash safety. Metadata is written with temp-file + fsync + atomic rename
- directory fsync; blocks are synced before becoming visible. Operations
order writes so that a crash at any point leaves at most orphans (a block
or commit document nothing references), never a reference to missing data.
Orphans are reclaimed by
gc, which is itself safe to re-run.
- directory fsync; blocks are synced before becoming visible. Operations
order writes so that a crash at any point leaves at most orphans (a block
or commit document nothing references), never a reference to missing data.
Orphans are reclaimed by
- Concurrency. Mutating operations (commit, drop, gc) take an exclusive advisory lock; readers are lock-free and always observe a consistent state.
- Compatibility. Format 2 readers read format 1 repos. Unknown newer formats are rejected with an explicit error, never misread. Within format 2, chunk boundaries are stable forever (fixed gear table and per-repo parameters), so blocks written today keep deduplicating against blocks written years from now.
- Commit documents are uncompressed JSON; metadata size grows with file count (see the benchmarks). Packing metadata is planned and will be a format bump.
- The store has no refcounting; space is reclaimed only by
gc(which, on packed repositories, rewrites the store around the live set: see docs/PACK.md). - Nothing in the format is encrypted or authenticated beyond content hashes.