Spec review pass (Draft 0.7): oxy status --json, section 24 aligned with the CLI, reference implementation status table, capabilities and snapshot wording aligned with the implementation 50a046e439e9
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-27 00:21 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
50a046e439e935ba6f7bd7fd745dcf2e566c526370df90a299e790a8e6b6bdfe- Depends on
- 135212de5370 20247a03a596 28fc23d7706a 30ea9c1e70fa 96f7610fa666 982864068a59 b7f102fcbb17 ee06a6c557b7 fce44ae9ad29 ff0af1ee299d
- Depended on by
- 91334b7fa574 4afe288b7946 0299076f09a2 b689779e9e74 f6dccc4533c8 2c9ddac3c387 3604179679eb d4dbab97e948 83d61e803ccd 6a81dbab471d 1c2bd9abdd4f 62794ecc7f74 d82e44097323 7aa2dc9369e3 05e71ed6cdf1 75167672ebcd e02bf0cd0533 c6aa5bd9d1c8 728da0f92161 957fee30826e 4dee277c327a fbda43e7d8ed 86fe29fd62ea 9ac1fbe211b4 694805cca585
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 27 atoms, 0 tree operations, 8 files
Browse the tree at this change
modified crates/oxy-core/src/repo/snapshot.rs +65 −0
| … | 179 unchanged lines |
||
| 180 | 180 | Ok(SnapshotResult { wc_hash: new_hash, wc, base, base_snapshot, statuses, changed, resolves, contributors }) |
|
| 181 | 181 | } |
|
| 182 | 182 | ||
| 183 | + | /// `oxy status --json` (spec §9.5): the changed paths and every conflict |
|
| 184 | + | /// of the working copy's base with its kind, path, and contributing |
|
| 185 | + | /// ChangeIds. |
|
| 186 | + | pub fn status_json(&self, snap: &SnapshotResult) -> Result<String> { |
|
| 187 | + | let heads = self.bookmark_heads()?; |
|
| 188 | + | let base_snapshot = &snap.base_snapshot; |
|
| 189 | + | let by_node = base_snapshot.tree.by_node(); |
|
| 190 | + | let path_of = |n: &NodeId| by_node.get(n).map(|e| e.path.clone()); |
|
| 191 | + | let id_of = |h: &ChangeHash| snap.base.get(h).map(|m| m.change_id.to_hex()).unwrap_or_else(|| h.to_hex()); |
|
| 192 | + | let paths = json_array(snap.statuses.iter().map(|s| format!("{{\"path\":{},\"status\":{}}}", json_str(&s.path), json_str(&format!("{:?}", s.status))))); |
|
| 193 | + | let mut conflicts: Vec<String> = Vec::new(); |
|
| 194 | + | for (p, f) in &base_snapshot.files { |
|
| 195 | + | if !f.is_conflicted() { |
|
| 196 | + | continue; |
|
| 197 | + | } |
|
| 198 | + | let (kind, changes) = match &f.content { |
|
| 199 | + | FileContent::Text { .. } => ("text", snap.base.conflict_regions(&f.node).contributors), |
|
| 200 | + | FileContent::Opaque { blobs } => ("replace", snap.base.blob_contributors(&f.node, blobs)), |
|
| 201 | + | FileContent::Dir => continue, |
|
| 202 | + | }; |
|
| 203 | + | let resolved = match snap.statuses.iter().find(|s| s.path == *p).map(|s| s.status) { |
|
| 204 | + | Some(ChangeStatus::Resolved) => "true", |
|
| 205 | + | Some(ChangeStatus::ConflictEdited) => "\"markers\"", |
|
| 206 | + | _ => "false", |
|
| 207 | + | }; |
|
| 208 | + | conflicts.push(format!( |
|
| 209 | + | "{{\"kind\":{},\"path\":{},\"node\":{},\"changes\":{},\"resolved\":{resolved}}}", |
|
| 210 | + | json_str(kind), |
|
| 211 | + | json_str(p), |
|
| 212 | + | json_str(&f.node.to_hex()), |
|
| 213 | + | json_array(changes.iter().map(|h| json_str(&id_of(h)))) |
|
| 214 | + | )); |
|
| 215 | + | } |
|
| 216 | + | for c in &base_snapshot.tree.conflicts { |
|
| 217 | + | let (kind, nodes): (&str, Vec<NodeId>) = match c { |
|
| 218 | + | crate::tree::TreeConflict::Name { nodes, .. } => ("name", nodes.clone()), |
|
| 219 | + | crate::tree::TreeConflict::MultiPlacement { node, .. } => ("multi-placement", vec![*node]), |
|
| 220 | + | crate::tree::TreeConflict::Orphan { node, .. } => ("orphan", vec![*node]), |
|
| 221 | + | crate::tree::TreeConflict::Cycle { node } => ("cycle", vec![*node]), |
|
| 222 | + | crate::tree::TreeConflict::Mode { node, .. } => ("mode", vec![*node]), |
|
| 223 | + | }; |
|
| 224 | + | let detail = match c { |
|
| 225 | + | crate::tree::TreeConflict::Name { name, parent, .. } => { |
|
| 226 | + | let dir = if *parent == NodeId::ROOT { String::new() } else { path_of(parent).unwrap_or_default() }; |
|
| 227 | + | format!(",\"path\":{}", json_str(&if dir.is_empty() { name.clone() } else { format!("{dir}/{name}") })) |
|
| 228 | + | } |
|
| 229 | + | _ => nodes.first().and_then(path_of).map(|p| format!(",\"path\":{}", json_str(&p))).unwrap_or_default(), |
|
| 230 | + | }; |
|
| 231 | + | conflicts.push(format!( |
|
| 232 | + | "{{\"kind\":{},\"nodes\":{}{detail}}}", |
|
| 233 | + | json_str(kind), |
|
| 234 | + | json_array(nodes.iter().map(|n| json_str(&n.to_hex()))) |
|
| 235 | + | )); |
|
| 236 | + | } |
|
| 237 | + | Ok(format!( |
|
| 238 | + | "{{\"bookmark\":{},\"working_copy\":{},\"heads\":{},\"clean\":{},\"conflicted\":{},\"paths\":{paths},\"conflicts\":{}}}", |
|
| 239 | + | json_str(&self.config.bookmark), |
|
| 240 | + | json_str(&snap.wc.change_id.to_hex()), |
|
| 241 | + | heads.len(), |
|
| 242 | + | snap.statuses.is_empty(), |
|
| 243 | + | base_snapshot.is_conflicted(), |
|
| 244 | + | json_array(conflicts) |
|
| 245 | + | )) |
|
| 246 | + | } |
|
| 247 | + | ||
| 183 | 248 | /// Turn the working-copy change into a normal change on the bookmark (spec §12.3). |
|
| 184 | 249 | pub fn commit(&self, message: &str, extra_deps: &[ChangeHash]) -> Result<ChangeHash> { |
|
| 185 | 250 | let snap = self.snapshot("oxy commit")?; |
|
| … | 628 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +35 −0
| … | 1981 unchanged lines |
||
| 1982 | 1982 | assert!(!repo.snapshot("status").unwrap().changed); |
|
| 1983 | 1983 | } |
|
| 1984 | 1984 | ||
| 1985 | + | #[test] |
|
| 1986 | + | fn status_json_lists_conflicts_with_contributors() { |
|
| 1987 | + | hermetic(); |
|
| 1988 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 1989 | + | let root = dir.path(); |
|
| 1990 | + | let repo = Repo::init(root).unwrap().with_home(root.join("home")); |
|
| 1991 | + | write(root, "a.txt", b"one\n"); |
|
| 1992 | + | let h1 = repo.commit("base", &[]).unwrap(); |
|
| 1993 | + | write(root, "a.txt", b"one\nleft\n"); |
|
| 1994 | + | let ha = repo.commit("left", &[]).unwrap(); |
|
| 1995 | + | repo.checkout(&[h1], true, "checkout").unwrap(); |
|
| 1996 | + | write(root, "a.txt", b"one\nright\n"); |
|
| 1997 | + | let hb = repo.commit("right", &[]).unwrap(); |
|
| 1998 | + | repo.checkout(&[ha, hb], true, "checkout").unwrap(); |
|
| 1999 | + | let snap = repo.snapshot("status").unwrap(); |
|
| 2000 | + | assert!(snap.base_snapshot.is_conflicted()); |
|
| 2001 | + | let json = repo.status_json(&snap).unwrap(); |
|
| 2002 | + | let ida = repo.store().get_change(&ha).unwrap().change_id.to_hex(); |
|
| 2003 | + | let idb = repo.store().get_change(&hb).unwrap().change_id.to_hex(); |
|
| 2004 | + | assert!(json.contains("\"conflicted\":true"), "{json}"); |
|
| 2005 | + | assert!(json.contains("\"kind\":\"text\",\"path\":\"a.txt\""), "{json}"); |
|
| 2006 | + | assert!(json.contains(&ida) && json.contains(&idb), "{json}"); |
|
| 2007 | + | assert!(json.contains("\"resolved\":false"), "{json}"); |
|
| 2008 | + | assert!(json.starts_with('{') && json.ends_with('}')); |
|
| 2009 | + | // Resolve and the report says so; a clean copy has no conflicts. |
|
| 2010 | + | write(root, "a.txt", b"one\nleft\nright\n"); |
|
| 2011 | + | let snap = repo.snapshot("status").unwrap(); |
|
| 2012 | + | let json = repo.status_json(&snap).unwrap(); |
|
| 2013 | + | assert!(json.contains("\"resolved\":true"), "{json}"); |
|
| 2014 | + | repo.commit("resolve", &[]).unwrap(); |
|
| 2015 | + | let snap = repo.snapshot("status").unwrap(); |
|
| 2016 | + | let json = repo.status_json(&snap).unwrap(); |
|
| 2017 | + | assert!(json.contains("\"clean\":true,\"conflicted\":false,\"paths\":[],\"conflicts\":[]"), "{json}"); |
|
| 2018 | + | } |
|
| 2019 | + |
modified crates/oxy-core/src/state.rs +13 −0
| … | 363 unchanged lines |
||
| 364 | 364 | /// For resolving a conflicted text node (spec §9.6): the live atoms that |
|
| 365 | 365 | /// are not part of any conflict region in order, every live atom that is |
|
| 366 | 366 | /// part of a region, and every change that contributed to a region. |
|
| 367 | + | /// The changes that introduced the live blobs of an opaque node, for a |
|
| 368 | + | /// replace conflict (spec §9.3). |
|
| 369 | + | pub fn blob_contributors(&self, node: &NodeId, blobs: &[ObjectRef]) -> Vec<ChangeHash> { |
|
| 370 | + | let mut out: Vec<ChangeHash> = Vec::new(); |
|
| 371 | + | for (h, _, new) in self.replaces.get(node).map(|v| v.as_slice()).unwrap_or(&[]) { |
|
| 372 | + | if new.is_some_and(|b| blobs.contains(&b)) && !out.contains(h) { |
|
| 373 | + | out.push(*h); |
|
| 374 | + | } |
|
| 375 | + | } |
|
| 376 | + | out.sort(); |
|
| 377 | + | out |
|
| 378 | + | } |
|
| 379 | + | ||
| 367 | 380 | pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions { |
|
| 368 | 381 | let mut out = ConflictRegions::default(); |
|
| 369 | 382 | let Some(g) = self.files.get(node) else { return out }; |
|
| … | 212 unchanged lines |
modified docs/guide.md +2 −1
| … | 58 unchanged lines |
||
| 59 | 59 | oxy show 658990aa # one change by ChangeId prefix, #hash prefix, or @ |
|
| 60 | 60 | ``` |
|
| 61 | 61 | ||
| 62 | - | `status` lists each path with a mark: `✚ added`, `✎ modified`, `✖ removed`, |
|
| 62 | + | `status --json` prints the same for tools, with every conflict's kind, path and |
|
| 63 | + | contributing ChangeIds. On a terminal, `status` lists each path with a mark: `✚ added`, `✎ modified`, `✖ removed`, |
|
| 63 | 64 | `➜ renamed`, `⚙ mode`. Renames are detected: a removed file and an added |
|
| 64 | 65 | file of the same kind whose contents are at least half alike are recorded |
|
| 65 | 66 | as a move of the same file, so identity survives a rename. |
|
| … | 277 unchanged lines |
modified docs/plan.md +2 −1
| … | 33 unchanged lines |
||
| 34 | 34 | - [x] Performance follow-up from the 1 GiB benchmark: commit takes 5.6 s on tmpfs but 63 s on the SSD, and clone 49 s, because every object is written with its own fsync (13,715 chunks). Write committed and fetched objects straight into a pack with one fsync, or batch loose writes. (Done: store write batches; small batches stay loose, large ones spill to one pack. SSD: commit 63 s → 8.8 s, clone 49 s → 15.9 s; spec 0.6r.) |
|
| 35 | 35 | - [x] Performance follow-up: `oxy status` re-hashes a clean 1 GiB working copy (2.4 s). Record size and mtime in the manifest and skip hashing files whose stat is unchanged (spec allows a stat cache). (Done: manifest records blob refs for opaque files; unchanged files are not read; two-second racy window; status on a clean 1 GiB copy 2.4 s → 4 ms; spec 0.6s.) |
|
| 36 | 36 | - [x] Documentation: `docs/guide.md` (user guide by task) and `docs/protocol.md` (for other implementers, derived from spec §16 and the wire encodings). (Done; every command and flag in the guide was checked against `--help`.) |
|
| 37 | - | - [ ] Spec review pass: resolve or re-word every "MAY ... until" allowance introduced during implementation, and close what can be closed in §27. |
|
| 37 | + | - [x] Spec review pass: resolve or re-word every "MAY ... until" allowance introduced during implementation, and close what can be closed in §27. (Done as Draft 0.7: no "MAY ... until" wording remained; `oxy status --json` implemented to meet §9.5; §12.4, §16.1 and §24 aligned with the implementation; new §26.1 status table; §27 records workspace conflicts and server locks as open.) |
|
| 38 | 38 | ||
| 39 | 39 | ## Log |
|
| 40 | 40 | ||
| 41 | 41 | - 2026-09-26 23:01 plan created. |
|
| 42 | + | - 2026-09-27 10:00 iteration 18: spec review pass (Draft 0.7) and oxy status --json. 113 tests. |
|
| 42 | 43 | - 2026-09-27 09:35 iteration 17: user guide and protocol document. 112 tests. |
|
| 43 | 44 | - 2026-09-27 09:10 iteration 16: manifest stat cache (status 2.4 s → 4 ms on 1 GiB). 112 tests. |
|
| 44 | 45 | - 2026-09-27 08:40 iteration 15: batched object writes (commit 8.8 s, clone 15.9 s for 1 GiB on SSD). 111 tests. |
|
| … | 14 unchanged lines |
modified docs/spec.md +47 −20
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | | | | |
|
| 4 | 4 | |---|---| |
|
| 5 | - | | **Status** | Draft 0.6s | |
|
| 5 | + | | **Status** | Draft 0.7 | |
|
| 6 | 6 | | **Format version** | 1 | |
|
| 7 | 7 | | **Date** | 2026-09-26 | |
|
| 8 | 8 | | **Audience** | Implementers, security reviewers, and contributors | |
|
| … | 351 unchanged lines |
||
| 360 | 360 | ||
| 361 | 361 | The short identifiers are ChangeId prefixes. Delete/insert conflicts render the deleted context as a `-------` section. Tree conflicts are materialized by writing each candidate with a suffix `.oxy-conflict-<n>` and are listed by `oxy status`. Replace conflicts materialize as `<name>.oxy-conflict-<n>` files. |
|
| 362 | 362 | ||
| 363 | - | `oxy status` MUST list every conflict, MUST exit with status 2 when any conflict exists in the working-copy state, MUST exit with status 3 when only workspace conflicts (§9.7) exist, and MUST support `--json` output enumerating conflicts by node, kind, and contributing ChangeIds. |
|
| 363 | + | `oxy status` MUST list every conflict, MUST exit with status 2 when any conflict exists in the working-copy state, MUST exit with status 3 when only workspace conflicts (§9.7) exist, and MUST support `--json` output enumerating conflicts by node, kind, and contributing ChangeIds. The JSON object has `bookmark`, `working_copy` (ChangeId), `heads`, `clean`, `conflicted`, `paths` (`[{path, status}]`) and `conflicts`: for a text or replace conflict `{kind, path, node, changes: [ChangeId], resolved: true | false | "markers"}`, for a tree conflict `{kind: name | multi-placement | orphan | cycle | mode, nodes, path?}`. |
|
| 364 | 364 | ||
| 365 | 365 | ### 9.6 Resolution |
|
| 366 | 366 | ||
| … | 98 unchanged lines |
||
| 465 | 465 | ||
| 466 | 466 | ### 12.4 Snapshot performance |
|
| 467 | 467 | ||
| 468 | - | Implementations MUST support a file-system monitor (Watchman or a native inotify/FSEvents/ReadDirectoryChangesW watcher) and MUST fall back to a stat-based crawl with a persistent cache keyed by `(path, size, mtime, inode, ctime)`. Snapshot latency targets are in §25. |
|
| 468 | + | Implementations MUST support a stat-based crawl with a persistent cache keyed by at least `(path, size, mtime)` and SHOULD support a file-system monitor (Watchman or a native inotify/FSEvents/ReadDirectoryChangesW watcher) for the latency targets in §25. |
|
| 469 | 469 | ||
| 470 | 470 | The reference implementation's cache is the workspace **manifest** written after every snapshot, commit, and checkout: per file its size, mtime, content hash, and, for an opaque file, the ObjectRef of its stored blob. On the next snapshot a file whose size and mtime match is not read: an opaque file contributes its recorded blob, a text file keeps its recorded hash. A file whose mtime lies within two seconds of the manifest's own write time is never trusted (the racy-stat window), so a modification that preserves size and lands in the same timestamp tick is still seen. An implementation MAY use a file-system monitor instead of, or in addition to, the manifest. |
|
| 471 | 471 | ||
| … | 198 unchanged lines |
||
| 670 | 670 | ||
| 671 | 671 | | Endpoint | Request | Response | |
|
| 672 | 672 | |---|---|---| |
|
| 673 | - | | `GET capabilities` | — | `{ versions: [1], transports, features: ["lazy", "checkpoint", "seal", "lock"], max_frame }` | |
|
| 673 | + | | `GET capabilities` | — | `{ versions: [1], features }`; feature names are `seal`, and, when offered, `lock`; `max_frame` and `transports` are reserved for framed transports | |
|
| 674 | 674 | | `GET refs` | `{ patterns }` | `[ { name, state: ObjectRef, heads, attestation: Signed<Attest> } ]` plus the current `Signed<Policy>` | |
|
| 675 | 675 | | `POST negotiate` | `{ have: [ChangeHash], want: [ChangeHash], filter }` | stream of `ChangeHash` the client lacks, in dependency order | |
|
| 676 | 676 | | `POST fetch` | `{ changes: [ChangeHash], include_blobs: bool, filter }` | stream of `Signed<Change>`, then every `Signed<Attest kind=seal>` covering them, then referenced `blob`/`chunk` objects | |
|
| … | 114 unchanged lines |
||
| 791 | 791 | Commands mirror Jujutsu where semantics match, so that muscle memory transfers. |
|
| 792 | 792 | ||
| 793 | 793 | ``` |
|
| 794 | - | oxy init [--no-policy] oxy clone <url> [--sparse PATH..] [--depth N] [--max-blob SIZE] |
|
| 795 | - | oxy status [--json] oxy diff [-r CHANGE] [PATH..] |
|
| 796 | - | oxy commit [-m MSG] [--depends-on CHANGE].. [--sign] |
|
| 797 | - | oxy new [PARENT..] oxy edit CHANGE |
|
| 798 | - | oxy split oxy squash oxy abandon CHANGE |
|
| 799 | - | oxy rebase -s CHANGE -d DEST oxy resolve [PATH..] |
|
| 800 | - | oxy log [-r REVSET] oxy show CHANGE |
|
| 801 | - | oxy bookmark create|set|delete|list oxy tag create NAME -r CHANGE |
|
| 802 | - | oxy fetch [REMOTE] oxy push [REMOTE] [-b BOOKMARK] |
|
| 803 | - | oxy undo oxy redo oxy op log |
|
| 804 | - | oxy lock PATH oxy unlock PATH |
|
| 794 | + | oxy init [PATH] oxy clone URL [--sparse PATH].. [--depth N] [--max-blob SIZE] |
|
| 795 | + | oxy status [--json] oxy diff oxy prompt [--format F] |
|
| 796 | + | oxy commit -m MSG [--depends-on CHANGE].. |
|
| 797 | + | oxy log [-n N] [-v] oxy show CHANGE |
|
| 798 | + | oxy describe [CHANGE] -m MSG oxy amend [-m MSG] [--into CHANGE] |
|
| 799 | + | oxy split CHANGE [PATH].. oxy abandon CHANGE oxy resolve |
|
| 800 | + | oxy checkout [CHANGE].. [--discard] oxy undo oxy op log |
|
| 801 | + | oxy tag create NAME [CHANGE].. [-m MSG] oxy tag list |
|
| 802 | + | oxy workspace add|list|forget oxy sparse [PATH].. oxy deepen [N] |
|
| 803 | + | oxy fetch [REMOTE] oxy pull [REMOTE] oxy push [REMOTE] [--force] |
|
| 804 | + | oxy remote add|remove|list oxy serve oxy lock|unlock PATH |
|
| 805 | 805 | oxy identity create|show|export|rotate|renew|revoke|delegate |
|
| 806 | - | oxy policy init|show|grant|revoke|root|sign |
|
| 807 | - | oxy remote add|trust|list oxy gc |
|
| 806 | + | oxy policy init|show|grant|revoke|root|sign oxy verify [CHANGE] |
|
| 808 | 807 | oxy git import|export|fetch|push (fetch and push go through a bare mirror under .oxy/git/) |
|
| 808 | + | oxy checkpoint oxy gc [--now] oxy config [--user] KEY VALUE |
|
| 809 | 809 | ``` |
|
| 810 | 810 | ||
| 811 | - | Change arguments accept a ChangeId prefix, a ChangeHash prefix (with `#`), a bookmark, `@` for the working-copy change, and `@-` for its parent. Ambiguous prefixes are an error. |
|
| 811 | + | 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 | 812 | ||
| 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). |
|
| 814 | + | ||
| 813 | 815 | ## 25. Performance targets |
|
| 814 | 816 | ||
| 815 | 817 | Measured on the Full profile, warm cache, NVMe storage, 8-core x86-64, with a 20 GiB repository of 2 million files and 500 000 changes. |
|
| … | 25 unchanged lines |
||
| 841 | 843 | - Adversarial chunking: inputs with no gear boundaries and inputs with boundaries at every byte. |
|
| 842 | 844 | - Git round trip on a corpus of public repositories. |
|
| 843 | 845 | ||
| 846 | + | ### 26.1 Reference implementation status |
|
| 847 | + | ||
| 848 | + | The reference implementation (`crates/oxy-core`, `crates/oxy-server`, the `oxy` binary) as of this draft: |
|
| 849 | + | ||
| 850 | + | | Area | Status | |
|
| 851 | + | |---|---| |
|
| 852 | + | | Objects, hashing, deterministic CBOR (§5) | Implemented; format 2 repositories, migration from format 1 | |
|
| 853 | + | | Change graph, commutation, conflicts and resolution (§6–§9) | Implemented, including order, delete/insert, tree, replace, and mode conflicts; workspace conflicts (§9.7) and exit status 3 are not detected | |
|
| 854 | + | | Rewrites (§8.4) | Implemented: describe, amend (also into older changes), split, abandon; orphaned references re-anchored | |
|
| 855 | + | | Operation log, undo (§12.1) | Implemented; `redo` reserved | |
|
| 856 | + | | Snapshots and the stat cache (§12.3, §12.4) | Implemented; no file-system monitor yet | |
|
| 857 | + | | Storage: loose objects, packs, GC, checkpoints, batching (§11, §13, §21) | Implemented; pack indexes are read fully rather than mapped | |
|
| 858 | + | | Identity, composite signatures, seals, attestations, policy, lifecycle (§14) | Implemented; `lock` attestations reserved | |
|
| 859 | + | | HTTPS transport and endpoints (§16) | Implemented, HTTP only; Noise and ZCT (§15) reserved; negotiation is exact (no Merkle buckets); `refs` takes no `patterns` | |
|
| 860 | + | | Partial, sparse, shallow (§17) | Implemented | |
|
| 861 | + | | Locks (§18) | Local advisory locks only; server locks and the `lock` endpoint reserved | |
|
| 862 | + | | Configuration, attributes, hooks (§19, §20) | Implemented (`.oxyattributes`: binary, text, eol, nocompress, lockable) | |
|
| 863 | + | | Thin profile and proxy (§22) | Reserved | |
|
| 864 | + | | Git interoperability (§23) | Implemented: import, export, fetch, push, rename detection | |
|
| 865 | + | | 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 | + | ||
| 844 | 867 | ## 27. Open questions |
|
| 845 | 868 | ||
| 846 | 869 | 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. |
|
| 847 | 870 | 2. **Word- or character-granularity atoms** for prose and minified files. Line atoms are fixed for v1. |
|
| 848 | - | 3. **Set reconciliation** at the scale of tens of millions of changes may need an IBLT-based negotiator; the Merkle bucket scheme in §16.2 is the v1 answer. |
|
| 871 | + | 3. **Set reconciliation** at the scale of tens of millions of changes may need an IBLT-based negotiator; the Merkle bucket scheme in §16.2 is the v1 answer, and the reference implementation's exact negotiation is sufficient below that scale. |
|
| 872 | + | 4. **Workspace conflicts (§9.7)** on case-insensitive file systems are specified but not yet detected by the reference implementation. |
|
| 873 | + | 5. **Server-side locks (§18)** need the `lock` endpoint and attestation to be exercised by an implementation before their wording is final. |
|
| 849 | 874 | ||
| 850 | 875 | --- |
|
| 851 | 876 | ||
| 852 | - | *Changelog.* Draft 0.6s (2026-09-27): the manifest as a stat cache with a racy-stat window (§12.4). |
|
| 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. |
|
| 878 | + | ||
| 879 | + | Draft 0.6s (2026-09-27): the manifest as a stat cache with a racy-stat window (§12.4). |
|
| 853 | 880 | ||
| 854 | 881 | Draft 0.6r (2026-09-27): batched object writes (§13.4). |
|
| 855 | 882 | ||
| … | 51 unchanged lines |
modified src/cli/mod.rs +6 −2
| … | 48 unchanged lines |
||
| 49 | 49 | /// Show or set the sparse filter (paths to materialize); re-checks out the bookmark |
|
| 50 | 50 | Sparse { paths: Vec<String> }, |
|
| 51 | 51 | /// Show the working copy: changed paths and conflicts |
|
| 52 | - | Status, |
|
| 52 | + | Status { |
|
| 53 | + | /// Machine-readable output: paths, and every conflict with its kind and contributing changes |
|
| 54 | + | #[arg(long)] |
|
| 55 | + | json: bool, |
|
| 56 | + | }, |
|
| 53 | 57 | /// One-line summary for shell prompts; fast and records nothing |
|
| 54 | 58 | Prompt { |
|
| 55 | 59 | /// Placeholders: %b bookmark, %c change id, %h heads, %d dirty (*), %x conflict (!), %A/%D ahead/behind (+n/-n), %a/%B raw counts |
|
| … | 324 unchanged lines |
||
| 380 | 384 | ui::init(cli.plain); |
|
| 381 | 385 | match cli.cmd { |
|
| 382 | 386 | Cmd::Init { path } => workspace::init(path), |
|
| 383 | - | Cmd::Status => workspace::status(), |
|
| 387 | + | Cmd::Status { json } => workspace::status(json), |
|
| 384 | 388 | Cmd::Prompt { format } => workspace::prompt(&format), |
|
| 385 | 389 | Cmd::Commit { message, depends_on } => workspace::commit(&message, &depends_on), |
|
| 386 | 390 | Cmd::Log { limit, verbose } => workspace::log(limit, verbose), |
|
| … | 68 unchanged lines |
modified src/cli/workspace.rs +5 −1
| … | 28 unchanged lines |
||
| 29 | 29 | } |
|
| 30 | 30 | } |
|
| 31 | 31 | ||
| 32 | - | pub fn status() -> Result<i32> { |
|
| 32 | + | pub fn status(json: bool) -> Result<i32> { |
|
| 33 | 33 | let repo = open()?; |
|
| 34 | 34 | let sp = ui::spinner("Snapshotting the working copy…"); |
|
| 35 | 35 | let snap = repo.snapshot("oxy status")?; |
|
| 36 | 36 | let heads = repo.bookmark_heads()?; |
|
| 37 | 37 | sp.clear(); |
|
| 38 | + | if json { |
|
| 39 | + | println!("{}", repo.status_json(&snap)?); |
|
| 40 | + | return Ok(if snap.base_snapshot.is_conflicted() { 2 } else { 0 }); |
|
| 41 | + | } |
|
| 38 | 42 | println!( |
|
| 39 | 43 | "{} {}{}working copy {}{}{}", |
|
| 40 | 44 | ui::dim("On"), |
|
| … | 442 unchanged lines |