|
|
… |
1 unchanged lines |
| 2 |
2 |
|
|
| 3 |
3 |
|
| | | |
| 4 |
4 |
|
|---|---| |
| 5 |
|
- |
| **Status** | Draft 0.7 | |
|
5 |
+ |
| **Status** | Draft 0.8 | |
| 6 |
6 |
|
| **Format version** | 1 | |
| 7 |
7 |
|
| **Date** | 2026-09-26 | |
| 8 |
8 |
|
| **Audience** | Implementers, security reviewers, and contributors | |
|
|
… |
171 unchanged lines |
| 180 |
180 |
|
|---|---| |
| 181 |
181 |
|
| `TreeOp` | Map with key 0 = variant tag: 0 `AddNode`, 1 `RemoveNode`, 2 `Move`, 3 `SetMode`; key 1 = node id; `AddNode` adds 2 kind, 3 mode, 4 parent, 5 name; `RemoveNode` adds 2 placement ref; `Move` adds 2 old placement ref, 4 parent, 5 name; `SetMode` adds 2 old mode ref, 3 mode. | |
| 182 |
182 |
|
| `Edit` | Map with key 0 = variant tag: 0 `Insert`, 1 `Delete`, 2 `Replace`; key 1 = node id; `Insert` adds 2 after, 3 before, 4 lines (array of byte strings) or 5 `lines_blob`; `Delete` adds 2 atom ranges; `Replace` adds 2 old (bytes or null), 3 new (bytes or null). | |
| 183 |
|
- |
| `NodeKind` | Unsigned integer: 0 file, 1 dir, 2 symlink. | |
|
183 |
+ |
| `NodeKind` | Unsigned integer: 0 file, 1 dir, 2 symlink, 3 subrepo (§7.8). | |
|
184 |
+ |
| Subrepo pin | The opaque content of a `subrepo` node: a CBOR map `{0: 1, 1: kind, 2: locator, 3: pin, 4: bookmark}` where `kind` is 0 for an Oxygen repository (`locator` = repository id bytes(16), `pin` = a state object's heads as `[ChangeHash]`) or 1 for a Git repository (`locator` = null, `pin` = commit sha as text); `bookmark` is the branch or bookmark name the pin was taken from. | |
| 184 |
185 |
|
| `Mode` | Unsigned integer bit field: bit 0 executable, bit 1 opaque content mode. Directories carry 0. | |
| 185 |
186 |
|
| `OpRef` (placement or mode reference) | `[bytes(32) change hash, uint index]`; the all-zero hash means this change. | |
| 186 |
187 |
|
| `AtomRef` | `0` for `START`, `1` for `END`, otherwise `[bytes(32) change hash, uint index]` with the all-zero hash meaning this change. | |
|
|
… |
44 unchanged lines |
| 231 |
232 |
|
|
| 232 |
233 |
|
### 7.1 Tree model |
| 233 |
234 |
|
|
| 234 |
|
- |
The repository tree is a set of nodes. Each node has a **NodeId**, a **kind** (`file`, `dir`, `symlink`), a **mode**, and zero or more **placements**. The mode of a file is its executable bit and its **content mode**, `text` or `opaque` (§7.5); a symlink's content is its target path and its content mode is always `opaque`; a directory has no mode. The root directory has the fixed NodeId of 16 zero bytes and no placement. |
|
235 |
+ |
The repository tree is a set of nodes. Each node has a **NodeId**, a **kind** (`file`, `dir`, `symlink`, `subrepo`, §7.8), a **mode**, and zero or more **placements**. The mode of a file is its executable bit and its **content mode**, `text` or `opaque` (§7.5); a symlink's content is its target path and its content mode is always `opaque`; a directory has no mode. The root directory has the fixed NodeId of 16 zero bytes and no placement. |
| 235 |
236 |
|
|
| 236 |
237 |
|
A placement is `(parent NodeId, name)`. Names are byte strings that MUST be valid UTF-8, MUST NOT be empty, `.`, or `..`, and MUST NOT contain `/` or NUL. Placements that a particular file system cannot represent distinctly, such as names differing only by case, are workspace conflicts (§9.7), not errors in the state. |
| 237 |
238 |
|
|
|
|
… |
55 unchanged lines |
| 293 |
294 |
|
- `.oxyignore` and `.oxyattributes` files that are symlinks MUST be treated as absent, with a warning from `oxy status`. |
| 294 |
295 |
|
- On file systems without symlink support, a symlink node is written as a plain file containing the target path, and `oxy status` reports it; snapshot MUST NOT convert such a file back into a content change. |
| 295 |
296 |
|
|
|
297 |
+ |
### 7.8 Subrepositories |
|
298 |
+ |
|
|
299 |
+ |
A **subrepo** node embeds another repository at a path, as Git's submodules do. Its content is a **pin**: which repository, and which state of it (§5.5, "Subrepo pin"). The pin is stored and versioned exactly like an opaque file, as a blob replaced by `Replace` edits, so two changes that move the same subrepo to different states produce an ordinary replace conflict (§9.3) that `oxy status` lists and a resolution settles. A subrepo has no placements of its own inside the parent beyond its one entry; its files belong to the nested repository and are never atoms of the parent. |
|
300 |
+ |
|
|
301 |
+ |
Two pin kinds exist. An **Oxygen pin** names a repository id and a state (a sorted set of heads), and is verifiable: the nested repository's changes are fetched from its own remotes and checked under its own policy. A **Git pin** names a commit hash and is what `oxy git import` produces for a Git submodule (§23); it is checked out with Git and never converted silently. |
|
302 |
+ |
|
|
303 |
+ |
The mapping from path to remote lives in `.oxymodules` at the repository root, a tracked file of `[path]` sections with `url` and optional `bookmark` (Oxygen) or `branch` (Git) keys, edited by `oxy submodule add|remove` and readable by hand. A pin without a `.oxymodules` entry is checked out only when the nested repository is already present. |
|
304 |
+ |
|
|
305 |
+ |
In the working copy a subrepo is a directory holding its own `.oxy/` (or `.git/`). The snapshot MUST NOT descend into it; it reads the nested repository's current bookmark heads (or Git `HEAD`) as the pin, so committing the parent records whatever the nested repository is at, as in Git. `oxy checkout` of the parent MUST move an existing nested repository to the pinned state (`oxy checkout` of those heads, or `git checkout --detach <sha>`), fetching from the recorded remote when objects are missing, and MUST leave an absent nested repository absent until `oxy submodule update --init` clones it. A nested working copy with uncommitted changes is never overwritten: the checkout reports it and continues. |
|
306 |
+ |
|
|
307 |
+ |
`oxy submodule add URL PATH [--bookmark NAME]` clones URL into PATH, records `.oxymodules`, and adds the node pinned at the clone's heads. `oxy submodule update [--init] [PATH..]` brings nested repositories to their pins. `oxy submodule status` lists each subrepo with its pin and whether the nested repository matches it. `oxy submodule remove PATH` removes the node and the `.oxymodules` entry; the directory is left in place. |
|
308 |
+ |
|
| 296 |
309 |
|
## 8. Dependencies and commutation |
| 297 |
310 |
|
|
| 298 |
311 |
|
### 8.1 Definition |
|
|
… |
20 unchanged lines |
| 319 |
332 |
|
|
| 320 |
333 |
|
Rewriting a change `C` to `C'` (amend, rebase, split, squash) produces a new object with the same ChangeId. Amending an older change with the working copy's edits (`oxy amend --into C`) is allowed when every atom, placement, and node those edits reference exists in `C`'s closure; the edits are appended to `C` with references to `C` turned into self references, so `C`'s own atoms keep their indices and dependents are rewritten with an identity correspondence. Splitting a head (`oxy split C PATH..`) rebuilds it as a first change holding its effect on the named paths and a second holding the rest under the original ChangeId. Any change `D` with `hash(C) ∈ deps(D)` is orphaned relative to `C'`. `oxy` MUST automatically rewrite orphaned descendants by translating their AtomRefs and PlacementRefs from `C` to `C'` where a correspondence exists. Where none exists the reference is **reified by re-anchoring**: an insert anchored on a vanished atom is anchored instead on the nearest surviving atom of the same insert chain of `C` (the preceding one for `after`, the following one for `before`), or on that chain's own anchors when none survives, so that an order conflict (§9.1) arises exactly when `C'` placed other content there; a deletion of a vanished atom is dropped; an operation or edit on a node that vanished with `C` is dropped. Each adjustment MUST be reported to the user. Removing `C` altogether rewires each dependent to depend on `deps(C)` and reifies references into `C` the same way. Changes that do not depend on `C` are untouched, and a rewritten change by another author is stored without that author's signature, which only they can renew. Rewriting is recorded in the operation log (§12) and is undoable. |
| 321 |
334 |
|
|
|
335 |
+ |
### 8.5 Folding and re-basing |
|
336 |
+ |
|
|
337 |
+ |
Changes that commute never need re-basing to be combined, so `oxy rebase` and `oxy squash` are history-editing tools, not merge tools. |
|
338 |
+ |
|
|
339 |
+ |
**Squash.** `oxy squash C --into D` folds change `C` into `D`, where `D` is a dependency of `C` and every atom, placement, and mode that `C` refers to in `D` is one `D` introduces or one both already know. The result `D'` carries `D`'s operations followed by `C`'s, with `C`'s references to `D` turned into self-references, keeps `D`'s ChangeId, and is signed afresh by its author; `C` is removed from the state and every change that depended on `C` or `D` is rewritten with the correspondence of §8.4. A `C` that depends on changes `D` does not depend on cannot be squashed into `D`; the command names them. `oxy amend --into` is the special case where `C` is the working copy. |
|
340 |
+ |
|
|
341 |
+ |
**Rebase.** `oxy rebase C --onto HEADS` re-expresses `C` against a different base: the files `C` touches are materialized twice, from `closure(deps(C))` and from `closure(deps(C)) ∪ {C}`, the difference is re-derived as edits against the state `HEADS` names, and the new change keeps `C`'s ChangeId with dependencies computed from what it now references (plus `HEADS` as explicit dependencies). This is the move a Git user makes with `rebase --onto`: it changes what a change is said to build on. It is refused when `HEADS` conflicts in any file `C` touches, since the re-derivation would have no single text to diff against, and when the result would be empty. Dependents of `C` are rewritten per §8.4. Because commuting changes already coexist, the common reason to rebase is to drop an accidental dependency, for instance on a change that is to be abandoned. |
|
342 |
+ |
|
| 322 |
343 |
|
## 9. Conflicts |
| 323 |
344 |
|
|
| 324 |
345 |
|
### 9.1 Text conflicts |
|
|
… |
73 unchanged lines |
| 398 |
419 |
|
State { version [0]: 1, heads [1]: [ChangeHash] } // sorted, unique; may be empty |
| 399 |
420 |
|
``` |
| 400 |
421 |
|
|
|
422 |
+ |
A repository holds any number of bookmarks. Each workspace has a **current bookmark** (`oxy bookmark switch NAME`, recorded per workspace) that its working copy sits on and that `commit`, `log`, `push`, and `pull` act on by default. `oxy bookmark create NAME [CHANGE..]` names a new state (default: the current working copy's base heads), `set NAME CHANGE..` moves one, `delete NAME` removes it (its changes stay until unreachable, §21), `list` shows all with their heads and the remote-tracking difference, and `oxy merge NAME` unions the named bookmark's heads into the current one, which is the whole of merging in this model: no merge change, conflicts reified (§9). `oxy checkout NAME` materializes a bookmark without switching to it. Bookmark names are resolved before change prefixes wherever a change is expected. |
|
423 |
+ |
|
| 401 |
424 |
|
Every reference, attestation, view, and checkpoint therefore names a state by one ObjectRef however many heads it has. Multiple heads are normal in this model: every independent change is a head until something depends on it, so a busy bookmark may have dozens. That is not a conflict in the §9 sense; `oxy log` shows the heads as parallel, and a state object keeps the cost of naming them constant for everything that refers to the bookmark. The empty state, `State { heads: [] }`, is what a new bookmark points at. |
| 402 |
425 |
|
|
| 403 |
426 |
|
### 10.2 Reference attestations (G2, G3) |
|
|
… |
57 unchanged lines |
| 461 |
484 |
|
- The working-copy change is unsigned and never pushed while `kind = working-copy`. |
| 462 |
485 |
|
- `oxy commit` (alias `oxy describe`) sets the message, sets `kind = normal`, and starts a fresh working-copy change on top of it. The change is signed immediately if `core.sign = "change"`; with the default `core.sign = "seal"` it is left unsigned locally and covered by a seal at push time (§14.4). |
| 463 |
486 |
|
- `oxy new`, `oxy edit <change>`, and `oxy abandon` move which change the workspace edits. |
| 464 |
|
- |
- There is no staging area and no stash. Partial commits are made with `oxy split`. |
|
487 |
+ |
- There is no staging area and no stash. A partial commit is a split of the working copy: `oxy commit PATH..` records only the named paths (and every edit of theirs) and leaves the rest as uncommitted work; `oxy commit --interactive` offers each hunk (one `Insert` or `Delete` edit on one file, or one tree operation) for inclusion, like `git add -p`; `oxy split` does the same to an existing change. A hunk that another selected hunk refers to is selected with it, so the committed change is always well-formed (§8.3). |
| 465 |
488 |
|
|
| 466 |
489 |
|
### 12.4 Snapshot performance |
| 467 |
490 |
|
|
|
|
… |
316 unchanged lines |
| 784 |
807 |
|
- **Import.** Each Git commit becomes one change whose edits are the diff between the union of its parents' states and its tree, so a merge commit becomes a resolution exactly when the parents conflict in the change model, and an ordinary change otherwise. The commit's author and hash are recorded as `Git-Author` and `Git-Commit` trailers; the author key is the zero KeyId, since nobody can sign on the original author's behalf. Submodule entries are skipped. Renames are detected on import exactly as for a snapshot (§7.1, similarity ≥ 50%), then tracked exactly. The Git commit's `Change-Id` trailer, if present and 128 bits, becomes the ChangeId; otherwise one is derived as BLAKE3-128 of the Git commit hash, so re-import is idempotent. Note that a change's own closure is what it depends on, not the Git snapshot: the imported bookmark's state equals the Git tree, but checking out an individual imported change yields only the files it and its dependencies touch. |
| 785 |
808 |
|
- **Export.** Each state along a bookmark becomes a Git commit whose tree is the materialized state. The ChangeId is written as a `Change-Id:` trailer and the ChangeHash as `Oxy-Hash:`. Conflicted states are exported with §9.5 markers and an `Oxy-Conflicted: true` trailer. Signatures are not exported; Git commits MAY be additionally signed with the Ed25519 key via SSH signature format. |
| 786 |
809 |
|
- **Fetch and push against a Git remote.** `oxy git fetch URL` keeps a bare mirror under `.oxy/git/` for the remote, fetches the branch into it, and imports the commits the bookmark does not have yet, seeding the import with the commits already known: an imported commit maps to its change, and a commit this repository exported maps to the bookmark state it was made from (so a Git commit on top of exported work is based on everything that work contained, including changes Git never saw). The bookmark moves to the union of its heads and the new changes, like a pull; the working copy follows when it was clean. A fetched state whose previous state was itself a Git tree is remembered as equal to the fetched tip. `oxy git push URL` refuses until the remote's tip is known, exports the bookmark's later states as commits chained onto that tip, and pushes the branch, so the push is always a fast-forward. |
|
810 |
+ |
- **Submodules.** A Git gitlink entry (mode `160000`) imports as a `subrepo` node with a Git pin (§7.8) and the matching `.gitmodules` entry becomes a `.oxymodules` entry with `branch` set to the submodule's configured branch when there is one. Export writes a Git pin back as a gitlink and `.oxymodules` back as `.gitmodules`; an Oxygen pin has no Git form and is exported as a gitlink only when the nested repository has a Git mirror commit for that state (§23, fetch and push), otherwise as an empty directory with a warning. |
| 787 |
811 |
|
- **Round trip.** Import followed by export MUST reproduce every tree hash. Export followed by import MUST reproduce every ChangeId. |
| 788 |
812 |
|
|
| 789 |
813 |
|
## 24. Command-line interface |
|
|
… |
9 unchanged lines |
| 799 |
823 |
|
oxy split CHANGE [PATH].. oxy abandon CHANGE oxy resolve |
| 800 |
824 |
|
oxy checkout [CHANGE].. [--discard] oxy undo oxy op log |
| 801 |
825 |
|
oxy tag create NAME [CHANGE].. [-m MSG] oxy tag list |
|
826 |
+ |
oxy bookmark create|set|delete|list|switch oxy merge BOOKMARK |
|
827 |
+ |
oxy commit [PATH].. [--interactive] oxy squash CHANGE --into DEST oxy rebase CHANGE --onto HEADS.. |
|
828 |
+ |
oxy submodule add|update|status|remove oxy blame PATH oxy bisect start|good|bad|run|reset |
| 802 |
829 |
|
oxy workspace add|list|forget oxy sparse [PATH].. oxy deepen [N] |
| 803 |
830 |
|
oxy fetch [REMOTE] oxy pull [REMOTE] oxy push [REMOTE] [--force] |
| 804 |
831 |
|
oxy remote add|remove|list oxy serve oxy lock|unlock PATH |
|
|
… |
5 unchanged lines |
| 810 |
837 |
|
|
| 811 |
838 |
|
Change arguments accept a ChangeId prefix, a ChangeHash prefix (with `#`), a bookmark or tag name, and `@` for the working-copy change. Ambiguous prefixes are an error. |
| 812 |
839 |
|
|
| 813 |
|
- |
Reserved for a later version, with these names: `oxy new`, `oxy edit`, `oxy squash`, `oxy rebase` (commuting changes need no rebase; the command would re-express a change against different dependencies), `oxy redo`, revsets (`log -r`), `oxy bookmark create|set|delete|list` (one bookmark per workspace today), and `oxy remote trust` (with the Noise transport, §15). |
|
840 |
+ |
Reserved for a later version, with these names: `oxy new`, `oxy edit`, `oxy redo`, revsets (`log -r`), and `oxy remote trust` (with the Noise transport, §15). |
| 814 |
841 |
|
|
| 815 |
842 |
|
## 25. Performance targets |
| 816 |
843 |
|
|
|
|
… |
47 unchanged lines |
| 864 |
891 |
|
| Git interoperability (§23) | Implemented: import, export, fetch, push, rename detection | |
| 865 |
892 |
|
| Conformance suite (§26) | Partial: encoding, signature, permutation, conflict, rewrite, protocol, chunking and Git tests exist as unit and integration tests in the crates; a standalone vector suite is not published yet | |
| 866 |
893 |
|
|
| 867 |
|
- |
## 27. Open questions |
|
894 |
+ |
## 27. Inspection: blame and bisect |
|
895 |
+ |
|
|
896 |
+ |
**Blame.** Every line of a file in a state is an atom identified by the change that introduced it (§7.3), so `oxy blame PATH` is a lookup, not a heuristic: for each line it prints the introducing change's ChangeId, author, and date, and `--json` gives the ChangeHash and atom index. A line inside a conflict region is attributed to the side that introduced it. Blame follows renames for free, since the node survives them, and it MUST cost no more than materializing the file. |
|
897 |
+ |
|
|
898 |
+ |
**Bisect.** A regression is a change, and the candidates lie between a known-good state and a known-bad one: `candidates = closure(bad) \ closure(good)`. `oxy bisect start`, then `oxy bisect good CHANGE..` and `oxy bisect bad CHANGE..` (either may name a bookmark), orders the candidates so that every prefix is dependency-closed and checks out `good ∪ prefix` at the midpoint. Each further `good`/`bad` halves the remaining range, and the search ends by naming the first change whose inclusion makes the state bad; `oxy bisect run CMD` automates it with the command's exit status (0 good, 125 skip, anything else bad). `oxy bisect reset` restores the working copy from before the search. Because prefixes are closed under dependencies, every tested state is well-formed; a change that cannot be tested in isolation because it commutes with the culprit is simply ordered after it. Bisect state lives under `.oxy/bisect/` and is not versioned. |
|
899 |
+ |
|
|
900 |
+ |
## 28. Open questions |
| 868 |
901 |
|
|
| 869 |
902 |
|
1. **Encryption at rest.** Per-repository XChaCha20-Poly1305 with a key hierarchy is reserved for format version 2; object headers already leave room. Convergent encryption is rejected because it leaks equality. |
| 870 |
903 |
|
2. **Word- or character-granularity atoms** for prose and minified files. Line atoms are fixed for v1. |
|
|
… |
3 unchanged lines |
| 874 |
907 |
|
|
| 875 |
908 |
|
--- |
| 876 |
909 |
|
|
| 877 |
|
- |
*Changelog.* Draft 0.7 (2026-09-27): review pass. `oxy status --json` shape (§9.5); stat crawl required and file-system monitor recommended (§12.4); capabilities aligned with the implementation (§16.1); §24 lists the implemented command line and names the reserved commands; new §26.1 reference implementation status; §27 records the open workspace-conflict and lock items. |
|
910 |
+ |
*Changelog.* Draft 0.8 (2026-09-27): subrepositories with Oxygen and Git pins (§7.8, §23); squash and rebase defined as history edits (§8.5); multiple bookmarks per repository with a per-workspace current bookmark and `oxy merge` (§10.1); partial and interactive commits (§12.3); blame and bisect (§27); open questions moved to §28. |
|
911 |
+ |
|
|
912 |
+ |
Draft 0.7 (2026-09-27): review pass. `oxy status --json` shape (§9.5); stat crawl required and file-system monitor recommended (§12.4); capabilities aligned with the implementation (§16.1); §24 lists the implemented command line and names the reserved commands; new §26.1 reference implementation status; §28 records the open workspace-conflict and lock items. |
| 878 |
913 |
|
|
| 879 |
914 |
|
Draft 0.6s (2026-09-27): the manifest as a stat cache with a racy-stat window (§12.4). |
| 880 |
915 |
|
|
|
|
… |
53 unchanged lines |