OXYGENHUB
# a405d4c99440 oxygen / docs / guide.md
19.4 KiB · Markdown · last changed Execution receipts: the receipt object and attestation 1 month ago renderedsource blame history raw

Oxygen user guide

This guide is organised by task. It assumes the oxy binary is on your path (cargo build --release puts it in target/release/). The design behind every command is in spec.md; this guide only tells you what to type and what to expect.

Oxygen tracks changes rather than snapshots: a change is the set of edits you made, it commutes with unrelated changes, and conflicts are recorded in history instead of blocking you. Two ideas recur below:

  • A bookmark (default main) names a state: the set of changes it contains. A state can have several heads when independent changes have not been related to each other; that is normal, not a problem.
  • The working copy is itself a change that Oxygen rewrites every time it looks at your files. oxy commit turns it into a real change.

Output uses colour and glyphs on a terminal; pipe it, pass --plain, or set NO_COLOR for plain ASCII.

1. Set yourself up

oxy identity create --name "Ada Lovelace" --email [email protected]
oxy config --user core.sign change

identity create generates a composite Ed25519 + ML-DSA-65 key pair, keeps the secret in ~/.config/oxy/keys/, and records you as the default author in ~/.config/oxy/config.toml. With core.sign = change every commit is signed; without it, changes are sealed in batches when you push (also authenticated, just later). Use --local to record the identity in one repository only.

oxy identity show prints the key, its expiry, delegates, and whether the secret is available. oxy identity export writes the public identity to a file to hand to whoever manages a trust policy (section 8).

2. Start a repository

mkdir project && cd project
oxy init
oxy status

oxy init creates .oxy/. Add .oxyignore (gitignore syntax, per directory, negations with !) for files that should never be tracked; /target and *.tmp are typical entries. As in Git, ignore rules only affect untracked files: a file already in history stays tracked until you delete it.

To take over an existing Git project, see section 12.

3. Everyday work

oxy status                 # what changed since the last commit
oxy diff                   # the edits, file by file
oxy commit -m "Add retry loop"
oxy log                    # newest first; -n 10, -v for full messages
oxy show 658990aa          # one change by ChangeId prefix, #hash prefix, or @

Commit part of what changed with oxy commit PATH.. -m "..." (only those paths; the rest stays as uncommitted work) or oxy commit -i -m "...", which offers each hunk in turn like git add -p; a hunk that another chosen hunk needs, such as the directory a new file lives in, comes along by itself.

status --json prints the same for tools, with every conflict's kind, path and contributing ChangeIds. On a terminal, status lists each path with a mark: ✚ added, ✎ modified, ✖ removed, ➜ renamed, ⚙ mode. Renames are detected: a removed file and an added file of the same kind whose contents are at least half alike are recorded as a move of the same file, so identity survives a rename.

Files containing NUL bytes are stored whole ("opaque"); text files are stored line by line and merge line by line. .oxyattributes overrides the guess (section 14).

