OXYGENHUB
# b689779e9e74 oxygen / CHANGELOG.md
26.9 KiB · Markdown · last changed Snapshots: large new files left out and named; past working copies expire 25 days ago renderedsource blame history raw

Changelog

Unreleased

  • 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.