OXYGENHUB
# 570e35310f31 oxygen / docs / lazy-clone.md
8.7 KiB · Markdown · last changed A shallow clone of a big repository works, and quickly (closes #137) 11 days ago renderedsource blame history raw
1
# Instant clone: a lazy working copy (oxygen#132)
2
3
Status: design, 2026-10-25. Nothing here is built yet beyond what it names
4
as existing. An experimental `oxy clone --lazy` (Linux) follows if this
5
measures well (§9).
6
7
## 1. The aim
8
9
`oxy clone URL` of any repository, however big, gives a working copy in
10
about a second: `ls`, `cd`, editors and builds see every file at once, a
11
file's bytes arrive the first time something reads it, and `status`,
12
`diff`, `commit` and `push` work at once and cost what the edits cost, not
13
what the repository weighs. Git's partial clone and VFS for Git do part of
14
this (VFS needs Windows and a special server); Oxygen can do all of it
15
with what it already has, because every object is content addressed and
16
a state's tree is known without its file contents.
17
18
## 2. What exists
19
20
- **Shallow clones** (spec §17): `oxy clone --depth 0` takes a checkpoint
21
  of the bookmark's heads and no history. The `checkpoint` message brings
22
  the checkpoint, its payload blob and **every file's graph blob** (spec
23
  §11, payload version 2; `Repo::checkpoint_bundle_for`,
24
  crates/oxy-core/src/repo/checkpoint.rs). A file graph holds the file's
25
  current text and its tombstones, so this is the whole state's text and
26
  more: for VS Code most of the clone's bytes.
27
- **Lazy blobs** (spec §13.5, §17): `--max-blob N` leaves opaque blobs
28
  larger than N to be fetched with `objects` on first read; `status` and
29
  `commit` never need them.
30
- **Sparse filters** (`--sparse PATH`): only those paths are materialized.
31
- **flow-casfs** (OxygenFlow, `crates/flow-casfs`): a read-only FUSE mount
32
  of a state from a local repository. The tree comes from the state
33
  index; a text file is rendered from its graph on first read or stat and
34
  cached on disk by graph key; an opaque file is its blob.
35
- **Content addressing everywhere**: an object fetched later is checked
36
  against the hash that named it, so a lazily fetched byte is as
37
  trustworthy as one cloned eagerly (spec §13).
38
39
## 3. The design
40
41
`oxy clone --lazy URL DIR`:
42
43
1. **Refs and the checkpoint skeleton.** Fetch `refs` (with the bookmark's
44
   attestation, checked against the policy as any clone does), then the
45
   checkpoint of its heads **without its graph blobs**: a `checkpoint`
46
   request with a new `graphs: false` field (additive; an older server
47
   ignores it and sends them, so the clone is merely not lazy). The
48
   payload's `files` entries name each graph blob by ObjectRef (graphs
49
   under 2 KiB are inline and come anyway), and its `tree` gives every
50
   path, mode and node. Clone writes the repository as a shallow one at
51
   depth 0 whose missing graph blobs are **lazy objects**, like lazy blobs
52
   today.
53
2. **The mount.** DIR is a FUSE mount (lower layer) of that state, built on
54
   flow-casfs: the directory tree is answered from the payload's tree at
55
   once; `stat` of a text file needs its length, so the payload gains
56
   (payload version 3, writer-optional) each file's rendered length, and
57
   until a server writes it the length comes with the graph on first
58
   `stat`. A read of a file whose graph is not stored fetches it with
59
   `objects` (hash checked), renders it, keeps both, and answers. Opaque
60
   files fetch their blob the same way. A small daemon (`oxy mount`,
61
   started by the clone, restarted by any oxy command that finds it gone)
62
   serves the mount; the repository itself stays in `DIR/.oxy`, which is
63
   a real directory beside the mount (bind or an overlay entry), so oxy
64
   never reads through FUSE.
65
3. **The writable layer.** Writes go to an upper directory
66
   (`.oxy/overlay/`): a written file is copied up whole on first write
67
   (its bytes fetched first if they were never read), a new file is
68
   created there, a removal is a whiteout, a rename is a whiteout plus a
69
   copy up. This is the ordinary overlay model, done in the daemon so it
70
   needs no privileges (kernel overlayfs over FUSE needs a user namespace
71
   and is not available everywhere).
72
4. **Status, diff and commit read the overlay only.** The upper layer is
73
   exactly the set of paths that may differ from the base state, so
74
   `status` lists it (comparing each copied-up file's bytes with the
75
   base rendering, which is stored, since copying up read it) and never
76
   walks the tree. `diff` and `commit` do the same, and the changes they
77
   build need only the touched files' graphs, all stored by then.
78
   `commit` clears the overlay entries it recorded: the mount's base
79
   state moves to the new heads.
