Mercurial manifests → Sapling tree manifests
The only flat, whole-tree answer to level 1 in this survey — and the documented story of why it had to become a tree.
| Ecosystem | Mercurial; Sapling (Meta's fork) |
| Level 1 | flat: one sorted text listing of every path in the repository; later, per-directory trees |
| Level 2 | per-file revlog identity |
| Node model | path + file revision id + one flag: x executable, l symlink (later t = tree) |
| Entry ordering | sorted rows of a text file |
| Digest | Sha1(min(p1, p2) + max(p1, p2) + content) — includes the parents |
| Documentation | 201910-manifests-past-and-future.md (Sapling) |
Overview
What it solves
Answering "what files exist at this commit" without walking history. Sapling's own retrospective defines it (201910-manifests-past-and-future.md):
The Manifest is the list of files in the repository at a given commit. […] Manifests can be seen as indexes, precomputed data, or derived data.
Design philosophy
The flat manifest is deliberately the simplest thing that works — "a big string that is associated with a commit identifier" — with an ABNF given in the note:
Manifest = *( Row LF )
Row = FilePath %x00 HgId [ Flag ]
Flag = %s"x" / %s"l"
FilePath = 1*( %x01-%x09 / %x0B-%xFF )
HgId = 40HEXDIGso a manifest looks like:
analytics/abtest.pig 06763db6de79098e8cdf14726ca506fdf16749bex
assets/banner.jpg 5ac863e17c7035f1d11828d848fb2ca450d89794
index.php 78f7c038716f258f451528e1e8241d895419f2eeTwo things are unusual against everything else in this survey.
There is no directory structure at all. Paths are full paths; analytics/ is not an object. So the manifest is a flat level 1 — closer to a sorted listing than to a tree — and the whole listing is one revlog entry.
The identity is not a pure content address. The note gives it as:
manifest_id = Sha1(min(p1, p2) + max(p1, p2) + content)where p1/p2 are the parent manifest ids. Two commits with byte-identical file listings and different histories therefore have different manifest ids — the opposite of git's property that identical trees share an object. This is the survey's only history-dependent identity, and it is worth naming precisely because it looks like content addressing and is not.
The transition, and why
Flat manifests are stored as revlog deltas, which makes them compact; what they are not is scalable to read. The note is blunt about the reason:
Even if Revlog can have indexes on the index file, the Manifest itself is not efficient. Parsing the contents of a flat Manifest is going to be expensive. For large code bases, we can assume that commits will touch a relatively small number of files so we would like a data structure that scales with O(|changed files|) rather than O(|repo files|).
and states the resulting bound:
Modeling the Manifest as a Tree will reduce scaling of the operations to O(|changed files| _ |directory size| _ |directory depth|). Assuming that
directory sizeanddirectory depthare capped, the operations are going to be O(|changed files|).
That is the clearest available statement of why every other level-1 scheme in this survey is per-directory. Git, OSTree and REAPI all start there; Mercurial is the one that started elsewhere and had to move, which makes its retrospective the evidence rather than the assumption.
The migration is also instructive about compatibility: the tree format was bootstrapped from the flat one — "A Flat Manifest that has one level can be seen as a Tree Manifest node" — by adding a third flag, t, marking a row as a subdirectory rather than a file. The node model grew by one character.
The note is candid about the storage consequence, too: because Mercurial gives each path its own revlog, tree manifests meant a revlog per directory node — "Personally, I don't like the idea having O(|files|) revlog entries. I find this solution unfortunate."
Dimensions
- Node model — the leanest in the survey: path, revision id, one flag. No mode bits beyond executable, no ownership, no timestamps.
- Ordering — sorted rows; with full paths in a flat manifest, the file-versus-directory ordering question git answers with an implicit
/cannot arise, because directories do not appear. It returns with tree manifests. - Level 1 — flat listing, then per-directory tree. The only subject to have shipped both.
- Level 2 — per-file revlog, delta-compressed; identity is a revision id, itself parent-dependent.
- Digest — SHA-1 over parents plus content: history-dependent.
- Partial verification — none for a flat manifest (it is one blob); per node once trees arrive.
Strengths
- Extremely simple format, trivially inspectable as text.
- Delta storage makes flat manifests compact across adjacent commits.
- The tree migration preserved the format's shape by adding one flag.
- The scaling rationale is written down by the people who lived it.
Weaknesses
O(|repo files|)to parse — the failure that forced the migration.- History-dependent identity: identical trees do not share a manifest id, so the dedup every Merkle scheme relies on is unavailable.
- A revlog per directory node once trees land —
O(|files|)storage entries, which the note itself criticizes. - Sorted text with NUL separators is a parsing surface with edge cases.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off | | ------------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------ | ---------- | ----------------------------------------- | | Flat, whole-repository listing | Simplest possible manifest; excellent delta compression | O( | repo files | ) parse cost; forced a redesign at scale | | Identity includes parent ids | Fits Mercurial's revlog/DAG model, where everything has parents | Identical trees get different ids — no cross-history dedup | | One flag character for the node model | Minimal format; easy to extend | Executable and symlink only; everything else is out of scope | | Bootstrap trees from the flat format, adding t | Reuses the parser and storage machinery | Inherits revlog-per-path storage, i.e. O( | files | ) entries | | Text, not binary | Debuggable, greppable | Parsing cost is exactly the thing that did not scale |
Sources
eden/scm/newdoc/notes/201910-manifests-past-and-future.md— the ABNF, the manifest-id formula, theO(|changed files|)argument, thetflageden/scm/lib/manifest-treeandmanifest-augmented-tree— the Rust implementations