| 1 |
# Oxygen user guide |
| 2 |
|
| 3 |
This guide is organised by task. It assumes the `oxy` binary is on your |
| 4 |
path (`cargo build --release` puts it in `target/release/`). The design |
| 5 |
behind every command is in [spec.md](spec.md); this guide only tells you |
| 6 |
what to type and what to expect. |
| 7 |
|
| 8 |
Oxygen tracks *changes* rather than snapshots: a change is the set of |
| 9 |
edits you made, it commutes with unrelated changes, and conflicts are |
| 10 |
recorded in history instead of blocking you. Two ideas recur below: |
| 11 |
|
| 12 |
- A **bookmark** (default `main`) names a *state*: the set of changes it |
| 13 |
contains. A state can have several **heads** when independent changes |
| 14 |
have not been related to each other; that is normal, not a problem. |
| 15 |
- The **working copy** is itself a change that Oxygen rewrites every time |
| 16 |
it looks at your files. `oxy commit` turns it into a real change. |
| 17 |
|
| 18 |
Output uses colour and glyphs on a terminal; pipe it, pass `--plain`, or set |
| 19 |
`NO_COLOR` for plain ASCII. |
| 20 |
|
| 21 |
## 1. Set yourself up |
| 22 |
|
| 23 |
``` |
| 24 |
oxy identity create --name "Ada Lovelace" --email [email protected] |
| 25 |
oxy config --user core.sign change |
| 26 |
``` |
| 27 |
|
| 28 |
`identity create` generates a composite Ed25519 + ML-DSA-65 key pair, keeps |
| 29 |
the secret in `~/.config/oxy/keys/`, and records you as the default author |
| 30 |
in `~/.config/oxy/config.toml`. With `core.sign = change` every commit is |
| 31 |
signed; without it, changes are sealed in batches when you push (also |
| 32 |
authenticated, just later). Use `--local` to record the identity in one |
| 33 |
repository only. |
| 34 |
|
| 35 |
`oxy identity show` prints the key, its expiry, delegates, and whether the |
| 36 |
secret is available. `oxy identity export` writes the public identity to a |
| 37 |
file to hand to whoever manages a trust policy (section 8). |
| 38 |
|
| 39 |
## 2. Start a repository |
| 40 |
|
| 41 |
``` |
| 42 |
mkdir project && cd project |
| 43 |
oxy init |
| 44 |
oxy status |
| 45 |
``` |
| 46 |
|
| 47 |
`oxy init` creates `.oxy/`. Add `.oxyignore` (gitignore syntax, per |
| 48 |
directory, negations with `!`) for files that should never be tracked; |
| 49 |
`/target` and `*.tmp` are typical entries. As in Git, ignore rules only |
| 50 |
affect untracked files: a file already in history stays tracked until you |
| 51 |
delete it. A project's `.gitignore` files are honoured too, so moving a Git |
| 52 |
project to Oxygen does not commit its build output; `.oxyignore` comes |
| 53 |
first, and a `!` rule there brings back what `.gitignore` leaves out. |
| 54 |
|
| 55 |
To take over an existing Git project, see section 12. |
| 56 |
|
| 57 |
## 3. Everyday work |
| 58 |
|
| 59 |
``` |
| 60 |
oxy status # what changed since the last commit |
| 61 |
oxy diff # the edits, file by file |
| 62 |
oxy commit -m "Add retry loop" |
| 63 |
oxy log # newest first; -n 10, -v for full messages |
| 64 |
oxy log -r 'main.. & touches("src/**")' # a revset (docs/revset_query_language.md) |
| 65 |
oxy show 658990aa # one change by ChangeId prefix, #hash prefix, or @ |
| 66 |
``` |
| 67 |
|
| 68 |
`oxy log` draws the history as a graph, newest first, two lines per change: |
| 69 |
|
| 70 |
``` |
| 71 |
◉ 2f8518c0 Andy McCall just now |
| 72 |
│ (working copy) |
| 73 |
● 1b5888b1 Andy McCall 2 minutes ago main #cc23b999 ✔ signed |
| 74 |
│ Edit b |
| 75 |
│ ● d6f52eca Andy McCall 3 minutes ago main #ec6a7dc4 ✔ signed |
| 76 |
├─╯ Edit a |
| 77 |
○ c0593116 Andy McCall 5 minutes ago #eeac7940 ✔ signed |
| 78 |
Both files |
| 79 |
``` |
| 80 |
|
| 81 |
The bright start of each ID is the shortest prefix that names it. `●` marks |
| 82 |
a head, `◉` the working copy. Bookmarks and tags show beside their heads, |
| 83 |
unless they have more than three. A lane follows the change each one most |
| 84 |
directly builds on, its newest dependency. Two edits that build on the same |
| 85 |
change take two lanes, which join above it, so independent work shows side |
| 86 |
by side. `~` means the change builds on something further down than the |
| 87 |
graph draws, or not shown; `-v` lists every dependency. |
| 88 |
|
| 89 |
Commit part of what changed with `oxy commit PATH.. -m "..."` (only those |
| 90 |
paths; the rest stays as uncommitted work) or `oxy commit -i -m "..."`, which |
| 91 |
offers each hunk in turn like `git add -p`; a hunk that another chosen hunk |
| 92 |
needs, such as the directory a new file lives in, comes along by itself. |
| 93 |
|
| 94 |
`status --json` prints the same for tools, with every conflict's kind, path and |
| 95 |
contributing ChangeIds. On a terminal, `status` lists each path with a mark: `✚ added`, `✎ modified`, `✖ removed`, |
| 96 |
`➜ renamed`, `⚙ mode`. Renames are detected: a removed file and an added |
| 97 |
file of the same kind whose contents are at least half alike are recorded |
| 98 |
as a move of the same file, so identity survives a rename. |
| 99 |
|
| 100 |
Files containing NUL bytes are stored whole ("opaque"); text files are |
| 101 |
stored line by line and merge line by line. `.oxyattributes` overrides the |
| 102 |
guess (section 14). |
| 103 |
|
| 104 |
Every change has two names: a **ChangeId** (`658990aa…`) that survives |
| 105 |
rewrites, and a content hash (`#7469d16c…`) that changes when the change is |
| 106 |
edited. Use whichever you have; prefixes work. |
| 107 |
|
| 108 |
## 3a. Bookmarks: branches without merges |
| 109 |
|
| 110 |
A bookmark names a state; each workspace works on one (`main` to begin |
| 111 |
with). Because changes commute, "merging" is the union of two bookmarks' |
| 112 |
heads, with no merge change and any real conflict recorded as in section 6. |
| 113 |
|
| 114 |
``` |
| 115 |
oxy bookmark create feature # at the working copy's base |
| 116 |
oxy bookmark switch feature # check it out; commits now go here |
| 117 |
oxy commit -m "Feature work" |
| 118 |
oxy bookmark switch main |
| 119 |
oxy merge feature # main now holds both; working copy follows if clean |
| 120 |
oxy bookmark list # ● current, heads, ↑n/↓n against origin |
| 121 |
oxy push -b feature # publish a bookmark other than the current one |
| 122 |
oxy bookmark set feature 658990aa # move it; oxy bookmark delete feature to drop it |
| 123 |
``` |
| 124 |
|
| 125 |
`oxy checkout feature` materialises a bookmark without switching to it, and |
| 126 |
a bookmark name works wherever a change is expected when it has one head. |
| 127 |
Each workspace remembers its own bookmark, so two directories can sit on |
| 128 |
two bookmarks of one repository. |
| 129 |
|
| 130 |
**A state is its heads, not a history.** If you are coming from Git, this |
| 131 |
is the one thing to know. A bookmark holds its heads and what they depend |
| 132 |
on, and a change depends only on the changes it builds on (the files it |
| 133 |
touched), not on everything recorded before it. So on a bookmark with |
| 134 |
several heads, a new bookmark at one change can be a much older-looking |
| 135 |
tree. `oxy bookmark create NAME CHANGE` refuses when that would leave out |
| 136 |
earlier work and lists what it would lack. Then pick one: |
| 137 |
|
| 138 |
``` |
| 139 |
oxy bookmark create logo 745307b4 --upto # everything here up to that change, as a Git branch would be |
| 140 |
oxy bookmark create logo 745307b4 --only # just that change and what it depends on |
| 141 |
oxy bookmark create logo # the whole current state, usually what you want |
| 142 |
``` |
| 143 |
|
| 144 |
`oxy bookmark set NAME CHANGE` asks the same when the move would lose |
| 145 |
changes the bookmark holds that were recorded before CHANGE, and takes |
| 146 |
`--upto` and `--only` too. After merging a proposal's target into its |
| 147 |
bookmark to resolve conflicts, `oxy bookmark set NAME CHANGE --upto` |
| 148 |
keeps everything the bookmark held. |
| 149 |
|
| 150 |
`oxy bookmark switch` says when the bookmark you switch to lacks changes |
| 151 |
that the one you left has. |
| 152 |
|
| 153 |
**A remote's bookmarks.** A clone fetches every change, checks out the |
| 154 |
remote's default bookmark, and names the others `origin/NAME`: use them |
| 155 |
anywhere a bookmark is expected (`oxy log origin/develop`, |
| 156 |
`oxy diff main origin/develop`). `oxy bookmark list --all` shows them. |
| 157 |
`oxy bookmark switch develop` makes a local `develop` from `origin/develop` |
| 158 |
when there is none, and `oxy pull` then keeps every local bookmark up to |
| 159 |
date with the remote's of the same name, not only the current one. Pulling |
| 160 |
is a union, so this never loses work. |
| 161 |
|
| 162 |
``` |
| 163 |
oxy clone https://oxygenhub.io/owner/game --recursive # subrepos too |
| 164 |
oxy bookmark list --all |
| 165 |
oxy bookmark switch develop # from origin/develop |
| 166 |
oxy pull # updates develop, main and the rest |
| 167 |
``` |
| 168 |
|
| 169 |
### A develop / feature flow |
| 170 |
|
| 171 |
Names with `/` work everywhere, pushing included: |
| 172 |
|
| 173 |
``` |
| 174 |
oxy bookmark create feature/42-login # at the working copy's base |
| 175 |
oxy bookmark switch feature/42-login --carry # started already? take the edits along |
| 176 |
oxy commit -F msg.txt # a long message from a file |
| 177 |
oxy push -b feature/42-login |
| 178 |
oxy bookmark switch develop |
| 179 |
oxy merge feature/42-login # the working copy follows when it was clean |
| 180 |
oxy push # develop, with the feature in it |
| 181 |
oxy bookmark delete feature/42-login # oxy push --delete feature/42-login on the remote |
| 182 |
``` |
| 183 |
|
| 184 |
`bookmark switch` stops when the working copy has uncommitted edits, so |
| 185 |
nothing is lost by accident. There are two ways past it: |
| 186 |
- `--carry` takes the edits along. It is refused only when the target |
| 187 |
bookmark has other versions of the files they touch. |
| 188 |
- `--discard` drops the edits. |
| 189 |
|
| 190 |
`oxy describe` names what the working copy holds, for build stamps: |
| 191 |
- a tag, when a tag holds exactly this state; |
| 192 |
- otherwise the nearest tag with the distance from it and the change; |
| 193 |
- or, with no tag behind it, the bookmark and the change; |
| 194 |
- with `-dirty` added when there are uncommitted edits. |
| 195 |
|
| 196 |
## 4. Fix up history |
| 197 |
|
| 198 |
Oxygen keeps its own record of every operation, so all of these are safe to |
| 199 |
try and to undo. |
| 200 |
|
| 201 |
``` |
| 202 |
oxy describe -m "Better message" # reword the head the working copy sits on |
| 203 |
oxy describe 658990aa -m "..." # reword any change; dependents are rewritten |
| 204 |
oxy amend # fold the working copy into the head |
| 205 |
oxy amend --into 658990aa # fold it into an older change |
| 206 |
oxy split 658990aa src/parser.rs # split a head: those paths become a separate first change |
| 207 |
oxy abandon 658990aa # drop a change; its content stays on disk |
| 208 |
oxy squash 658990aa --into 9df9ac29 # fold a change into one it depends on |
| 209 |
oxy rebase 658990aa --onto feature # re-express a change against another base |
| 210 |
oxy rebase 658990aa --onto develop --into-working-copy # conflicted? resolve it here, then commit |
| 211 |
oxy undo # undo the last operation, whatever it was |
| 212 |
oxy op log # every operation, what it changed, and its command |
| 213 |
``` |
| 214 |
|
| 215 |
### The operation log |
| 216 |
|
| 217 |
Every command that changes the repository records an operation: the |
| 218 |
bookmarks, tags and working copy as they were after it. `oxy op log` lists |
| 219 |
them newest first, each with what it changed and the command that did it; |
| 220 |
working-copy snapshots fold into one row. |
| 221 |
|
| 222 |
``` |
| 223 |
@ 13adb0d0b8bf just now |
| 224 |
│ main: 1 → 2 heads; working copy now 91ab4d50 |
| 225 |
│ oxy commit -m "Third" |
| 226 |
○ fa21727d70da just now |
| 227 |
│ created bookmark test (1 head) |
| 228 |
│ oxy bookmark create test |
| 229 |
○ 2d15930afdef … 830168a3ddcd just now |
| 230 |
│ 2 working-copy snapshots |
| 231 |
``` |
| 232 |
|
| 233 |
Any operation can be looked at, gone back to, or undone alone: |
| 234 |
|
| 235 |
``` |
| 236 |
oxy --at-op fa21727d log # the log as it was after that operation; any read-only command works |
| 237 |
oxy op restore fa21727d # bookmarks and working copy as they were then |
| 238 |
oxy op revert fa21727d # undo just that operation, keeping everything after it |
| 239 |
oxy undo # takes back the last command, a restore included |
| 240 |
``` |
| 241 |
|
| 242 |
`op restore` and `op revert` are operations themselves, so `oxy undo` takes |
| 243 |
them back. `op revert` puts back each bookmark the operation moved, unless |
| 244 |
it has moved again since. Tags and the trust policy never go backwards. |
| 245 |
`undo` brings the files back as well when nothing uncommitted would be |
| 246 |
lost; an undone commit's edits stay on disk as uncommitted work. |
| 247 |
|
| 248 |
Commands that move the working copy end by saying where it is: |
| 249 |
|
| 250 |
``` |
| 251 |
* Committed 95245a21 #35bbf5097e0c Third |
| 252 |
Working copy 91ab4d50 |
| 253 |
Bookmark main: 2 heads, newest 95245a21 Third |
| 254 |
``` |
| 255 |
|
| 256 |
Rewriting a change that others depend on rewrites them too, keeping their |
| 257 |
ChangeIds. Because unrelated changes already coexist, `rebase` is rarely |
| 258 |
needed; its job is to change what a change is said to build on, for instance |
| 259 |
to drop a dependency on something you are about to abandon. Where a dependent referred to a line that no longer exists, the |
| 260 |
reference is re-anchored to the nearest surviving line and `oxy abandon` |
| 261 |
prints a note about it. |
| 262 |
|
| 263 |
## 4a. Who wrote this, and what broke it |
| 264 |
|
| 265 |
``` |
| 266 |
oxy blame src/parser.rs # every line: the change that introduced it, author, age |
| 267 |
oxy blame src/parser.rs --json |
| 268 |
``` |
| 269 |
|
| 270 |
Blame is exact: each line is an atom that remembers the change that |
| 271 |
introduced it, so renames cost nothing and there is no heuristic. |
| 272 |
|
| 273 |
``` |
| 274 |
oxy bisect start |
| 275 |
oxy bisect good v1.2 # a change, bookmark or tag known to be fine |
| 276 |
oxy bisect bad main # and one known to be broken |
| 277 |
oxy bisect run make test # exit 0 good, 125 skip, anything else bad |
| 278 |
oxy bisect reset # back to where you were |
| 279 |
``` |
| 280 |
|
| 281 |
Without `run`, each step checks out a state to test and you answer with |
| 282 |
`oxy bisect good`, `bad`, or `skip`. The candidates are the changes the bad |
| 283 |
state has that the good one lacks, ordered so every tested state is |
| 284 |
well-formed; the answer is the first change whose inclusion breaks things. |
| 285 |
|
| 286 |
## 5. Move around |
| 287 |
|
| 288 |
``` |
| 289 |
oxy checkout # the bookmark's heads |
| 290 |
oxy checkout 658990aa # the state of one change and its dependencies |
| 291 |
oxy checkout v1.2 # a tag |
| 292 |
oxy checkout --discard ... # throw away uncommitted work first |
| 293 |
``` |
| 294 |
|
| 295 |
Checking out a single older change gives you the files that change and its |
| 296 |
dependencies touch, not a full snapshot from that date: in Oxygen a change |
| 297 |
depends only on what it actually builds on. |
| 298 |
|
| 299 |
## 6. Conflicts |
| 300 |
|
| 301 |
A conflict never stops a commit, fetch, or pull. It is recorded in the |
| 302 |
state and shown by `oxy status`, which exits with status 2 while any |
| 303 |
conflict remains. Conflicted text files are written with markers: |
| 304 |
|
| 305 |
``` |
| 306 |
<<<<<<< oxy conflict 1 of 2, side 1: change k7m2p9 "Add retry loop" |
| 307 |
... lines ... |
| 308 |
======= oxy conflict 1 of 2, side 2: change q3x8w1 "Refactor client" |
| 309 |
... lines ... |
| 310 |
>>>>>>> oxy conflict 1 of 2 end |
| 311 |
``` |
| 312 |
|
| 313 |
Edit the file to what it should be (keep one side, both, or neither) and |
| 314 |
remove the markers. Tree conflicts, such as two files wanting the same |
| 315 |
name, appear as `<name>.oxy-conflict-<n>` candidates: keep the one you |
| 316 |
want under the right name and delete the rest. |
| 317 |
|
| 318 |
``` |
| 319 |
oxy resolve # which conflicts your edits resolve, which still have markers |
| 320 |
oxy commit -m "Resolve retry vs refactor" |
| 321 |
``` |
| 322 |
|
| 323 |
The commit is recorded as a *resolution*, a change that depends on every |
| 324 |
side of the conflict; anyone who fetches it gets the conflict resolved. |
| 325 |
|
| 326 |
## 7. Work with others |
| 327 |
|
| 328 |
Run a server (any machine with the repositories under one directory): |
| 329 |
|
| 330 |
``` |
| 331 |
oxy serve --root /srv/oxy --bind 0.0.0.0:8890 --token SECRET \ |
| 332 |
--tls-cert cert.pem --tls-key key.pem |
| 333 |
``` |
| 334 |
|
| 335 |
Without `--tls-*` the server speaks plain HTTP; keep that on a loopback |
| 336 |
address or behind a TLS terminator. `--token` makes pushes require a bearer |
| 337 |
token; reads stay open. |
| 338 |
|
| 339 |
On your machine: |
| 340 |
|
| 341 |
``` |
| 342 |
echo SECRET | oxy auth login https://host:8890 --with-token --ca my-ca.pem |
| 343 |
oxy clone https://host:8890/project |
| 344 |
cd project |
| 345 |
oxy push |
| 346 |
oxy pull |
| 347 |
``` |
| 348 |
|
| 349 |
`--ca` is only for a server with a self-signed certificate. On a hub such as |
| 350 |
OxygenHub, `oxy auth login https://oxygenhub.io` signs in through the |
| 351 |
browser instead: it shows a code to approve on the hub, and the hub issues a |
| 352 |
token that lasts 90 days past its last use. |
| 353 |
|
| 354 |
### Signing in |
| 355 |
|
| 356 |
`oxy auth login HUB` keeps one token per hub (scheme, host and port) in |
| 357 |
`~/.config/oxy/credentials.toml`, readable by you only, and every repository |
| 358 |
on that hub uses it: clone, fetch, push and subrepos alike. Nothing secret is |
| 359 |
written into a checkout. `oxy auth status` lists the hubs you are signed in |
| 360 |
to and checks each token with its hub; `oxy auth logout HUB` forgets the token |
| 361 |
and revokes it on the hub. |
| 362 |
|
| 363 |
To keep tokens in the system keyring instead, name a Git credential helper: |
| 364 |
`oxy config --user auth.helper libsecret` (Linux), `osxkeychain` (macOS) or |
| 365 |
`manager` (Windows). The file then keeps only the account and CA. |
| 366 |
|
| 367 |
A token is found in this order: the remote's own `remote.<name>.token`, then |
| 368 |
`$OXY_TOKEN` (for CI), then the store, then a remote on the same hub in an |
| 369 |
enclosing checkout (so a subrepo uses its parent's). oxy sends a token over |
| 370 |
`https` only, or over plain `http` to this machine; it is never sent to |
| 371 |
another host after a redirect, and a credentials file that others can read |
| 372 |
is ignored. |
| 373 |
|
| 374 |
- `oxy fetch` updates `origin/main` without touching your bookmark. |
| 375 |
- `oxy pull` fetches, then makes your bookmark, and every other local |
| 376 |
bookmark the remote also has, the union of both sides. |
| 377 |
There is no merge commit; unrelated changes simply coexist, and a real |
| 378 |
conflict is recorded as in section 6. The working copy follows when it was |
| 379 |
clean. |
| 380 |
- `oxy push` sends your changes and moves the remote bookmark. If the remote |
| 381 |
moved since you last fetched, the push is refused: pull and push again. |
| 382 |
`--force` overrides the compare-and-swap. |
| 383 |
- `oxy remote add|remove|list` manages remotes; the clone source is `origin`. |
| 384 |
|
| 385 |
A repository can also be cloned from a local path, which is handy for tests |
| 386 |
and for a bare "hub" on a shared disk. |
| 387 |
|
| 388 |
## 8. Decide who may publish: the trust policy |
| 389 |
|
| 390 |
Without a policy a repository is *unverified*: everything is accepted and |
| 391 |
every remote command says so. A policy names root keys and the keys allowed |
| 392 |
to update each bookmark; servers and clients enforce it. |
| 393 |
|
| 394 |
``` |
| 395 |
oxy policy init # your key becomes the only root; push publishes it |
| 396 |
oxy policy show |
| 397 |
oxy policy grant alice.identity # alice.identity came from her `oxy identity export` |
| 398 |
oxy policy grant ci.identity --pattern 'release/*' |
| 399 |
oxy policy revoke 9458b656 # by KeyId prefix |
| 400 |
oxy verify # signature status of every change in the log |
| 401 |
``` |
| 402 |
|
| 403 |
Several people can hold root keys: |
| 404 |
|
| 405 |
``` |
| 406 |
oxy policy root add bob.identity |
| 407 |
oxy policy root threshold 2 # future updates need two root signatures |
| 408 |
``` |
| 409 |
|
| 410 |
Once the threshold is above one, an update you make alone becomes a |
| 411 |
*proposal file* (`policy-<serial>.proposal`, or `--out`). Send it to another |
| 412 |
root holder, who runs `oxy policy sign policy-7.proposal`; whoever completes |
| 413 |
the threshold applies it, and the next push publishes it. Everyone else |
| 414 |
picks it up on fetch, verifying each step from the policy they already |
| 415 |
trust. |
| 416 |
|
| 417 |
## 9. Keys over time |
| 418 |
|
| 419 |
``` |
| 420 |
oxy identity rotate # fresh key pair; old signatures stay valid |
| 421 |
oxy identity renew --expires 365 # same keys, new expiry in days (0 = never) |
| 422 |
oxy identity delegate add agent.identity --expires 8h # sign changes for me today |
| 423 |
oxy identity delegate add ci.identity --scope changes,refs # sign and push for me |
| 424 |
oxy identity delegate remove 3f2a… |
| 425 |
oxy identity revoke 3f2a… --reason "agent host compromised" # a key you delegated to |
| 426 |
``` |
| 427 |
|
| 428 |
Setting an agent up takes a home of its own, so it never holds your |
| 429 |
secret key. In the agent's shell, inside any checkout: |
| 430 |
|
| 431 |
``` |
| 432 |
export OXY_HOME=~/.oxy-agents/claude |
| 433 |
oxy identity create --name "Claude Code on my laptop" --email [email protected] |
| 434 |
# prints the agent's KeyId; its identity file is keys/<KeyId>.identity in that home |
| 435 |
oxy config --user author.name "Your Name" # the agent authors as you… |
| 436 |
oxy config --user author.email "[email protected]" |
| 437 |
oxy config --user author.key <YOUR KeyId> |
| 438 |
oxy config --user author.signer <THE AGENT's KeyId> # …and signs as itself |
| 439 |
oxy config --user core.sign change |
| 440 |
``` |
| 441 |
|
| 442 |
Then, in your own shell, in the repository: `oxy identity delegate add |
| 443 |
~/.oxy-agents/claude/keys/<KeyId>.identity --scope changes --expires 8h` |
| 444 |
(several files at once are one rotation). This rotates your identity: |
| 445 |
the new key is written to your user config, and to this repository's |
| 446 |
file too if it pinned the old one, so other checkouts follow; put the |
| 447 |
new KeyId in each agent's `author.key`, and re-link it on your hub. |
| 448 |
`oxy identity show` warns if a repository still pins an older key |
| 449 |
(`oxy config --unset author.key` there). Every commit the agent makes |
| 450 |
carries you as author and its signature: `oxy log -r 'delegated()'` |
| 451 |
lists them, `oxy verify` names the agent, and its own `oxy push` is |
| 452 |
refused under any policy, so you land its work. |
| 453 |
|
| 454 |
``` |
| 455 |
oxy identity revoke # retire your own key |
| 456 |
oxy identity revoke 3f2a… --reason "laptop stolen" # any key, if you hold a root |
| 457 |
``` |
| 458 |
|
| 459 |
Rotation, renewal, and delegation all issue a successor identity that names |
| 460 |
the previous one. If a policy names your key, it is updated to the successor |
| 461 |
in the same step (as a proposal when more root signatures are needed). |
| 462 |
Changes signed before the rotation remain authorised; a revoked key |
| 463 |
authorises nothing. `oxy verify` shows signatures by revoked keys as valid |
| 464 |
but revoked. |
| 465 |
|
| 466 |
## 10. Tags and workspaces |
| 467 |
|
| 468 |
``` |
| 469 |
oxy tag create v1.2 -m "First release" # the current heads; or name changes |
| 470 |
oxy tag list |
| 471 |
oxy workspace add ../project-hotfix # a second checkout sharing the repository |
| 472 |
oxy workspace list |
| 473 |
oxy workspace forget ../project-hotfix |
| 474 |
``` |
| 475 |
|
| 476 |
A tag is an immutable state, signed when you have a key, and travels with |
| 477 |
push and fetch. Workspaces have their own working copy and bookmark |
| 478 |
(`workspace.bookmark`) but share objects and history. |
| 479 |
|
| 480 |
### Receipts from CI |
| 481 |
|
| 482 |
A runner such as OxygenFlow signs a receipt for every job it runs: what ran, |
| 483 |
in what environment, on which files, and the result. Receipts are kept with |
| 484 |
the state they ran on. |
| 485 |
|
| 486 |
``` |
| 487 |
oxy receipt list # receipts for the bookmark's state |
| 488 |
oxy receipt show 3f2a9c # one in full, with its signature checked |
| 489 |
oxy receipt export 3f2a9c build.receipt # hand it to someone |
| 490 |
oxy receipt verify build.receipt # check one without keeping it |
| 491 |
oxy receipt import build.receipt # verify and keep it |
| 492 |
``` |
| 493 |
|
| 494 |
## 11. Big repositories: partial, sparse, shallow |
| 495 |
|
| 496 |
``` |
| 497 |
oxy clone URL --max-blob 1M # files above 1 MiB are fetched on first read |
| 498 |
oxy clone URL --sparse src --sparse docs # materialise only these subtrees |
| 499 |
oxy clone URL --depth 50 # only the last 50 levels of history |
| 500 |
oxy sparse src docs tests # change the filter later |
| 501 |
oxy deepen 100 # fetch more history below the boundary |
| 502 |
``` |
| 503 |
|
| 504 |
Lazy blobs and sparse trees do not affect commits: what you do not have is |
| 505 |
left exactly as it was. A shallow clone starts from a *checkpoint* of the |
| 506 |
state, so status and commit work at full speed from the first minute. |
| 507 |
|
| 508 |
## 11a. Nested repositories (submodules) |
| 509 |
|
| 510 |
A subrepo is another repository checked out at a path and pinned to a state, |
| 511 |
Oxygen or Git. The pin is versioned like a file, so two people pinning |
| 512 |
different states get an ordinary conflict. |
| 513 |
|
| 514 |
``` |
| 515 |
oxy submodule add https://host/lib.git libs/lib # Git or Oxygen URL; records .oxymodules |
| 516 |
oxy commit -m "Add lib" # the pin is committed with the parent |
| 517 |
oxy submodule status # pin vs what is on disk |
| 518 |
oxy submodule update --init # after a clone: fetch nested repositories |
| 519 |
oxy submodule remove libs/lib # drop the node; the directory stays |
| 520 |
``` |
| 521 |
|
| 522 |
Committing the parent records whatever the nested repository is at, and |
| 523 |
checking out an older parent state moves the nested repository back, unless |
| 524 |
it has uncommitted changes, which are never overwritten. An absent nested |
| 525 |
repository keeps its pin until you run `update --init`. Git submodules |
| 526 |
survive `oxy git import` as Git pins and go back out as gitlinks on export. |
| 527 |
|
| 528 |
## 12. Git interoperability |
| 529 |
|
| 530 |
``` |
| 531 |
oxy git import ../project.git ./project-oxy --branch main # convert a Git history |
| 532 |
oxy git fetch https://github.com/you/project.git # bring new Git commits in |
| 533 |
oxy git push https://github.com/you/project.git # publish your changes to Git |
| 534 |
oxy git export ../mirror.git # a plain Git mirror of the bookmark |
| 535 |
``` |
| 536 |
|
| 537 |
Import turns each commit into one change (merge commits become |
| 538 |
resolutions), keeps authors and hashes as `Git-Author`/`Git-Commit` |
| 539 |
trailers, detects renames, and skips submodules. `git fetch`/`git push` keep |
| 540 |
a bare mirror under `.oxy/git/`; a push is refused until the remote's tip |
| 541 |
has been fetched, so it always fast-forwards. Exported commits carry |
| 542 |
`Change-Id` and `Oxy-Hash` trailers. |
| 543 |
|
| 544 |
## 13. Automate: hooks |
| 545 |
|
| 546 |
Executables in `.oxy/hooks/<event>` (never cloned) receive one JSON object |
| 547 |
on stdin. `pre-commit`, `pre-push` and other `pre-*` hooks veto by exiting |
| 548 |
non-zero, and their stderr becomes the error message. Events: |
| 549 |
`pre-commit`, `post-commit`, `pre-push`, `post-fetch`, `post-checkout`, and |
| 550 |
`semantic-deps`, which runs at commit when `hooks.semantic-deps = true` and |
| 551 |
answers `{"deps": ["<hash>", …]}` to make the change depend on more than it |
| 552 |
textually references. |
| 553 |
|
| 554 |
## 14. Per-path attributes, ignores, locks |
| 555 |
|
| 556 |
`.oxyattributes` uses gitattributes syntax: |
| 557 |
|
| 558 |
``` |
| 559 |
*.png binary |
| 560 |
*.txt text eol=lf |
| 561 |
*.psd lockable nocompress |
| 562 |
``` |
| 563 |
|
| 564 |
`binary`/`text` override content sniffing, `eol=lf|crlf` normalises line |
| 565 |
endings in the store and on checkout, `nocompress` stores chunks raw, and |
| 566 |
`lockable` files are checked out read-only until you take the local lock: |
| 567 |
|
| 568 |
``` |
| 569 |
oxy lock assets/logo.psd |
| 570 |
oxy unlock assets/logo.psd |
| 571 |
``` |
| 572 |
|
| 573 |
Locks are advisory and local; server-side locks are planned. |
| 574 |
|
| 575 |
## 15. Configuration |
| 576 |
|
| 577 |
Two files, both `key = "value"` lines: `~/.config/oxy/config.toml` |
| 578 |
(`oxy config --user KEY VALUE`) and `.oxy/config` (`oxy config KEY VALUE`). |
| 579 |
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. |
| 580 |
|
| 581 |
|
| 582 |
|
| 583 |
| `author.name`, `author.email`, `author.key` | who you are; `key` is set by `identity create` | |
| 584 |
| `author.signer` | the key that signs instead of `author.key`: an agent's own key, which your identity lists as a delegate (also `$OXY_SIGNER`) | |
| 585 |
| `core.sign` | `change` to sign every commit; otherwise seals at push | |
| 586 |
| `core.op-retention` | days for which old operations keep their states alive (90) | |
| 587 |
| `core.gc-grace` | days an unreachable object survives (14) | |
| 588 |
| `core.checkpoint-interval` | changes between automatic checkpoints (256) | |
| 589 |
| `filter.max-blob`, `filter.paths` | lazy blobs and sparse subtrees (section 11) | |
| 590 |
| `hooks.semantic-deps` | run the semantic-deps hook at commit | |
| 591 |
| `workspace.bookmark` | this workspace's bookmark (`main`) | |
| 592 |
| `remote.<name>.url`, `.token`, `.ca` | remotes; token and CA fall back to `OXY_TOKEN` and `OXY_CA`, then `oxy auth login`'s store | |
| 593 |
| `auth.helper` (user file) | a Git credential helper to keep `oxy auth` tokens in, such as `libsecret` | |
| 594 |
|
| 595 |
## 16. Shell prompt |
| 596 |
|
| 597 |
`oxy prompt` prints `main 658990aa*+2` (bookmark, working copy, `*` dirty, |
| 598 |
`!` conflicted, ahead/behind origin) in a few milliseconds and records |
| 599 |
nothing. See [prompt.md](prompt.md) for bash, oh-my-bash, zsh, and fish |
| 600 |
snippets and the `--format` placeholders. |
| 601 |
|
| 602 |
## 17. Housekeeping |
| 603 |
|
| 604 |
``` |
| 605 |
oxy gc # prune unreachable objects past the grace period, repack into one pack |
| 606 |
oxy gc --now # ignore the grace period (only when nothing else is writing) |
| 607 |
oxy fsck # check every object, the op log, references, signatures, checkpoints |
| 608 |
oxy doctor # check identity, config and remotes, with what to do about each problem |
| 609 |
oxy checkpoint # serialise the bookmark's state so large histories load instantly |
| 610 |
``` |
| 611 |
|
| 612 |
Both run automatically: packing when loose objects pile up, checkpoints |
| 613 |
every `core.checkpoint-interval` changes. |
| 614 |
|
| 615 |
## 17a. Scripting and editors |
| 616 |
|
| 617 |
Every reporting command takes `--json` (`status`, `log --path`, `show`, |
| 618 |
`diff`, `hunks`, `blame`, `resolve`, `op log`, `bookmark list`, `remote list`, |
| 619 |
`fetch`, `pull`, `push`, `submodule status`, `bisect status`, `verify`, |
| 620 |
`policy show`, `root`, `version`). `oxy cat PATH -r CHANGE` prints a file as |
| 621 |
it was, `oxy discard PATH..` restores files to the base, and `oxy hunks` |
| 622 |
numbers the working copy's hunks for `commit --hunks` and `discard --hunks`. |
| 623 |
|
| 624 |
## 18. If something looks wrong |
| 625 |
|
| 626 |
- **"the remote has change … which you do not; pull first"**: someone |
| 627 |
pushed since your last fetch. `oxy pull`, then push. |
| 628 |
- **"policy needs N root signature(s)"** on fetch: the server's policy was |
| 629 |
updated in a way your copy cannot verify. Ask a root holder whether the |
| 630 |
chain of updates was published; a fresh clone trusts the current policy. |
| 631 |
- **`oxy status` exits 2**: conflicts remain; see section 6. |
| 632 |
- **Many heads on `main`**: independent changes that do not reference one |
| 633 |
another. Nothing to fix; the state is their union. |
| 634 |
- **Undo anything**: `oxy undo` reverts the last operation, including |
| 635 |
pulls, checkouts, and rewrites. |
| 636 |
|
| 637 |
## Shell completions and man pages |
| 638 |
|
| 639 |
```sh |
| 640 |
oxy completions bash > ~/.local/share/bash-completion/completions/oxy # bash |
| 641 |
oxy completions zsh > "${fpath[1]}/_oxy" # zsh |
| 642 |
oxy completions fish > ~/.config/fish/completions/oxy.fish # fish |
| 643 |
oxy man --dir ~/.local/share/man/man1 # then: man oxy-commit |
| 644 |
``` |
| 645 |
|
| 646 |
The oh-my-bash plugin in `contrib/oh-my-bash` loads the bash completions itself. |