80
5. **Fetch, pull, checkout.** A fetch brings changes as today (their
81
   blobs lazy). Moving the working copy to another state swaps the
82
   mount's base: the new state's tree is known from the changes and the
83
   stored checkpoint, the overlay is kept when its paths are unchanged
84
   between the two states (the `--carry` rule, §10.1), and graphs needed
85
   to render the new state are lazy again.
86
6. **Push.** As today: the pushed changes and the blobs they bring. A
87
   change built on a lazy base never needs the base's unread files.
88
89
## 4. Offline and failures
90
91
A file read once is kept, so it reads offline. Reading a file never read
92
while the hub cannot be reached fails with `EIO`, and the daemon logs the
93
path and the reason; `oxy status` says how many files are not local yet.
94
`oxy lazy fetch [PATH..]` brings them in ahead (a directory, the whole
95
state, or the files a build reads, as a background job), and `oxy lazy
96
materialize` turns the working copy into an ordinary one (unmounted, every
97
file written), after which nothing is lazy. A hash that does not match is
98
an error for that read, never a wrong byte.
99
100
## 5. Without FUSE
101
102
Where FUSE is not available (macOS without macFUSE, Windows, a container
103
without /dev/fuse), `--lazy` falls back to what exists: a depth-0 clone
104
with lazy blobs, and `--sparse` paths when given, saying so. Windows
105
ProjFS and macOS File Provider are later backends behind the same daemon
106
interface (lookup, read, write-through to the upper layer).
107
108
## 6. The protocol
109
110
- `checkpoint` gains `graphs: false`: leave out graph blobs (and their
111
  chunks) the payload names by reference.
112
- `objects` already fetches any object by hash, with a blob's chunks; it
113
  needs batching limits only (as `push.batch-bytes` bounds pushes).
114
- Optional later: `render` by graph key (the rendered bytes rather than
115
  the graph with its tombstones), smaller for long-lived files; a client
116
  checks it by rendering the graph when it has it, so it is an
117
  optimization a client may refuse.
118
- A host should keep `objects` cheap: it is now on the read path of
119
  editors and builds. OxygenHub's state cache and bucket stores (Strata)
120
  already serve objects by hash.
121
122
## 7. Trust
123
124
Nothing is weaker than an eager clone. The clone checks the bookmark's
125
attestation and the checkpoint against its heads (a checkpoint is valid
126
only if it reproduces from its heads, spec §11, which a lazy client cannot
127
check without the graphs: it trusts the attested heads and checks each
128
graph it fetches against the checkpoint's reference). `oxy verify` on a
129
lazy clone checks what is stored and says how much is not.
130
131
## 8. Not in scope
132
133
History (`log`, `blame` beyond the checkpoint) on a lazy clone fetches
134
changes on demand later (`oxy deepen` exists); the first version answers
135
`log` from the checkpoint's change list as a shallow clone does.
136
137
## 9. Measurements
138
139
On Earth, with the VS Code history (`~/bench/vscode-full9.oxy`, 166k
140
changes, served by `oxy serve` on the LAN so the network is the same for
141
every case), each timed cold (`echo 3 > drop_caches` not available: a
142
fresh clone directory each time, and the server warm), median of three:
143
144
| Step | git clone | oxy clone | oxy clone --depth 0 | oxy clone --lazy |
145
|---|---|---|---|---|
146
| clone | | | | |
147
| `ls -R \| wc -l` | | | | |
148
| read one file (`src/vs/code/electron-main/main.ts`) | | | | |
149
| edit it, `status` | | | | |
150
| `diff` | | | | |
151
| `commit` | | | | |
152
| `push` (to a fresh bare repo) | | | | |
153
| bytes over the wire | | | | |
154
| disk after | | | | |
155
156
Git is `git clone --no-local` from a `git daemon` of `~/bench/vscode.git`
157
(and `--filter=blob:none` as a second row, Git's own lazy clone). The
158
first three oxy columns can be measured now; they set the bar the
159
prototype must clear: the lazy clone should finish in seconds and its
160
first `status` in milliseconds, and nothing else may get slower.
161
162
## 10. The prototype (L2)
163
164
1. `checkpoint` with `graphs: false` in oxy-core and oxy-server, and a
165
   repository flag for lazy graph objects (fetch on load, like lazy blobs).
166
2. `oxy mount` (Linux, `fuser`, as flow-casfs) with the upper layer, in a
167
   new crate `oxy-mount` beside the CLI, behind a cargo feature so builds
168
   without FUSE are unaffected.
169
3. `status`/`diff`/`commit` from the overlay when the working copy is
170
   mounted.
171
4. `oxy clone --lazy`, `oxy lazy fetch`, `oxy lazy materialize`.
172
5. Measured as in §9; numbers on oxygen#132.