Changelog
Unreleased
- A shallow clone of a big repository works, and quickly (#137).
oxy clone --depth 0of 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 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 413with 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.keyread 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 anuntagattestation naming the tag's state, signed like a tag (a ten-minute keyless key too), and the nextoxy pushsends it to a remote that still has the tag;oxy push --delete tags/NAMEdoes 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 afetchwithdraws 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'sBackendgainsvet_tag_withdrawal(OxygenHub refuses while a release uses the tag), andRepo::host_tag_createandhost_tag_withdrawallet 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 --verifyand the hub said INVALID ("grant is by an unknown author"). OxygenHub now sends the grant's signer with the certificate (grantor_identity, additive),oxykeeps 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 oroxy submodule update. Clients now sendUser-Agent: oxy/<version>and declare what they understand inOxy-Features(certified-tagsso far; docs/protocol.md, Client capabilities). Under a policy, a server leaves out of its refs answer to a client that does not declarecertified-tagsevery tag that only a certificate authorizes, so that client clones the rest as if the tag did not exist. The rule isRepo::refs_for_clientin oxy-core;oxy-servergains aBackend::adjust_refshook, given the request headers, whose default applies it, sooxy serveand every embedding host do. - Keyless agents can create release tags (#129).
oxy tag createandoxy pushunder 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 inrefsandpushgain 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), andoxy gcfolds 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 firstoxy gcalso 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 nextoxy gc, so upgrade every client that writes to the bucket first, then runoxy gconce. A server's/healthcounts journal reads asread_journal. A repository's own directory works as before. -
A server can refuse a push by what it adds (oxygenhub#268).
oxy-server'sBackendhas avet_pushstep, called with the push's changes before anything is stored or moved; OxygenHub uses it for push protection (secret scanning). The refusal reachesoxy pushas 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 byfetchandpulland 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 verifyaudits 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);--jsontoo. 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-witnessadd-checkpointcall. 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.--jsongives each file's counts. -
oxy mergetakes a remote's bookmark (#117).oxy merge oxygenhub/developsaid "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 doctorchecks 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
gcdeletes them only an hour later;oxy gcsays 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
/healthcounts 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 logshowsvia agent:claude@andymccallandoxy verifyshowssigned by agent:claude@andymccall, rather than the author's name with an unfamiliar key. -
oxy config --unset core.signworks. 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 withoh agent grant NAMEandoxy auth grantkeeps. 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.hubnames 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:Boragent: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 verifyreads receipts that OxygenFlow runners sign with such keys. -
Policies grant subjects an issuer certifies.
oxy policy grant-subject SUBJECT --issuer IDENTITY --scope SCOPESlets 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 countersignedacceptedattestation 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.signernow 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 cloneof 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 slsawrites 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 --pemwrites 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 20went from 34 ms to 30 ms and a cleandifffrom 48 ms to 30 ms. -
A credential helper that does not read its input works (#115). An
erasethat 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 switchandoxy checkoutprinted 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 verifyspent its day rebuilding them. A bounded walk that keeps nothing answers the same. -
--carrytakes work the target already has (#100). Switching bookmarks or checking out with--carryrefused 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
--jsonprints them (#103). A change named by its full 64-digit hash without the#, asoxy bookmark show --jsonand 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
statusand kept by the operation log for good: deleting the file and runninggc --nowfreed nothing. Now a new, untracked file oversnapshot.max-new-file-size(default 100 MiB) is left out and named bystatus, 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 forcore.snapshot-retention(default 7 days), sogcprunes what only they held, andoxy gc --expire-snapshots --nowdoes it at once.undoandop restoreto an operation whose working copy has expired keep the files as they are. -
oxy undoundoes 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 amendwarns 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_keepingkeeps 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 removeremoves 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 everyremote.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 --claimandoxy git fetch --claimsetgit.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 pushsent 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 aboutpush.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 asobjects. -
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,
diffwent from 85 ms to 63 ms andcat/blamefrom 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 logno 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 20went 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 PATHSno 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 catrendered 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 mainwent 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 --pathrendered every file of the tree twice just to find the path, and both it and thetouches()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.jsonwent from 53.6 s to 0.16 s (Git: 0.15 s) andtouches("package.json")from 3.4 s to 0.16 s, with the same output.
0.4.2 (2026-10-09)
-
pullbrings 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 bybookmark set, a landing or a pull from another workspace stayed on old code whilestatussaid clean. It now follows whenever its base is part of the bookmark's state, clean or carrying uncommitted work, andstatussays 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=1takes the old path. -
A binary file changed and made executable at once keeps both (#87).
oxy commitand 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.remotenames the default remote;bookmark listcompares 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 Nand 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 prompttook a subrepo's directory for a recently changed file and showed*until a fulloxy statusran. Subrepos are now compared by their pins. -
Catching up a stale bookmark (#74).
oxy bookmark switchbrings a bookmark that is only behind its remote up to it first, rather than checking out the stale state.oxy pulltakes uncommitted work along when the pulled changes leave its files alone, instead of leaving the working copy behind, andcheckout --carryno longer counts a subrepo already at the target's pin as a change. -
bookmark setfast-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--uptodoes, and says so. A head the bookmark already holds is still refused without--uptoor--only. -
A bookmark's heads by name (#73).
oxy bookmark show [NAME]lists each head, newest first;bookmark list -vdoes it for every bookmark, andbookmark listnames the newest head's subject.bookmark(NAME)in a revset is a local or remote bookmark's heads and nothing else. -
Clearer messages.
submodule statusandupdatename.oxymodulesentries that were never added, with the command that pins them, andupdateexits 1 (#72).oxy cat -r CHANGE's help and error say that a change means its dependency closure, and suggest-r BOOKMARKor--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 fetchresumes it.oxy git recoverrescues 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 ../pathwith a relative path works. -
oxy git verifyin 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--lastfrom there, and says how many are still to come. -
Measuring an import (#80).
OXY_IMPORT_STOP_AFTER=Nends 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, behindOXY_IMPORT_PARTIAL_MERGES=1until 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 URLkeeps a repository's objects and operation log ondir://, S3, GCS or Azure, behind a local cache ofstorage.cachebytes (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::CachingStoreputs 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,rotateandrenewrecord 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 showwarns when a repository pins a key other than the user config's;oxy config --unset KEYdrops a key;oxy identity delegate addtakes several identity files in one rotation and reminds you that agents configured with yourauthor.keyneed 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, ordir://PATH, thenpush,pull,cloneandgcagainst 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 checkruns the conformance suite against a store before it is trusted.docs/strata.mdis the guide; the bucket layout is not stable before 1.0. -
The object store on any Store (#68).
.oxy/objectsis now the sameStorea 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 8hlets 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 getschanges,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, andauthor.signer(or$OXY_SIGNER) names its key, sooxy commitin your checkout carries you as author and the agent's signature.oxy logsaysvia NAMEbeside the author,oxy showhas aviarow with the agent's identity and the delegation it acted under,oxy identity shownames each delegate, andoxy log -r 'delegated()'lists what agents did. The guide's §9 has the setup. -
Revsets (#65).
oxy log -r,show -r,verify -randabandon -rtake 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 predicatessigned_by,delegated,unsigned,revoked,attested,built. Saved revsets:oxy config revset.mine 'author(me) & ::@'makesmine(). A first page takes about 10 ms on this repository; evaluation has a time budget.docs/revset_query_language.md. -
oxy gcandoxy git importsay 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-riscv64and ariscv64.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 upgradekeeps 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 BOOKMARKswitches to that bookmark (asoxy bookmark switchdoes, with--discardor--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 BOOKMARKmakes a workspace on that bookmark rather than this one's. -
A new workspace has its subrepos (#61).
oxy workspace addbrings 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 fetchreports 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;--jsongivesaheadandbehind. A change that was fetched but not pulled resolves by id or hash, andoxy showsays it is only inorigin/main. With one remote not calledorigin, fetch, pull and push use it, andbookmark listand the prompt count against it. -
Clones record local paths absolute (#57).
oxy clone ../repo copyandoxy remote add NAME ../repostore 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 blameprints 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 login 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 statuschecks the disk.log -n 100is 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 statusanswers 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 diffrenders 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 loggives 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-nlimits the listing.--jsonadds each operation's changes. -
oxy op restore,oxy op revertand--at-op(#52).op restore OPbrings back the bookmarks and working copy as they were after OP.op revert OPundoes OP alone and keeps everything after it.--at-op OPruns any read-only command on the repository as it was then. Restore and revert are operations too, sooxy undotakes them back.undonow 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 logis 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.--jsonis unchanged. -
A remote's bookmarks work like local ones (#48).
origin/NAMEnames a remote's bookmark wherever a bookmark is expected, andoxy bookmark list --alllists those with no local bookmark.oxy bookmark switch NAMEmakes a local bookmark fromorigin/NAMEwhen there is none (REMOTE/NAMEpicks another remote).oxy pullupdates every local bookmark the remote has too, not only the current one, and names the others it moved.oxy clonechecks out the remote's default bookmark (servers now name it inrefs), says how many other bookmarks the remote has, and takes--recursiveto 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,statusandlogout(#47).oxy auth login https://oxygenhub.iosigns 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-tokenreads a token from standard input, for CI and scripts.oxy auth logoutalso revokes the token on the hub.- A token goes over
httpsonly (plainhttponly 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.
- The token is kept in
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
.gitignorefiles count, below.oxyignore. A project coming from Git no longer commits its build output, and!in.oxyignorecan bring back what.gitignoreleaves out. Ignore rules still apply to untracked paths only. - A folder holding a repository of its own is named.
oxy status,oxy commitandstatus --json(untracked_nested) name a folder with its own.gitor.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 afteroxy 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.gzand.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. - Linux x86_64 and arm64 (glibc 2.28 or later) as