OXYGENHUB
◆ main oxygen / CHANGELOG.md
63.9 KiB · Markdown · last changed Release 0.11.0 22 hours ago renderedsource blame history raw

Changelog

0.11.0 (2026-11-04)

  • Changed: the changelog is checked on the WASM tier. A changelog job in the pipeline runs OxygenFlow's checks module on any runner with the WASM tier, failing when the changelog's sections are out of shape or Cargo.toml's version has none.
  • Changed: moved subrepo pins are named (#162). A pin moved by work inside a subrepo is a pin bump the parent's next commit records, and it used to go in unsaid. oxy status now lists it under Subrepo pins (old → new), and a subrepo's own uncommitted edits, which the parent does not record; a bare oxy commit that records a moved pin says so and how to leave it out (oxy commit PATHS).
  • Added: oxy submodule remove --delete (#164). It deletes the subrepo's directory too, read-only object files and all, and refuses while the subrepo has uncommitted edits (--force to delete them anyway); without it, removal says the directory is left and how to delete it. A directory moved by hand used to fail halfway, after the copy, on the read-only files.
  • Changed: an agent's commit says "awaiting seal", not "unsigned" (#165). In a checkout that signs keyless under an agent grant, a change of its own author that the next push will seal shows ◌ awaiting seal in oxy log and oxy show; "unsigned" is kept for changes nothing will sign.
  • Fixed: a stale author.key can no longer fork your identity (#163). A checkout that pinned author.key to a key a rotation made elsewhere had already succeeded built a second successor of it on oxy identity delegate add, splitting the identity in two. rotate, renew and delegate add/remove now refuse to start from a key that already has a successor in your home, naming the newest key and the oxy config that points at it; their report says which key they started from and which config named it; and oxy identity show warns when the configured key is not the newest, and names the key where an identity forks.

0.10.0 (2026-11-01)

  • Fixed: fetch, pull and clone are far faster with many bookmarks (#158). They fetched each bookmark in its own exchange with the hub, and changes that bookmarks share were sent again for each one; a pull with nothing new still asked for a bundle per bookmark. Now every bookmark's heads are fetched in one exchange, and none when nothing is new. On a 47 MB repository with 160 bookmarks: a pull with nothing new 169 s → 3.5 s, a clone 324 s → 26 s. OXY_TIMINGS now also shows the refs, transparency log and per-bookmark checks of a fetch.
  • Added: oxy git colocate, a .git beside .oxy (oxygenhub#355). Your editor's Git features (blame, history, gutter marks) and other Git tools now work on an Oxygen checkout: oxy keeps a .git there with the current bookmark exported after every command, Git's index at its tree so git status shows only your uncommitted work, and .oxy/ excluded. One way: commit with oxy. --off stops it.
  • Changed: .git is never content. oxy no longer picks up a .git directory at a checkout's top, and checkout refuses a path with a .git component at any depth (as Git does), so a change cannot plant Git hooks.
  • Added: a Git export from the change graph (oxygenhub#355). oxy-core's git_export_graph writes every bookmark and tag to a bare Git repository so that the same changes give the same commits wherever it runs: each change a commit whose parents are its dependencies, each bookmark a branch (a merge of its heads when it has several), tags as tags, incrementally. It is what OxygenHub serves to git clone. (oxy git export still follows the checkout's own history.)
  • Added: oxy mcp, Oxygen for AI agents (#159). An MCP server over stdio for a repository: an agent calls typed tools (status, diff, log, show, cat, blame, bookmarks, tags, conflicts, op log, identity, commit, amend, describe, fetch, pull, push, tlog verify) and gets each command's JSON back instead of reading a shell. It signs as the identity its environment configures, so started with an agent home every change is sealed via the agent with nothing to forget per command, and says who in its initialize reply. Commits name their paths (or all: true); undo and forced pushes are off unless started with --allow-destructive. The protocol itself is oxy-core's mcp module, for oh mcp to reuse. See docs/mcp.md.
  • Icons for HaikuDepot (#157). packaging/haikuports/icons/ has oxy's and oh's icons as HVIF (Haiku's vector format) with 16, 32 and 64 px PNGs, written by make-icons.py: the Oxygen mark on a pale tile for oxy, on OxygenHub's dark tile for oh, the same family as the VS Code extensions.

0.9.0 (2026-10-29)

  • Releases carry their source (#157). Each release now has oxy-VERSION-src.tar.gz, the source as released, reproducible (sorted, fixed times and owners, gzip without a timestamp), listed in SHA256SUMS, for packagers to fetch at a fixed checksum. packaging/haikuports/make-recipe.py oxy|oh TARBALL VERSION writes the haikuports recipe from it (oh's from OxygenHub's source tarball); the 0.8.0 recipe, which used the on-demand archive, is gone.

  • Added: a haikuports recipe for oxy (#156). packaging/haikuports/ holds the recipe that builds oxy for HaikuDepot from the release source and every crate in Cargo.lock, and the script that writes it for a new release; the 0.8.0 recipe builds with haikuporter on Haiku R1/beta6.

  • Fixed: an expired checkout token no longer hides your sign-in (#155). A checkout's own token for a remote (remote.NAME.token) is used first, so when it expired every fetch, pull and push answered 401 although oxy auth login had a good sign-in for the same hub. A checkout token the hub no longer knows (401) now falls back to that sign-in, with a warning naming oxy config --unset remote.NAME.token to drop the stale one; never to another checkout's token, and not when the token is refused for permission (403, 404), so a narrowly scoped token is never swapped for a wider sign-in. When both are refused, the error says to sign in again and unset it.

  • Added: the artifact object type (Cylinder C1, #108). An artifact is now an object of its own in the store (oxy-core's artifact module), named by its object hash as every object is, and oxy fsck checks it; its tree stays a blob. Stores written before read as they did: the type is a new one at the end of the list.

  • Added: artifacts (Cylinder, step C1, #108). oxy artifact pack DIR --kind archive|package|container|runner-image|report [--arch --os --entrypoint --env K=V --label K=V] packs a directory into an artifact: each file an ordinary blob of the repository's store, so files and chunks it holds already cost nothing, under a tree and an artifact encoding in canonical CBOR, named by the BLAKE3 of that encoding (docs/cylinder.md §4.1). oxy artifact list | show NAME | unpack NAME DEST | verify NAME (any unique prefix of six characters or more; --json on list, show and pack), and oxy gc keeps every artifact's blobs. The new cylinder crate holds the model.

  • Added: oxy on Haiku (#154). oxy builds and runs on Haiku R1/beta6 (x86_64) with HaikuPorts' Rust; oxy-VERSION-haiku-x86_64.tar.gz is on the releases page from 0.8.0, with its own SHA256SUMS-haiku. The README says how to install it and how to build it (ZSTD_SYS_USE_PKG_CONFIG=1).

  • Fixed: a subrepo put back after its removal is pinned in the change that registers it (#152). oxy submodule remove leaves a marker so the next snapshot drops the node while the directory stays; the snapshot cleared it only after reading it, so when nothing looked at the working copy between the removal's commit and the subrepo's return, the change that registered it again in .oxymodules left it out and a second commit was needed. A marker is now dropped before it is read once the removal is recorded or the path is registered again.

  • Fixed: oxy identity create needs no repository (#144). Outside one it makes a user identity: the secret and identity go to the oxy home's keys and the user file names it as the author, so a new user or machine sets up an identity first; a repository made or cloned afterwards signs with it. Only --local needs a repository, and inside one it is kept there too, as before.

0.8.0 (2026-10-27)

  • A server can add a note to a reference a push moved (#148). oxy-server's Backend has a push_notes step: each note is added to that accepted reference's result, which oxy push shows in place of "updated". OxygenHub will use it to say when a push moves the bookmark of a proposal that has already landed (oxygenhub#329).

  • Identical sides settle by themselves (#146). Two bookmarks that each resolved the same conflict to the same text conflicted again when merged, both sides the identical lines. A conflict whose sides are byte-identical now settles as merge=union adjacent insertions do (#145), in any file and keeping one side: a checkout writes it once ("kept one of its identical sides") and the next commit records the resolution, and a host landing a proposal settles it too.

  • A subrepo at its pinned state is unmodified on any bookmark (#147). A subrepo whose heads were exactly its pin's but which sat on another bookmark than the pin named counted as modified, so oxy bookmark switch refused ("the working copy has changes"). Only the state counts now: the same repository at the same heads, or the same Git commit; the recorded pin stands.

  • A shallow clone fetches its checkpoint in parts (#137). The checkpoint of a big state came as one answer, the whole state's graph blobs in one body (about 2 GB for the VS Code history), built and held whole on both sides. A server now answers checkpoint-index (the checkpoint and its history, no graphs), and the client fetches the graph blobs it lacks with objects, 2,048 at a time; objects reads them in pack order. An older server answers 404 and gets the one-piece request as before.

  • A keyless agent follows its owner's newest identity (#82). Every delegation rotates its owner's identity, so the author.key an agent's home was given goes stale, and its changes named an identity two rotations old (verifying only because a grant from before a rotation still counts). With author.for = ACCOUNT in the home, the hub sends the account's newest linked identity with each certificate, with its chain back to the grant's key; oxy keeps it in the home (keys/author.for), stores the chain beside the change so the grant checks, and authors as it from then on. A repository's own author.key pin still wins.

  • Changelogs stop conflicting (#145). A file marked merge=union in .oxyattributes (CHANGELOG.md merge=union) has its adjacent insertions settled by the checkout itself: both sides kept, the earlier change's first, as oxy resolve --both does, and the next commit records the resolution. Only adjacent insertions: where two changes changed the same lines, the markers stay. Oxygen's own changelog is marked so.

  • Two changes adding lines at the same place are settled with one command (#121). Like Git, oxy leaves a conflict when two changes each add lines after the same line, since either order can be wrong for code; settling it meant editing out the markers by hand. oxy resolve --both [PATH]… now keeps every side of each such adjacent insertion, the earlier change's first (--reverse for the other order), and oxy status says which conflicts qualify. It leaves the markers where the sides changed the same lines (keeping both would keep two versions of them) or where the lines between the markers were edited. Conflict markers now list their sides earliest change first, not in hash order.

0.7.0 (2026-10-26)

  • Many objects are read in pack order (#137): Store::get_many (a clone's checkpoint, a fetch's objects) reads packed objects by pack and offset, each thread a run of them, so a spinning disk sweeps instead of seeking across the packs; results keep the asked order. The server's gathering of a VS Code-sized checkpoint (tens of thousands of objects in ~95 packs) was the slow part of a depth-0 clone on Earth.
  • Windows paths print plainly (#143): a repository's root showed as \\?\C:\work\demo in every message, Windows' verbatim form from canonicalize; oxy now drops the prefix where the plain path says the same (oxy_core::portable::canonical). Found running the shipped Windows build on a Windows 11 VM.
  • Oxygen's CI writes a test report beside its coverage (#139): the test job runs under cargo-nextest (.config/nextest.toml, profile ci) as well as cargo-llvm-cov, keeping a JUnit report whether or not the tests pass, for the hub's Tests tab.
  • Pushing a new bookmark sends only what the remote lacks (#142). A push was worked out against the remote's heads for that bookmark alone, so a bookmark new to the remote sent its whole history again: slow on a big repository, and OxygenHub's push protection scanned all of it (and refused a proposal's bookmark for an example key in a change main had had for weeks). The heads of every bookmark the remote lists now count as already there.
  • A binary file's or subrepo pin's conflict can be settled from the command line (#140). Two changes that replaced the same binary file, or moved the same subrepo pin, leave a replace conflict with no markers to edit, and every commit was refused until it was settled, which only a hosted edit could do. oxy resolve PATH… now settles it with what the working copy holds at the path (the file, or the subrepo's pin): the next commit is a resolution replacing every version with it. A path with no replace conflict is refused, and a choice is forgotten once committed.
  • The object store works where hard links are refused (#141). Android forbids apps hard links in their own storage, so the OxygenHub app's phone runner could clone nothing (Permission denied). Where a link is refused, objects are renamed into place with renameat2(RENAME_NOREPLACE): as atomic, and still never over a name that is taken.
  • Rendering a big tree is many times faster, and clone, pull, switch and oxy workspace add stop rendering it twice (#137). Measured on the VS Code history's depth-0 clone (19,545 files, 166,425 changes), oxy workspace add took 467 s on Earth and 176 s on an 8-core workstation, 11 GB at its peak; it now takes 7.5 s and 1.8 GB on the workstation. Three causes: (1) whether one change depends on another, which rendering asks about every deleted line, was answered by a depth-first walk that compared a change with the one sought only on coming back up to it, so it went through thousands of changes first although the answer is nearly always a direct dependency (spec §8.1): 262,678 questions took 4.4 billion steps and 838 CPU-seconds, and now take 5 CPU-seconds, with the same answers; (2) finding the subrepos rendered every file of the tree, after every clone, pull, switch and workspace add, where reading the tree and the subrepos' pins is enough; (3) a new workspace started on the target state with an empty directory, so the checkout's first snapshot found the whole tree deleted and rendered it to say so. OXY_TIMINGS=1 now also says how a checkout's files were ordered (graph: single order…) and how its blobs were read.
  • oxy serve takes a push of more than 2 MB (#138). It set no request limit of its own, so axum's default of 2 MB applied, and a larger push part was cut off with Broken pipe at the client and nothing in the server's log. It now takes up to 4 GiB a request (oxy_server::BODY_LIMIT), OxygenHub's default push_body_limit; OxygenHub was not affected.
  • A push no longer sends a replaced or removed binary file's old content again (#136). A bundle carried both the old and the new blob of every binary file a change replaces, so a change removing ~1,100 tracked cache files (oxygenhub-android's set-up) pushed all of them again, over 100 MB, though the hub stored each when it was added. The old blob is always the new blob of a change the replacement depends on, which the receiver already holds or which the same bundle carries, so pushes and fetch answers now carry only the new one.

0.6.0 (2026-10-25)

  • A shallow clone of a big repository works, and quickly (#137). oxy clone --depth 0 of the VS Code history failed after eleven minutes ("object … not found"): the blobs of the state's binary files are named only by the changes that set them, which a depth-0 clone does not take, so nothing brought them. They are now fetched with the checkpoint (in parts), or left to be fetched on read under --max-blob. The checkpoint itself was stored object by object, each written loose and flushed (57,498 objects at 27 a second); it is now stored in one batch, as a fetch's objects are.
  • A checkpoint is served faster, and says where its time goes (#137). A server gathered a checkpoint's graph blobs and their chunks one read at a time: six minutes for the VS Code state's 57,000. They are now read in two parallel batches. OXY_TIMINGS=1 now times the checkpoint's phases on both sides (checkpoint: gather its objects, serve: encode the checkpoint, remote: checkpoint answer, shallow: store the checkpoint and the rest).
  • A depth-0 clone takes no change (#137). oxy clone --depth 0 took the head changes themselves (the VS Code import's main has 33,835 heads), though the checkpoint already holds every change's metadata, so depth 0 now takes none (spec §17's "a checkpoint plus zero history"): its heads become the shallow boundary, and commit, push and pull on top work as before (a shallow repository takes and builds on changes below what it cloned, which the remote holds).
  • A push no longer passes a proxy's request limit (#133). A push to a hub behind Cloudflare, which takes at most 100 MB in one request, was refused HTTP 413 with Cloudflare's HTML page printed raw: parts of a long push were about 256 MiB, and one change's blobs went in one request however many there were. Parts are now about 64 MiB (push.batch-bytes), a part whose blobs pass that sends them first in parts of objects alone, an HTML error page is shown by its title, and a 413 says how to push in smaller parts.
  • A keyless agent's grant survives its owner's key rotation (#135). Every delegation an owner adds or removes renews their identity, and a repository that names the newest key as author.key read every change their agent sealed as INVALID ("key … is neither the author nor a delegate"), and its pushes were refused, because the agent's grant was signed by the owner's earlier key. A grant by a key the author's identity was rotated or renewed from now counts when it was made before that rotation; one an old key made after it does not.
  • A tag can be withdrawn, by a signed withdrawal (oxygenhub#301). There was no way to remove a tag anywhere: a mistaken release tag stayed for good. oxy tag delete NAME [-r REASON] records an untag attestation naming the tag's state, signed like a tag (a ten-minute keyless key too), and the next oxy push sends it to a remote that still has the tag; oxy push --delete tags/NAME does both at once. A receiver under a policy accepts it only from a key that may create the tag, keeps it beside the tag's creation, and lists withdrawn tags in its refs answer (key 8), so a fetch withdraws the tag there too (tag v1 was withdrawn on origin, and here). A withdrawn name is never used again, so a tag name never names two states. oxy-server's Backend gains vet_tag_withdrawal (OxygenHub refuses while a release uses the tag), and Repo::host_tag_create and host_tag_withdrawal let a host make or withdraw a tag for a signed-in user. The wire change is additive, but an older server refuses a withdrawal (wrong attestation kind), and an older client keeps a withdrawn tag.
  • A keyless agent's changes verify in a repository that has never seen its owner (#134). An agent's ten-minute key is certified under its owner's signed grant, and a change it signed only verified where the owner's identity was already stored: in a new repository, or one the owner had not pushed to since a key rotation, oxy log --verify and the hub said INVALID ("grant is by an unknown author"). OxygenHub now sends the grant's signer with the certificate (grantor_identity, additive), oxy keeps it (else uses <oxy home>/keys/<key>.identity, or refuses with how to put it there), and a push brings it beside the certificate, so the receiver can check the grant.

0.5.1 (2026-10-20)

  • oxy 0.5.0 and older can clone a repository again after a keyless agent tagged it (#131). Since #129 a release tag may be signed by a ten-minute certified key, which released clients do not know how to authorize: they refused the tag (key … may not create tags/v0.5.0 under this repository's policy), and with it the whole clone, fetch or oxy submodule update. Clients now send User-Agent: oxy/<version> and declare what they understand in Oxy-Features (certified-tags so far; docs/protocol.md, Client capabilities). Under a policy, a server leaves out of its refs answer to a client that does not declare certified-tags every tag that only a certificate authorizes, so that client clones the rest as if the tag did not exist. The rule is Repo::refs_for_client in oxy-core; oxy-server gains a Backend::adjust_refs hook, given the request headers, whose default applies it, so oxy serve and every embedding host do.
  • Keyless agents can create release tags (#129). oxy tag create and oxy push under a keyless home was refused "unknown key": the push did not carry the tag signer's identity and certificate, and tags were authorized only by keys the policy names, never by a certificate under a subject grant. The push now carries them, and a receiver authorizes a short-lived key's tag by a subject grant at its own clock, as it does a bookmark move, and records when it accepted it (OxygenHub signs that with its key). A fetch checks such a tag at that acceptance, or at its integrated time when the hub's log gives one, so a release tag made with a ten-minute key is still fetched months later. A fetch never checks it at the time the tag claims: without an acceptance the tag is left out with a warning (tags/NAME not updated), and the rest is fetched. A client keeps the acceptance and serves and pushes it with the tag, so a tag passed on to another remote, or cloned from a clone, keeps its time. The wire change is additive: tags in refs and push gain keys 4 to 6 (acceptance, log place, cosigned checkpoints), which older readers ignore.

0.5.0 (2026-10-20)

  • A repository kept in a bucket finds signatures and seals without a LIST each (#116). Every lookup of an object's signatures, seals, receipts, approvals or certificate listed a prefix in the bucket, and most of these lookups found nothing. Writers now also add each such entry to a journal (pack-entries, updated by compare-and-swap), and oxy gc folds them all into one list. Once a store has been folded, a reader reads the lists and the journal when it opens it, and again after 30 seconds (as long as the pack listing is kept), and looks everything up from them. Another writer's entry is seen within 30 seconds, as its pack would be, even by a handle a server keeps open. The first oxy gc also copies the one-list keys of much older stores (sigs/, seals/, certs/, …) into the current layout and marks them folded, so readers stop requesting them; until now nearly every lookup made one of these requests and found nothing. On the test store, 40 lookups went from 85 requests (40 LISTs) to 7 (one journal read and no LIST). Older readers are unaffected, because every entry still has its own key. Entries written by oxy 0.4.6 or older after the fold are missed until the next oxy gc, so upgrade every client that writes to the bucket first, then run oxy gc once. A server's /health counts journal reads as read_journal. A repository's own directory works as before.

  • A server can refuse a push by what it adds (oxygenhub#268). oxy-server's Backend has a vet_push step, called with the push's changes before anything is stored or moved; OxygenHub uses it for push protection (secret scanning). The refusal reaches oxy push as the server's own text, several lines included, and a refusal that mentions a certificate or a timeout no longer picks up a misleading hint.

  • Fetch checks the hub's transparency log (oxygenhub#238). A hub that keeps a log sends its latest checkpoint with every refs answer, and each bookmark's attestation with its log entry and inclusion proof. The first fetch pins the hub's log key in <oxy home>/tlog/<hub> with the last checkpoint seen. A later checkpoint must be signed by that key and extend the last one: a smaller log, another root at the same size or a failed consistency proof fails the fetch, saying the hub showed two histories. An attestation not logged yet, or not proved, is only a note for now, printed by fetch and pull and in --json. A hub that comes back with a new log key (a new key, a reused address, a rename) is a loud note, and the new key is pinned. Older hubs and clients ignore the new fields.

  • oxy tlog verify audits a hub's whole log (oxygenhub#238). It accepts the hub's checkpoint by the same rules as fetch, downloads every entry and requires them to make the root the checkpoint signs, then lists this repository's entries, a key's (--key) or all of them (--all); --json too. It also shows which witnesses cosigned and when.

  • Trusted time from witnesses (oxygenhub#240). An entry's integrated time is the earliest time a trusted witness cosigned a checkpoint that holds it, by the witness's clock, so a hub can only make it later, never earlier. A fetch pins the hub's witnesses beside its log key (<oxy home>/tlog/<hub>.witnesses; a new or changed witness key is a note), counts a cosignature only when it verifies against a pinned key, and checks a delegate's delegation and a short-lived key's certificate at that time. Without one it checks them as before, at the hub's acceptance or the attestation's own time. A claimed time more than 10 minutes off the integrated time is noted, not refused, for now. The wire change is additive.

  • oxy witness serve: a witness anyone can run (oxygenhub#238). oxy witness serve --name HOST --log ORIGIN=FILE … serves the C2SP tlog-witness add-checkpoint call. It cosigns, with this oxy home's key and its own clock, each checkpoint that verifies with the log's key and extends the last one it cosigned, and refuses a fork. What it cosigned is kept per log in --state, so it survives a restart.

  • oxy show --stat (#119). Lists the files a change touched instead of its diff: each path with lines added and removed and its kind, renames as from → to, and a total. --json gives each file's counts.

  • oxy merge takes a remote's bookmark (#117). oxy merge oxygenhub/develop said "no bookmark": merge looked only at local bookmarks, so merging what a remote has meant switching to it, pulling and switching back. It now takes a remote's tracking bookmark as last fetched, as revsets do, and says where it looked when neither exists.

  • Commit a resolution by paths, and amend only some paths (#118). A conflict resolution can be committed by paths, leaving other work uncommitted on top of the resolved state, as long as the paths include every file it resolves, whole; otherwise the refusal names them. oxy amend PATH… folds only those paths into the head and leaves the rest uncommitted.

  • oxy doctor checks the trust policy (#127). A repository with no policy was "Everything looks right" while every fetch warned that nothing was verified. Doctor now has a trust line: no policy is a warning that says who sets one and how; a policy shows its serial, roots, rules and expiry, warns within 30 days of expiry and fails once it has expired.

  • The same file added on both sides is one file (#101). The same path added on two bookmarks with identical content (files, symlinks, or subrepos at the same pin) now merges with no conflict and nothing to resolve. Every peer and the hub show it the same way whatever order the changes came in, and it stays one file through later edits, moves, removals and partial commits. Different content or a different mode is still a name conflict, and directories are not unified. The stored and signed formats are unchanged.

  • A bookmark nobody can verify no longer breaks every fetch (oxygenhub#266, oxygenhub#271). A delegate or short-lived key that moved a bookmark without signing any of its changes left every client failing the whole fetch with "unknown key". A fetch now carries the identities and certificates of the keys that signed every bookmark's latest attestation and every tag's, so clients, older ones included, can verify the move. A client that still cannot verify a bookmark other than the current one leaves that bookmark as it was, warns naming it and why, and fetches the rest. The current bookmark (the one a pull takes or a clone checks out) still fails, as does a missing, unauthorised or rolled-back attestation. The signers are gathered once per operation, not on every fetch call: a repository in a bucket with 145 bookmarks had taken 14 s a fetch.

  • Readers never wait for a push or gc (oxygenhub#249). A repository can now be read safely while another process writes it. A handle reads the operation head once and keeps it, so one request never mixes two views. A read that misses reloads the pack list and tries once more. A repack retires the packs and loose objects it replaces instead of deleting them, and gc deletes them only an hour later; oxy gc says what it deleted or kept for readers. A forge no longer needs to hold readers back while it pushes or collects garbage.

  • A repository kept in a bucket lists its packs less often (#116). Every open listed the bucket's packs, and a forge opens the store once per request. The pack listing is now kept for 30 seconds and forgotten the moment this process writes or deletes a pack, so its own packs are never hidden; another writer's new pack is seen within 30 seconds. Opening a store no longer sends a request per pack, and a bucket store's stat is a HEAD of the key, not a listing. A server's /health counts pack listings, kept listings and other listings apart.

  • Text that looks like a conflict marker no longer blocks a resolution (#126). A file that showed oxy's own conflict markers, such as documentation or a test fixture, could never be committed as resolved ("markers remain"). A file now still has markers only when it holds a marker line oxy wrote for its conflicts. A real marker left behind is still caught.

  • A carried switch no longer says carried files were removed (#120). After a switch that carried the working copy's changes, oxy said new files were "removed from disk" although they were still there. Carried files are now left out of what was removed, and the switch names what it carried.

0.4.6 (2026-10-17)

  • A keyless agent's grant token is its only credential (oxygenhub#241). A home holding an agent grant token and no hub token now offers the grant to that hub, which accepts it for fetch and push as an agent's token of the owner's account (writing when the grant gives refs). The agent pushes its own work, so its seal is its own; it needs no personal access token.

  • Agents' signatures say whose they are. A short-lived key is named by the subject it was certified for, so oxy log shows via agent:claude@andymccall and oxy verify shows signed by agent:claude@andymccall, rather than the author's name with an unfamiliar key.

  • oxy config --unset core.sign works. It failed with "core.sign must be change or seal"; unsetting now returns to the default.

0.4.5 (2026-10-17)

  • Agents sign with no key of their own (oxygenhub#241). With author.signer = keyless, oxy signs with a fresh key the hub certifies for ten minutes whenever it must sign, kept in memory only. The agent proves itself with a grant token its owner made with oh agent grant NAME and oxy auth grant keeps. The owner's own key signs the grant, which travels inside every certificate issued under it, so the agent's commits count in the owner's name without trusting the hub for authorship. The owner revokes the grant on the hub, and there is no key left to rotate or leak. keyless.hub names the hub (default: the default remote's).

  • Certificates for short-lived keys (spec §14.7). A hub's key certifies a fresh key for a subject such as repo:OWNER/NAME:bookmark:B or agent:NAME@ACCOUNT, with a scope and a window of at most an hour. Pushes and fetches carry the certificates of the keys that signed what they bring. oxy receipt verify reads receipts that OxygenFlow runners sign with such keys.

  • Policies grant subjects an issuer certifies. oxy policy grant-subject SUBJECT --issuer IDENTITY --scope SCOPES lets keys certified for a subject sign changes and move bookmarks. A push is checked at the receiver's clock. A fetch is checked at the time the issuer says it accepted the update, a countersigned accepted attestation the hub serves with the bookmark, so an expired key cannot backdate one.

  • A push seals for the author it signs for. A delegate or a certified key set as author.signer now seals the author's unsigned changes at push, so it no longer has to sign each change at commit (core.sign = change).

  • gc keeps short-lived keys' certificates for every change and attestation it keeps, not only receipts', so a fresh clone can still check a certified bookmark after a gc.

0.4.4 (2026-10-17)

  • A fresh clone takes tags made before a key rotation (#114). A tag's signature was checked against the current trust policy with no regard for rotation, so every tag signed by a key since rotated away from was refused, and a fresh oxy clone of a repository with such a tag failed (this repository's own v0.1.0). A tag signed by a predecessor of a key the policy names is accepted when it is dated before that key's successor was made; a superseded key makes no new ones, and revoked keys never count.

  • SLSA provenance of a receipt (oxygenhub#239). oxy receipt export --format slsa writes what a valid receipt says as an in-toto Statement with an SLSA provenance v1 predicate, in a DSSE envelope signed with your key, the runner's signed receipt riding along inside it. oxy identity export --pem writes the public key cosign and other DSSE verifiers check it with.

  • Repositories kept in a bucket answer quickly (oxygenhub#228, #234). A forge serving a repository from GCS or S3 opened a new client for every request, read large packs by range without keeping what it read, asked the bucket for a loose copy of every packed object it read, and asked again and again about objects it lacked. Clients are now shared per bucket and credentials, ranges and sizes are kept on disk, packs are looked in first, and a key found missing is taken as missing for 30 seconds unless this process writes it. On a hosted repository in GCS, negotiate went from 7-10 s to well under a second and bucket requests from about 540 a minute to about 30. The store counts its requests by kind, for a server to show.

  • A quicker log and an instant clean diff (#94). The workspace's state index keeps its changes newest first, and the log takes them as a list, with no map of every change and no sort. A diff whose working copy changes nothing is empty without loading the base. On VS Code at 20 000 commits, log -n 20 went from 34 ms to 30 ms and a clean diff from 48 ms to 30 ms.

  • A credential helper that does not read its input works (#115). An erase that exits without reading what oxy sends it failed now and then with Broken pipe; a closed pipe is no error now, as in Git.

  • gc leaves a fully packed store as it is. gc repacks at its harder compression level, and a store that was already one such pack with nothing loose was rewritten every time, the same pack again: 37 seconds for this repository's 48 000 objects on a forge, nightly, under the repository's write lock. Such a store is now left alone.

  • A switch names what it takes off disk (#102). oxy bookmark switch and oxy checkout printed only how many files they removed, easy to miss when a switch to a bookmark lacking 80 files took an editor's project file with them. They now name the first few removed files, with a warning when twenty or more go.

  • Binary files' current versions found without whole closures (#107). Which replacement of a binary file is live was decided by building each replacing change's entire closure; on a history as long as VS Code's (166,000 commits) each is a hundred thousand changes, the memo could not keep them, and oxy git verify spent its day rebuilding them. A bounded walk that keeps nothing answers the same.

  • --carry takes work the target already has (#100). Switching bookmarks or checking out with --carry refused when uncommitted files were byte-for-byte what the target state holds, as after a proposal of that work landed, leaving only --discard. Such work is now already applied: no conflict, nothing to carry, and the rest of the work comes along.

  • Full hashes as --json prints them (#103). A change named by its full 64-digit hash without the #, as oxy bookmark show --json and the hub's API print heads, resolves wherever a change is named (bookmark set, bookmark create, checkout, …), so JSON output feeds back into commands.

  • Large new files are not snapshotted unseen, and past snapshots expire (#98). A 1 GB image left untracked in a working tree was taken in by the next status and kept by the operation log for good: deleting the file and running gc --now freed nothing. Now a new, untracked file over snapshot.max-new-file-size (default 100 MiB) is left out and named by status, with how to ignore it or raise the limit; tracked files and files already in the working copy are unaffected. Past working-copy snapshots keep their files only for core.snapshot-retention (default 7 days), so gc prunes what only they held, and oxy gc --expire-snapshots --now does it at once. undo and op restore to an operation whose working copy has expired keep the files as they are.

  • oxy undo undoes this workspace's last command (#99). It undid the repository's newest operation, whichever workspace made it: in a repository with several workspaces an undo could take back another workspace's commit or merge, and it said only which operation it went back to. Operations now record their workspace, and undo takes back this workspace's last command, leaving other workspaces' work as it is. On a bookmark both share, only its own heads go; a change another workspace built on it, or the workspace it would take away, refuses the undo and names it. Undo now says which command it undid and what that changed, passes over pushes (the remote keeps what it was sent), and warns when an undone commit is already on a remote. oxy amend warns when it rewrites a change a remote already has, so the next push will need --force.

  • gc can keep what a host names (oxygenhub#126). Repo::gc_keeping keeps extra blobs and their chunks beside what history reaches, so a forge's job artifacts, referenced only from its own database, survive garbage collection.

  • oxy remote remove removes all of a remote (#86). It blanked the URL alone: the token, CA and store settings stayed in the config and the remote's tracking bookmarks in the view, read later as if current. Now every remote.NAME.* key goes, with its tracking bookmarks and a store remote's mirror; an unknown name is refused.

0.4.3 (2026-10-10)

  • Claim your own commits in a Git import (#97). oxy git import --claim and oxy git fetch --claim set git.claim: a commit whose Git author email is your identity's email is authored with your key (and sealed by you on push), so a forge credits it to your account; anyone else's commits keep the zero key, as before.

  • Git imports of long histories much faster (#96). Rendering a file asks whether the changes that deleted and inserted neighbouring lines depend on each other; in a deep history each answer was a walk through thousands of changes, asked again for every line, under a lock every rendering thread queued on. Answers are now remembered for the file being rendered, the shared memos are read without excluding other threads, and a merge renders its files on every core. On VS Code past 66 000 commits, 500 commits imported in 261 s rather than 734 s, with the same history.

  • A long push goes in parts (#95). oxy push sent a whole history in one request, held in memory on both sides; the full VS Code import would have passed a hub's 4 GiB limit. It now sends parts of about push.batch-bytes (default 256 MiB) in dependency order, objects alone, and the last part moves the bookmark; the spinner says which part is going. A server stores a part and reports it as objects.

  • A state index read only as far as it is used (#94). Each file's history in the workspace's state index is stored after the rest with a hash of its own, and is read and checked only when a command builds that file, so a diff or a one-file cat no longer reads and hashes the histories of every file (67 MB of an 84 MB index on VS Code at 20 000 commits). Changes added on top of an index no longer decode the histories they touch. A history found damaged is an error that says so, and the index is removed and rebuilt by the next command. Under load on that repository, diff went from 85 ms to 63 ms and cat/blame from 102 ms to 80 ms.

  • Removing a file removes every copy of it (#88). When two lines of history each put a file back in the same place, it has two placements there and shows as one file. Removing or moving it retired only one, so the other stayed behind, and once its directory went it was an orphan: a conflict in the state. It is the one difference the Git import of VS Code had in its first 31 000 commits. Both now go together, in a commit and in a Git import.

  • A quicker log and diff on long histories (#94, first part). oxy log no longer reads the working copy's metadata through the store's cache, walks the history with a fast hash and without copying each change's metadata, and works out the shortest unique id prefixes for the changes it shows rather than for every change. The state index is read without copying the file histories in it, and its change list is checked on every core. On VS Code at 20 000 commits, log -n 20 went from 74 ms to 40 ms and loading the state index from 43 ms to 28 ms, with the same output.

  • Faster commits, and the state index after a checkpoint (#93). A state loaded through a checkpoint read the changes after it whole, text included, so it could never be saved as the workspace's state index and every command after a checkpoint loaded history the long way (25 s for the first commit on VS Code at 20 000 commits). It now reads them as the index needs them. oxy commit PATHS no longer takes a second snapshot to count what it left when it took everything. A one-file commit on VS Code went from about 450 ms to 300 ms; 70 ms of that is the store's fsync, which stays.

  • Reading one file reads one file (#92). oxy cat rendered every file of the state to print one. It now builds only that file, from the workspace's state index when it reads the working copy's base or the state it is on. On VS Code at 20 000 commits, cat package.json -r main went from 26 s to 0.10 s and at a change from 2016 from 0.66 s to 0.16 s; on the full 166 000-change history it takes 1.9 s and 1.3 GB, where it ran for minutes and used 9 GB.

  • A file's history in a fraction of a second (#91). oxy log --path rendered every file of the tree twice just to find the path, and both it and the touches() revset decoded every change of the history, its text included, to see what each one touched. They now read the path from the tree alone and keep the nodes each change touches in a cache beside the store (.oxy/index/touched, rebuilt if missing). On VS Code at 20 000 commits, log --path package.json went from 53.6 s to 0.16 s (Git: 0.15 s) and touches("package.json") from 3.4 s to 0.16 s, with the same output.

0.4.2 (2026-10-09)

  • pull brings a working copy that fell behind up to its bookmark (#89). The working copy followed a pull only from exactly the bookmark's old heads, and not at all when the pull brought nothing new, so one left behind by bookmark set, a landing or a pull from another workspace stayed on old code while status said clean. It now follows whenever its base is part of the bookmark's state, clean or carrying uncommitted work, and status says when the working copy is behind its bookmark and by how many changes.

  • Faster merges in a Git import (#80). A merge is built from only the paths it can change, as an ordinary commit is: on VS Code at 139 000 commits merges import 3.2 times faster (9.5 s against 30.9 s on average) and the import as a whole 2.7 times faster, with exactly the history the whole-tree path builds. OXY_IMPORT_WHOLE_MERGES=1 takes the old path.

  • A binary file changed and made executable at once keeps both (#87). oxy commit and the Git import dropped the executable bit when a binary file got new contents in the same change.

  • Several remotes and no origin (#83). core.remote names the default remote; bookmark list compares with a remote that has the bookmark and names it; the error without a default says what to do.

  • No i386 notice from apt (#85). The .deb's apt source names the machine's own architecture, and an upgrade adds it to a source written before, so a system with i386 enabled no longer warns on every update.

  • Clearer messages. A missing author identity says which config set it and how to pin the right one (#82); oxy git verify --max-differences N and a summary of what differs by kind.

0.4.1 (2026-10-08)

  • The prompt agrees with status in a repository with subrepos (#75). Straight after a commit or a merge, oxy prompt took a subrepo's directory for a recently changed file and showed * until a full oxy status ran. Subrepos are now compared by their pins.

  • Catching up a stale bookmark (#74). oxy bookmark switch brings a bookmark that is only behind its remote up to it first, rather than checking out the stale state. oxy pull takes uncommitted work along when the pulled changes leave its files alone, instead of leaving the working copy behind, and checkout --carry no longer counts a subrepo already at the target's pin as a change.

  • bookmark set fast-forwards (#78). Moving a bookmark onto work built on its newest head keeps the older changes it held that the new one doesn't depend on, as --upto does, and says so. A head the bookmark already holds is still refused without --upto or --only.

  • A bookmark's heads by name (#73). oxy bookmark show [NAME] lists each head, newest first; bookmark list -v does it for every bookmark, and bookmark list names the newest head's subject. bookmark(NAME) in a revset is a local or remote bookmark's heads and nothing else.

  • Clearer messages. submodule status and update name .oxymodules entries that were never added, with the command that pins them, and update exits 1 (#72). oxy cat -r CHANGE's help and error say that a change means its dependency closure, and suggest -r BOOKMARK or --at-op (#76).

  • A long Git import can stop and carry on (#77, #79). An import moves its bookmark to the commits done so far every 2 000 commits, so oxy git fetch resumes it. oxy git recover rescues an import that stopped before its first checkpoint, or ran on an older oxy: it indexes the objects already written (each checked, a torn last one cut off) and points the bookmark at them. It recovered 139 086 commits of VS Code in under two minutes. oxy git fetch ../path with a relative path works.

  • oxy git verify in bounded memory (#81). It held the ancestry of every commit and ran out of memory on VS Code; it now holds only the commits whose children are still to come, and checks 120 000 commits in under 1 GB. On an import not finished yet it stops at the last imported commit, counts --last from there, and says how many are still to come.

  • Measuring an import (#80). OXY_IMPORT_STOP_AFTER=N ends an import cleanly after N commits with its timings; the trace gives each commit's time and which path each merge took. Merges built from only the paths they can change are in, behind OXY_IMPORT_PARTIAL_MERGES=1 until verified at depth: on VS Code a small merge goes from about 18 s to under a second. An import stopping early no longer dies of SIGPIPE.

  • Cloud stores take credentials handed over (#68). oxy_strata::Credentials (an S3 access key, a Google service account key, an Azure service principal) can be given when a store is opened, and a program can register a lookup for the stores it opens by URL. With them, nothing from the environment or the providers' files is used, which lets a hub keep each repository's credentials itself. Their debug form never shows a secret.

  • A repository kept on a store (#68). oxy init --store URL keeps a repository's objects and operation log on dir://, S3, GCS or Azure, behind a local cache of storage.cache bytes (2 GiB by default) that can be deleted at any time. Checked against the real services: a commit takes 0.5 to 0.9 seconds, and reopening with no cache about half a second.

  • A caching store, and the operation head on a store (#68). strata::CachingStore puts a local directory in front of any store: reads are kept, writes go to the remote first, the cache stays under a size limit by removing what was used least recently, and references always come from the remote. A repository kept on a store other than this disk's directory now holds its operation head there, moved by compare-and-swap with merge and retry.

  • A large merge's file no longer imports empty (#71). When a merge rendered its files in batches, a file whose graph the import had already built came out empty, so the merge recorded the whole file as inserted again and conflicted with itself: VS Code's history failed verification from 2016 on. Built graphs are now copied. The 10 412-commit reproducer imports and verifies every commit.

  • Git import holds a budget of graph memory (#25). A long-lived file's graph keeps every line it ever had, and the import shed a kept state's graphs only past 2 000 of them; it now also sheds them past 1.5 million atoms, and gives freed memory back after each merge. A 28 637-commit slice of VS Code imports at 5.1 GB instead of 16.9 GB, in 36 minutes instead of 61, with the same verify result. The dependency check no longer memoises every queried change's whole ancestry.

  • A rotation follows you everywhere (#69). delegate add, delegate remove, rotate and renew record the successor key in the user config when it named the old key, and in the repository's own file when that pinned it, so every checkout moves together. oxy identity show warns when a repository pins a key other than the user config's; oxy config --unset KEY drops a key; oxy identity delegate add takes several identity files in one rotation and reminds you that agents configured with your author.key need the new one.

0.4.0 (2026-10-04)

  • Oxygen Strata: a repository on a bucket (#63). oxy remote add NAME s3://BUCKET/PREFIX (--profile, --region, --endpoint), gs://BUCKET/PREFIX (--credentials), az://ACCOUNT/CONTAINER/PREFIX, or dir://PATH, then push, pull, clone and gc against it as against any remote, with no server in between: objects are written once, the references record moves by compare-and-swap, and two pushers merge and retry rather than lose each other's work. Credentials come from each cloud's own chain. oxy remote check runs the conformance suite against a store before it is trusted. docs/strata.md is the guide; the bucket layout is not stable before 1.0.

  • The object store on any Store (#68). .oxy/objects is now the same Store a bucket is, with the same keys and the same speed: the first step towards a repository that lives in its owner's bucket with the hub as one client of it. Signatures, seal and receipt entries are written once each, so two writers of a shared store never lose each other's; older repositories still read.

  • Scoped, expiring delegates (#67). oxy identity delegate add FILE --scope changes --expires 8h lets an AI agent sign changes for you while it can never move a bookmark or tag, or seal, whatever you may do; a host that lands for you gets changes,refs. You can revoke a key you delegated to. Older identities still read: a bare key means every scope.

  • An agent signs as itself, you stay the author. The agent's keys live in its own $OXY_HOME, and author.signer (or $OXY_SIGNER) names its key, so oxy commit in your checkout carries you as author and the agent's signature. oxy log says via NAME beside the author, oxy show has a via row with the agent's identity and the delegation it acted under, oxy identity show names each delegate, and oxy log -r 'delegated()' lists what agents did. The guide's §9 has the setup.

  • Revsets (#65). oxy log -r, show -r, verify -r and abandon -r take an expression over history: symbols (bookmarks, origin/main, tags, change prefixes, @), ::X, X::, X..Y, |, &, ~, deps, dependents, heads, roots, latest, independent, author, description, date (-7d..), kind, touches (through renames), conflicts, and the trust predicates signed_by, delegated, unsigned, revoked, attested, built. Saved revsets: oxy config revset.mine 'author(me) & ::@' makes mine(). A first page takes about 10 ms on this repository; evaluation has a time budget. docs/revset_query_language.md.

  • oxy gc and oxy git import say what they are doing (#66, #25). Each phase and count of a gc, and every 500 commits of an import with the commit in hand, through the spinner or as lines without a terminal.

  • Large merges import in batches (#25). A merge touching thousands of files builds and renders its graphs 500 files at a time: a 10 000-commit slice of VS Code imports in 135 s at 1.6 GB (from 140 s at 1.6 GB, and far less for the merges alone). The whole history still needs more than 20 GB at its largest merges, which is the next piece of work.

  • Release builds link on every target. The cloud stores send through ureq, so macOS and Windows builds no longer pull in a C library the cross-compiler cannot link.

0.3.1 (2026-10-02)

  • RISC-V (#59). Builds for 64-bit RISC-V Linux (linux-riscv64 and a riscv64 .deb), tested natively on an OrangePi RV2.

  • apt (oxygenhub#85). The .deb adds the repository at https://oxygenhub.io/apt on its first install, so apt upgrade keeps oxy current.

  • Built by OxygenFlow. Every file of this release comes from the dist job of the release commit's run, signed by its runner: the release's trust page shows each as an attested build.

  • Being on the bookmark you asked for (#60). oxy checkout BOOKMARK switches to that bookmark (as oxy bookmark switch does, with --discard or --carry), where it used to write its files under the current bookmark's name, so the next commit moved the wrong bookmark. oxy workspace add PATH BOOKMARK makes a workspace on that bookmark rather than this one's.

  • A new workspace has its subrepos (#61). oxy workspace add brings each subrepo at its pin from this workspace's own copy, with no network or sign-in, its remote pointing where the subrepo comes from.

0.3.0 (2026-10-02)

  • Fetch says when a bookmark is behind (#56). oxy fetch reports each bookmark against the remote's copy ("main is 1 change behind: oxy pull to take it", ahead, diverged) and says "up to date" only when they match; --json gives ahead and behind. A change that was fetched but not pulled resolves by id or hash, and oxy show says it is only in origin/main. With one remote not called origin, fetch, pull and push use it, and bookmark list and the prompt count against it.

  • Clones record local paths absolute (#57). oxy clone ../repo copy and oxy remote add NAME ../repo store the absolute path, so fetch, pull and push still find the remote from inside the clone.

  • Faster commits (#41). A commit writes the manifest once, its own snapshot neither records an operation nor encodes the change twice, syncs are batched, and edited files are diffed on every core: a one-file commit on the Linux tree takes 0.3 to 0.5 s (from about 3 s; Git 0.15 s, which does not sync), 1 000 files 0.8 s (from 4.9 s).

  • First commit of a large tree in half the time and less memory (#41). The Linux tip commits in 12 to 17 s (from 38 s; Git 32 s), peaking at 2.6 GB resident (from 4.4 GB): line offsets take four bytes, inline lines are encoded without a value each, stored texts go at most 32 MiB a batch, and oxy commit (and describe, amend, abandon, split, squash, rebase) read only the new change's metadata to print it.

  • Diff and fsck (#41). A diff answered from the status cache renders only the files it shows (one file on the Linux tree 0.15 s, from 1.2 s; 1 000 files level with Git), and fsck checks objects in parallel (8 to 10 s, Git 12 to 13 s).

  • Blame twice as fast (#41). oxy blame prints through a buffered writer and formats each change once, and the state index's tree view loads in one step: blame of the Linux tree's MAINTAINERS (30 000 lines) is 0.08 s, from 0.16 s; tuned Git 0.06 s. The faster tree view also takes 16 ms off diff, commit and checkout.

  • oxy log in milliseconds (#41). Log no longer checks the files on disk: its working-copy row shows the working copy as last recorded, "(working copy)" or "(working copy, uncommitted changes)", as Git's log never looks at the working tree; oxy status checks the disk. log -n 100 is 0.03 s on the Linux tree (from 0.17 s) and 0.02 s on a 10 000-commit history.

  • Status twice as fast as Git on the Linux tree (#41). With uncommitted work, oxy status answers from a status cache while every file is as the last snapshot left it, and the manifest keeps every directory's time, so the tree is listed only when a directory's entries changed. One edited file is 0.10 to 0.12 s (from 0.44 s; Git 0.22 s) and a clean status 0.09 to 0.12 s (Git 0.20 to 0.26 s), in 80 MB. oxy diff renders the working copy from the cache too (0.20 s, from 0.41 s; Git 0.08 s). A commit after a status takes that status's snapshot instead of walking the tree again (0.37 s, from 0.68 s; Git 0.15 s). A file written within the racy window is now hashed by the prompt's check too, as it was by status.

  • Quieter notes (#55). Notes start with a dimmed › instead of ℹ, which many terminal fonts draw as a boxed or emoji-style "i". A spinner's final line no longer shows two ticks or runs into the next line.

0.2.0 (2026-10-01)

  • The operation log says what each operation did (#51). oxy op log gives each operation a summary, such as "main: 1 → 2 heads; working copy now 91ab4d50", with its command dimmed beneath. Runs of working-copy snapshots fold into one row, and -n limits the listing. --json adds each operation's changes.

  • oxy op restore, oxy op revert and --at-op (#52). op restore OP brings back the bookmarks and working copy as they were after OP. op revert OP undoes OP alone and keeps everything after it. --at-op OP runs any read-only command on the repository as it was then. Restore and revert are operations too, so oxy undo takes them back. undo now takes back a command's own checkout with it (pull and restore record one), and brings the files back when nothing uncommitted would be lost.

  • Commands say where they left you (#53). Commit, checkout, switch, rebase, merge, amend, abandon, pull, undo, restore and revert end with the working copy and the bookmark's heads, newest first, as jj does.

  • oxy log is a graph (#50). Each change takes two lines: its ID (the shortest unique prefix bright), author, time, bookmarks and tags, hash and signature, then its subject. Lanes follow the change each one most directly builds on, and join where two lines of work meet, so independent work shows side by side. A dependency further than twelve rows down is marked ~, which keeps the graph to a few lanes even on a long history. --json is unchanged.

  • A remote's bookmarks work like local ones (#48).

    • origin/NAME names a remote's bookmark wherever a bookmark is expected, and oxy bookmark list --all lists those with no local bookmark.
    • oxy bookmark switch NAME makes a local bookmark from origin/NAME when there is none (REMOTE/NAME picks another remote).
    • oxy pull updates every local bookmark the remote has too, not only the current one, and names the others it moved.
    • oxy clone checks out the remote's default bookmark (servers now name it in refs), says how many other bookmarks the remote has, and takes --recursive to clone the subrepos. A clone, switch or pull says when subrepos are not at their pins.
  • A newer repository format names the oxy that cannot read it (#49). Instead of "unsupported format version 2", oxy says the data was written by a newer oxy, and gives its own version and path, which shows an older copy earlier on PATH.

  • Sign in once per hub: oxy auth login, status and logout (#47). oxy auth login https://oxygenhub.io signs in through the browser, and every repository on that hub then clones, fetches and pushes with the token, subrepos included. Nothing secret goes into a checkout any more.

    • The token is kept in ~/.config/oxy/credentials.toml, readable by you only, or in the system keyring through a Git credential helper (oxy config --user auth.helper libsecret).
    • --with-token reads a token from standard input, for CI and scripts. oxy auth logout also revokes the token on the hub.
    • A token goes over https only (plain http only to this machine), never to another host after a redirect. A credentials file that others can read is ignored.
    • Lookup order: remote.<name>.token, OXY_TOKEN, the store, then an enclosing checkout's remote on the same hub. Clones no longer copy a token into a subrepo's configuration.
    • Identity secret keys are now created owner-only, rather than tightened just after being written.

Measured on the Linux kernel's tree (95 943 files); docs/benchmarks.md has the tables.

  • Faster than Git on the first commit, a clean status, fsck, gc and the pack's size.
    • First commit: 21.9 s (Git 33.7 s).
    • Clean status: 0.17 to 0.19 s (Git 0.22 s).
    • fsck: 8.2 s (Git 12.6 s).
    • gc: 33.7 s (Git 70 s).
    • The pack: 293.5 MB (Git's 295.5 MB).
  • Level with Git on a local clone (about 17 s). Status with one file changed, diff, commit, log, blame and checkout are two to six times faster than at 0.1.0. They are still behind Git:
    • Status with one file changed: 0.41 s, against 0.22 s.
    • Commit: 0.67 s, against 0.21 s.
    • Blame: 0.16 s, against 0.07 s.
    • Checkout: 0.74 s, against 0.35 s.
  • A project's .gitignore files count, below .oxyignore. A project coming from Git no longer commits its build output, and ! in .oxyignore can bring back what .gitignore leaves out. Ignore rules still apply to untracked paths only.
  • A folder holding a repository of its own is named. oxy status, oxy commit and status --json (untracked_nested) name a folder with its own .git or .oxy, rather than skipping it silently. They say how to track its files, keep it as a subrepo, or leave it out.
  • oxy rebase CHANGE --onto T --into-working-copy. Where a rebase would be conflicted, the target is merged into the current bookmark instead, and the conflict is resolved in the working copy, as after oxy merge.
  • mimalloc is the allocator. Most of the speed above came from it: status, walks and index loads allocate many small values from every thread.
  • Internals. Each item below is a cache and is rebuilt when it does not check:
    • the workspace manifest is a checksummed binary file;
    • the state index is decoded lazily, caught up by replaying a few changes, and kept for the last two states;
    • the store caches change metadata;
    • a pack can carry a trained zstd dictionary.

0.1.0 (2026-09-30)

  • The first builds for people to download.

    • Linux x86_64 and arm64 (glibc 2.28 or later) as .tar.gz and .deb.
    • macOS x86_64 and arm64.
    • Windows x86_64.

    All are cross-compiled from Linux with scripts/dist.sh, with SHA256SUMS. A prerelease: formats may still change before 1.0.