Every change has two names: a ChangeId (658990aa…) that survives rewrites, and a content hash (#7469d16c…) that changes when the change is edited. Use whichever you have; prefixes work.

3a. Bookmarks: branches without merges

A bookmark names a state; each workspace works on one (main to begin with). Because changes commute, "merging" is the union of two bookmarks' heads, with no merge change and any real conflict recorded as in section 6.

oxy bookmark create feature        # at the working copy's base
oxy bookmark switch feature        # check it out; commits now go here
oxy commit -m "Feature work"
oxy bookmark switch main
oxy merge feature                  # main now holds both; working copy follows if clean
oxy bookmark list                  # ● current, heads, ↑n/↓n against origin
oxy push -b feature                # publish a bookmark other than the current one
oxy bookmark set feature 658990aa  # move it; oxy bookmark delete feature to drop it

oxy checkout feature materialises a bookmark without switching to it, and a bookmark name works wherever a change is expected when it has one head. Each workspace remembers its own bookmark, so two directories can sit on two bookmarks of one repository.

4. Fix up history

Oxygen keeps its own record of every operation, so all of these are safe to try and to undo.

oxy describe -m "Better message"        # reword the head the working copy sits on
oxy describe 658990aa -m "..."          # reword any change; dependents are rewritten
oxy amend                               # fold the working copy into the head
oxy amend --into 658990aa               # fold it into an older change
oxy split 658990aa src/parser.rs        # split a head: those paths become a separate first change
oxy abandon 658990aa                    # drop a change; its content stays on disk
oxy squash 658990aa --into 9df9ac29     # fold a change into one it depends on
oxy rebase 658990aa --onto feature      # re-express a change against another base
oxy undo                                # undo the last operation, whatever it was
oxy op log                              # the operation log

Rewriting a change that others depend on rewrites them too, keeping their ChangeIds. Because unrelated changes already coexist, rebase is rarely needed; its job is to change what a change is said to build on, for instance to drop a dependency on something you are about to abandon. Where a dependent referred to a line that no longer exists, the reference is re-anchored to the nearest surviving line and oxy abandon prints a note about it.

4a. Who wrote this, and what broke it

oxy blame src/parser.rs          # every line: the change that introduced it, author, age
oxy blame src/parser.rs --json

Blame is exact: each line is an atom that remembers the change that introduced it, so renames cost nothing and there is no heuristic.

oxy bisect start
oxy bisect good v1.2             # a change, bookmark or tag known to be fine
oxy bisect bad main              # and one known to be broken
oxy bisect run make test         # exit 0 good, 125 skip, anything else bad
oxy bisect reset                 # back to where you were

Without run, each step checks out a state to test and you answer with oxy bisect good, bad, or skip. The candidates are the changes the bad state has that the good one lacks, ordered so every tested state is well-formed; the answer is the first change whose inclusion breaks things.

5. Move around

oxy checkout                    # the bookmark's heads
oxy checkout 658990aa           # the state of one change and its dependencies
oxy checkout v1.2               # a tag
oxy checkout --discard ...      # throw away uncommitted work first

Checking out a single older change gives you the files that change and its dependencies touch, not a full snapshot from that date: in Oxygen a change depends only on what it actually builds on.

6. Conflicts

A conflict never stops a commit, fetch, or pull. It is recorded in the state and shown by oxy status, which exits with status 2 while any conflict remains. Conflicted text files are written with markers:

<<<<<<< oxy conflict 1 of 2, side 1: change k7m2p9 "Add retry loop"
... lines ...
======= oxy conflict 1 of 2, side 2: change q3x8w1 "Refactor client"
... lines ...
>>>>>>> oxy conflict 1 of 2 end

Edit the file to what it should be (keep one side, both, or neither) and remove the markers. Tree conflicts, such as two files wanting the same name, appear as <name>.oxy-conflict-<n> candidates: keep the one you want under the right name and delete the rest.

oxy resolve      # which conflicts your edits resolve, which still have markers
oxy commit -m "Resolve retry vs refactor"

The commit is recorded as a resolution, a change that depends on every side of the conflict; anyone who fetches it gets the conflict resolved.

7. Work with others

Run a server (any machine with the repositories under one directory):

oxy serve --root /srv/oxy --bind 0.0.0.0:8890 --token SECRET \
          --tls-cert cert.pem --tls-key key.pem

Without --tls-* the server speaks plain HTTP; keep that on a loopback address or behind a TLS terminator. --token makes pushes require a bearer token; reads stay open.

On your machine:

oxy clone https://host:8890/project
cd project
oxy config remote.origin.token SECRET   # or export OXY_TOKEN
oxy config remote.origin.ca my-ca.pem   # self-signed server: extra roots to trust (or OXY_CA)
oxy push
oxy pull
  • oxy fetch updates origin/main without touching your bookmark.
  • oxy pull fetches, then makes your bookmark the union of both sides. There is no merge commit; unrelated changes simply coexist, and a real conflict is recorded as in section 6. The working copy follows when it was clean.
  • oxy push sends your changes and moves the remote bookmark. If the remote moved since you last fetched, the push is refused: pull and push again. --force overrides the compare-and-swap.
  • oxy remote add|remove|list manages remotes; the clone source is origin.

A repository can also be cloned from a local path, which is handy for tests and for a bare "hub" on a shared disk.

8. Decide who may publish: the trust policy

Without a policy a repository is unverified: everything is accepted and every remote command says so. A policy names root keys and the keys allowed to update each bookmark; servers and clients enforce it.

oxy policy init                    # your key becomes the only root; push publishes it
oxy policy show
oxy policy grant alice.identity    # alice.identity came from her `oxy identity export`
oxy policy grant ci.identity --pattern 'release/*'
oxy policy revoke 9458b656         # by KeyId prefix
oxy verify                         # signature status of every change in the log

Several people can hold root keys:

oxy policy root add bob.identity
oxy policy root threshold 2        # future updates need two root signatures

Once the threshold is above one, an update you make alone becomes a proposal file (policy-<serial>.proposal, or --out). Send it to another root holder, who runs oxy policy sign policy-7.proposal; whoever completes the threshold applies it, and the next push publishes it. Everyone else picks it up on fetch, verifying each step from the policy they already trust.

9. Keys over time

oxy identity rotate                   # fresh key pair; old signatures stay valid
oxy identity renew --expires 365      # same keys, new expiry in days (0 = never)
oxy identity delegate add ci.identity # let another key sign on your behalf
oxy identity delegate remove 3f2a…
oxy identity revoke                   # retire your own key
oxy identity revoke 3f2a… --reason "laptop stolen"   # any key, if you hold a root

Rotation, renewal, and delegation all issue a successor identity that names the previous one. If a policy names your key, it is updated to the successor in the same step (as a proposal when more root signatures are needed). Changes signed before the rotation remain authorised; a revoked key authorises nothing. oxy verify shows signatures by revoked keys as valid but revoked.

10. Tags and workspaces

oxy tag create v1.2 -m "First release"   # the current heads; or name changes
oxy tag list
oxy workspace add ../project-hotfix      # a second checkout sharing the repository
oxy workspace list
oxy workspace forget ../project-hotfix

A tag is an immutable state, signed when you have a key, and travels with push and fetch. Workspaces have their own working copy and bookmark (workspace.bookmark) but share objects and history.

Receipts from CI

A runner such as OxygenFlow signs a receipt for every job it runs: what ran, in what environment, on which files, and the result. Receipts are kept with the state they ran on.

oxy receipt list                         # receipts for the bookmark's state
oxy receipt show 3f2a9c                  # one in full, with its signature checked
oxy receipt export 3f2a9c build.receipt  # hand it to someone
oxy receipt verify build.receipt         # check one without keeping it
oxy receipt import build.receipt         # verify and keep it

11. Big repositories: partial, sparse, shallow

oxy clone URL --max-blob 1M          # files above 1 MiB are fetched on first read
oxy clone URL --sparse src --sparse docs   # materialise only these subtrees
oxy clone URL --depth 50             # only the last 50 levels of history
oxy sparse src docs tests            # change the filter later
oxy deepen 100                       # fetch more history below the boundary

Lazy blobs and sparse trees do not affect commits: what you do not have is left exactly as it was. A shallow clone starts from a checkpoint of the state, so status and commit work at full speed from the first minute.

11a. Nested repositories (submodules)

A subrepo is another repository checked out at a path and pinned to a state, Oxygen or Git. The pin is versioned like a file, so two people pinning different states get an ordinary conflict.

oxy submodule add https://host/lib.git libs/lib     # Git or Oxygen URL; records .oxymodules
oxy commit -m "Add lib"                              # the pin is committed with the parent
oxy submodule status                                 # pin vs what is on disk
oxy submodule update --init                          # after a clone: fetch nested repositories
oxy submodule remove libs/lib                        # drop the node; the directory stays

Committing the parent records whatever the nested repository is at, and checking out an older parent state moves the nested repository back, unless it has uncommitted changes, which are never overwritten. An absent nested repository keeps its pin until you run update --init. Git submodules survive oxy git import as Git pins and go back out as gitlinks on export.

12. Git interoperability

oxy git import ../project.git ./project-oxy --branch main   # convert a Git history
oxy git fetch https://github.com/you/project.git            # bring new Git commits in
oxy git push  https://github.com/you/project.git            # publish your changes to Git
oxy git export ../mirror.git                                # a plain Git mirror of the bookmark

Import turns each commit into one change (merge commits become resolutions), keeps authors and hashes as Git-Author/Git-Commit trailers, detects renames, and skips submodules. git fetch/git push keep a bare mirror under .oxy/git/; a push is refused until the remote's tip has been fetched, so it always fast-forwards. Exported commits carry Change-Id and Oxy-Hash trailers.

13. Automate: hooks

Executables in .oxy/hooks/<event> (never cloned) receive one JSON object on stdin. pre-commit, pre-push and other pre-* hooks veto by exiting non-zero, and their stderr becomes the error message. Events: pre-commit, post-commit, pre-push, post-fetch, post-checkout, and semantic-deps, which runs at commit when hooks.semantic-deps = true and answers {"deps": ["<hash>", …]} to make the change depend on more than it textually references.

14. Per-path attributes, ignores, locks

.oxyattributes uses gitattributes syntax:

*.png     binary
*.txt     text eol=lf
*.psd     lockable nocompress

binary/text override content sniffing, eol=lf|crlf normalises line endings in the store and on checkout, nocompress stores chunks raw, and lockable files are checked out read-only until you take the local lock:

oxy lock assets/logo.psd
oxy unlock assets/logo.psd

Locks are advisory and local; server-side locks are planned.

15. Configuration

Two files, both key = "value" lines: ~/.config/oxy/config.toml (oxy config --user KEY VALUE) and .oxy/config (oxy config KEY VALUE). The repository file wins. OXY_HOME relocates the user directory. OXY_TIMINGS=1 prints how long each phase of a snapshot takes (loading the base state, rendering it, reading the working copy, building the change) to standard error, for finding out where a slow command spends its time.

KeyMeaning
author.name, author.email, author.keywho you are; key is set by identity create
core.signchange to sign every commit; otherwise seals at push
core.op-retentiondays for which old operations keep their states alive (90)
core.gc-gracedays an unreachable object survives (14)
core.checkpoint-intervalchanges between automatic checkpoints (256)
filter.max-blob, filter.pathslazy blobs and sparse subtrees (section 11)
hooks.semantic-depsrun the semantic-deps hook at commit
workspace.bookmarkthis workspace's bookmark (main)
remote.<name>.url, .token, .caremotes; token and CA fall back to OXY_TOKEN and OXY_CA

16. Shell prompt

oxy prompt prints main 658990aa*+2 (bookmark, working copy, * dirty, ! conflicted, ahead/behind origin) in a few milliseconds and records nothing. See prompt.md for bash, oh-my-bash, zsh, and fish snippets and the --format placeholders.

17. Housekeeping

oxy gc              # pack loose objects, prune unreachable ones past the grace period
oxy gc --now        # ignore the grace period (only when nothing else is writing)
oxy fsck            # check every object, the op log, references, signatures, checkpoints
oxy doctor          # check identity, config and remotes, with what to do about each problem
oxy checkpoint      # serialise the bookmark's state so large histories load instantly

Both run automatically: packing when loose objects pile up, checkpoints every core.checkpoint-interval changes.

17a. Scripting and editors

Every reporting command takes --json (status, log --path, show, diff, hunks, blame, resolve, op log, bookmark list, remote list, fetch, pull, push, submodule status, bisect status, verify, policy show, root, version). oxy cat PATH -r CHANGE prints a file as it was, oxy discard PATH.. restores files to the base, and oxy hunks numbers the working copy's hunks for commit --hunks and discard --hunks.

18. If something looks wrong

  • "the remote has change … which you do not; pull first": someone pushed since your last fetch. oxy pull, then push.
  • "policy needs N root signature(s)" on fetch: the server's policy was updated in a way your copy cannot verify. Ask a root holder whether the chain of updates was published; a fresh clone trusts the current policy.
  • oxy status exits 2: conflicts remain; see section 6.
  • Many heads on main: independent changes that do not reference one another. Nothing to fix; the state is their union.
  • Undo anything: oxy undo reverts the last operation, including pulls, checkouts, and rewrites.

Shell completions and man pages

oxy completions bash > ~/.local/share/bash-completion/completions/oxy   # bash
oxy completions zsh > "${fpath[1]}/_oxy"                                 # zsh
oxy completions fish > ~/.config/fish/completions/oxy.fish               # fish
oxy man --dir ~/.local/share/man/man1                                    # then: man oxy-commit

The oh-my-bash plugin in contrib/oh-my-bash loads the bash completions itself.