OXYGENHUB

Compare identical-sides … main

⇄

29 changes in main not in identical-sides. 57 files differ between the two states.

Release 0.11.0 mainv0.11.0 head
Andy McCall via agent:claude@andymccall agent ·
Name moved subrepo pins in status and commit; changelog on the WASM tier (closes #162) mainv0.11.0 head
Andy McCall via agent:claude@andymccall agent ·
A stale author.key can no longer fork the identity (closes #163)
Andy McCall via agent:claude@andymccall agent ·
oxy 0.10.0 v0.10.0
Andy McCall via agent:claude@andymccall agent ·
HaikuPorts recipes: x86_64 only and no minimum rustc, as the haikuports review asked (refs #157) mainv0.10.0v0.11.0 head
Andy McCall via agent:claude@andymccall agent ·
HaikuPorts recipes: copyright 2026, as the LICENSE says (refs #157) mainv0.10.0v0.11.0 head
Andy McCall via agent:claude@andymccall agent ·
oxy 0.9.0 v0.9.0
Andy McCall via agent:claude@andymccall agent ·
The source tarball leaves out the job disk's lost+found (refs #157) mainv0.10.0v0.11.0v0.9.0 head
Andy McCall via agent:claude@andymccall agent ·
oxy 0.8.0 v0.8.0
Andy McCall via agent:claude@andymccall agent ·
CHANGELOG: one #148 entry (the merge kept both sides' copy) mainsubrepo-pin-statev0.10.0v0.11.0v0.8.0v0.9.0 head
Andy McCall via agent:claude@andymccall agent ·
Merge main into subrepo-pin-state: keep both changelog entries resolution
Andy McCall via agent:claude@andymccall agent ·
Merge main into subrepo-pin-state: keep both changelog entries resolution
Andy McCall via agent:claude@andymccall agent ·
hide whitespace changes
added .flow/checks.wasm +0 −0

No line changes.

modified .flow/pipeline.kdl +20 −0
…
2 unchanged lines
3 3
// Earth; the jobs appear as checks on the change.
4 4
schema-version 1
5 5
6 +
// Cheap checks on the WASM tier (oxygenflow#96): OxygenFlow's checks module,
7 +
// copied from oxygenflow/.flow/checks.wasm (built there from
8 +
// crates/flow-checks; copy it again when it changes). No image and no KVM,
9 +
// so any runner with the WASM tier takes it: a phone, plex, windows-1.
10 +
sandbox "wasm" {
11 +
    runtime "wasm"
12 +
    memory-limit "64MiB"
13 +
}
14 +
15 +
// The changelog's shape, and a section for the version Cargo.toml says, so a
16 +
// release never finds its notes missing.
17 +
job "changelog" {
18 +
    sandbox "wasm"
19 +
    scope "CHANGELOG.md" "Cargo.toml" ".flow/checks.wasm"
20 +
    exec ".flow/checks.wasm" "changelog" "CHANGELOG.md" "Cargo.toml"
21 +
}
22 +
6 23
// The OxygenFlow build image: Rust with rustfmt and clippy, Node, and every
7 24
// crate of Cargo.lock vendored (oxygenflow/images/rust). Rebuild and re-pin
8 25
// it when Cargo.lock changes.
…
171 unchanged lines
180 197
        artifact "oxy_riscv64.deb" {
181 198
            path "/src/target/dist-flat/oxy_riscv64.deb"
182 199
        }
200 +
        artifact "oxy-src.tar.gz" {
201 +
            path "/src/target/dist-flat/oxy-src.tar.gz"
202 +
        }
183 203
        artifact "SHA256SUMS" {
184 204
            path "/src/target/dist-flat/SHA256SUMS"
185 205
        }
…
30 unchanged lines
modified CHANGELOG.md +32 −1
1 1
# Changelog
2 2
3 -
## Unreleased
3 +
## 0.11.0 (2026-11-04)
4 +
5 +
- **Changed: the changelog is checked on the WASM tier.** A `changelog` job in the pipeline runs OxygenFlow's checks module on any runner with the WASM tier, failing when the changelog's sections are out of shape or Cargo.toml's version has none.
6 +
- **Changed: moved subrepo pins are named (#162).** A pin moved by work inside a subrepo is a pin bump the parent's next commit records, and it used to go in unsaid. `oxy status` now lists it under *Subrepo pins* (old → new), and a subrepo's own uncommitted edits, which the parent does not record; a bare `oxy commit` that records a moved pin says so and how to leave it out (`oxy commit PATHS`).
7 +
- **Added: `oxy submodule remove --delete` (#164).** It deletes the subrepo's directory too, read-only object files and all, and refuses while the subrepo has uncommitted edits (`--force` to delete them anyway); without it, removal says the directory is left and how to delete it. A directory moved by hand used to fail halfway, after the copy, on the read-only files.
8 +
- **Changed: an agent's commit says "awaiting seal", not "unsigned" (#165).** In a checkout that signs keyless under an agent grant, a change of its own author that the next push will seal shows `◌ awaiting seal` in `oxy log` and `oxy show`; "unsigned" is kept for changes nothing will sign.
9 +
- **Fixed: a stale `author.key` can no longer fork your identity (#163).** A checkout that pinned `author.key` to a key a rotation made elsewhere had already succeeded built a second successor of it on `oxy identity delegate add`, splitting the identity in two. `rotate`, `renew` and `delegate add`/`remove` now refuse to start from a key that already has a successor in your home, naming the newest key and the `oxy config` that points at it; their report says which key they started from and which config named it; and `oxy identity show` warns when the configured key is not the newest, and names the key where an identity forks.
10 +
11 +
## 0.10.0 (2026-11-01)
12 +
13 +
- **Fixed: fetch, pull and clone are far faster with many bookmarks (#158).** They fetched each bookmark in its own exchange with the hub, and changes that bookmarks share were sent again for each one; a pull with nothing new still asked for a bundle per bookmark. Now every bookmark's heads are fetched in one exchange, and none when nothing is new. On a 47 MB repository with 160 bookmarks: a pull with nothing new 169 s → 3.5 s, a clone 324 s → 26 s. `OXY_TIMINGS` now also shows the refs, transparency log and per-bookmark checks of a fetch.
14 +
- **Added: `oxy git colocate`, a `.git` beside `.oxy` (oxygenhub#355).** Your editor's Git features (blame, history, gutter marks) and other Git tools now work on an Oxygen checkout: oxy keeps a `.git` there with the current bookmark exported after every command, Git's index at its tree so `git status` shows only your uncommitted work, and `.oxy/` excluded. One way: commit with oxy. `--off` stops it.
15 +
- **Changed: `.git` is never content.** oxy no longer picks up a `.git` directory at a checkout's top, and checkout refuses a path with a `.git` component at any depth (as Git does), so a change cannot plant Git hooks.
16 +
- **Added: a Git export from the change graph (oxygenhub#355).** oxy-core's `git_export_graph` writes every bookmark and tag to a bare Git repository so that the same changes give the same commits wherever it runs: each change a commit whose parents are its dependencies, each bookmark a branch (a merge of its heads when it has several), tags as tags, incrementally. It is what OxygenHub serves to `git clone`. (`oxy git export` still follows the checkout's own history.)
17 +
- **Added: `oxy mcp`, Oxygen for AI agents (#159).** An MCP server over stdio for a repository: an agent calls typed tools (status, diff, log, show, cat, blame, bookmarks, tags, conflicts, op log, identity, commit, amend, describe, fetch, pull, push, tlog verify) and gets each command's JSON back instead of reading a shell. It signs as the identity its environment configures, so started with an agent home every change is sealed via the agent with nothing to forget per command, and says who in its `initialize` reply. Commits name their paths (or `all: true`); undo and forced pushes are off unless started with `--allow-destructive`. The protocol itself is oxy-core's `mcp` module, for `oh mcp` to reuse. See docs/mcp.md.
18 +
- **Icons for HaikuDepot (#157).** `packaging/haikuports/icons/` has oxy's and oh's icons as HVIF (Haiku's vector format) with 16, 32 and 64 px PNGs, written by `make-icons.py`: the Oxygen mark on a pale tile for oxy, on OxygenHub's dark tile for oh, the same family as the VS Code extensions.
4 19
20 +
## 0.9.0 (2026-10-29)
21 +
22 +
- **Releases carry their source (#157).** Each release now has `oxy-VERSION-src.tar.gz`, the source as released, reproducible (sorted, fixed times and owners, gzip without a timestamp), listed in SHA256SUMS, for packagers to fetch at a fixed checksum. `packaging/haikuports/make-recipe.py oxy|oh TARBALL VERSION` writes the haikuports recipe from it (oh's from OxygenHub's source tarball); the 0.8.0 recipe, which used the on-demand archive, is gone.
23 +
- **Added: a haikuports recipe for oxy (#156).** `packaging/haikuports/` holds the recipe that builds oxy for HaikuDepot from the release source and every crate in `Cargo.lock`, and the script that writes it for a new release; the 0.8.0 recipe builds with haikuporter on Haiku R1/beta6.
24 +
- **Fixed: an expired checkout token no longer hides your sign-in (#155).** A checkout's own token for a remote (`remote.NAME.token`) is used first, so when it expired every fetch, pull and push answered 401 although `oxy auth login` had a good sign-in for the same hub. A checkout token the hub no longer knows (401) now falls back to that sign-in, with a warning naming `oxy config --unset remote.NAME.token` to drop the stale one; never to another checkout's token, and not when the token is refused for permission (403, 404), so a narrowly scoped token is never swapped for a wider sign-in. When both are refused, the error says to sign in again and unset it.
25 +
- **Added: the `artifact` object type (Cylinder C1, #108).** An artifact is now an object of its own in the store (oxy-core's `artifact` module), named by its object hash as every object is, and `oxy fsck` checks it; its tree stays a blob. Stores written before read as they did: the type is a new one at the end of the list.
26 +
- **Added: artifacts (Cylinder, step C1, #108).** `oxy artifact pack DIR --kind archive|package|container|runner-image|report [--arch --os --entrypoint --env K=V --label K=V]` packs a directory into an artifact: each file an ordinary blob of the repository's store, so files and chunks it holds already cost nothing, under a tree and an artifact encoding in canonical CBOR, named by the BLAKE3 of that encoding (docs/cylinder.md §4.1). `oxy artifact list | show NAME | unpack NAME DEST | verify NAME` (any unique prefix of six characters or more; `--json` on list, show and pack), and `oxy gc` keeps every artifact's blobs. The new `cylinder` crate holds the model.
27 +
- **Added: oxy on Haiku (#154).** oxy builds and runs on Haiku R1/beta6 (x86_64) with HaikuPorts' Rust; `oxy-VERSION-haiku-x86_64.tar.gz` is on the releases page from 0.8.0, with its own `SHA256SUMS-haiku`. The README says how to install it and how to build it (`ZSTD_SYS_USE_PKG_CONFIG=1`).
28 +
29 +
- **Fixed: a subrepo put back after its removal is pinned in the change that registers it (#152).** `oxy submodule remove` leaves a marker so the next snapshot drops the node while the directory stays; the snapshot cleared it only after reading it, so when nothing looked at the working copy between the removal's commit and the subrepo's return, the change that registered it again in `.oxymodules` left it out and a second commit was needed. A marker is now dropped before it is read once the removal is recorded or the path is registered again.
30 +
- **Fixed: `oxy identity create` needs no repository (#144).** Outside one it makes a user identity: the secret and identity go to the oxy home's keys and the user file names it as the author, so a new user or machine sets up an identity first; a repository made or cloned afterwards signs with it. Only `--local` needs a repository, and inside one it is kept there too, as before.
31 +
32 +
## 0.8.0 (2026-10-27)
33 +
5 34
- **A server can add a note to a reference a push moved (#148).** `oxy-server`'s `Backend` has a `push_notes` step: each note is added to that accepted reference's result, which `oxy push` shows in place of "updated". OxygenHub will use it to say when a push moves the bookmark of a proposal that has already landed (oxygenhub#329).
6 35
7 36
- **Identical sides settle by themselves (#146).** Two bookmarks that each resolved the same conflict to the same text conflicted again when merged, both sides the identical lines. A conflict whose sides are byte-identical now settles as `merge=union` adjacent insertions do (#145), in any file and keeping one side: a checkout writes it once ("kept one of its identical sides") and the next commit records the resolution, and a host landing a proposal settles it too.
8 37
38 +
- **A subrepo at its pinned state is unmodified on any bookmark (#147).** A subrepo whose heads were exactly its pin's but which sat on another bookmark than the pin named counted as modified, so `oxy bookmark switch` refused ("the working copy has changes"). Only the state counts now: the same repository at the same heads, or the same Git commit; the recorded pin stands.
39 +
9 40
- **A shallow clone fetches its checkpoint in parts (#137).** The checkpoint of a big state came as one answer, the whole state's graph blobs in one body (about 2 GB for the VS Code history), built and held whole on both sides. A server now answers `checkpoint-index` (the checkpoint and its history, no graphs), and the client fetches the graph blobs it lacks with `objects`, 2,048 at a time; `objects` reads them in pack order. An older server answers 404 and gets the one-piece request as before.
10 41
11 42
- **A keyless agent follows its owner's newest identity (#82).** Every delegation rotates its owner's identity, so the `author.key` an agent's home was given goes stale, and its changes named an identity two rotations old (verifying only because a grant from before a rotation still counts). With `author.for = ACCOUNT` in the home, the hub sends the account's newest linked identity with each certificate, with its chain back to the grant's key; oxy keeps it in the home (`keys/author.for`), stores the chain beside the change so the grant checks, and authors as it from then on. A repository's own `author.key` pin still wins.
…
283 unchanged lines
modified Cargo.lock +15 −4
…
571 unchanged lines
572 572
]
573 573
574 574
[[package]]
575 +
name = "cylinder"
576 +
version = "0.11.0"
577 +
dependencies = [
578 +
 "blake3",
579 +
 "oxy-core",
580 +
 "tempfile",
581 +
 "thiserror 1.0.69",
582 +
]
583 +
584 +
[[package]]
575 585
name = "data-encoding"
576 586
version = "2.11.1"
577 587
source = "registry+https://github.com/rust-lang/crates.io-index"
…
911 unchanged lines
1489 1499
1490 1500
[[package]]
1491 1501
name = "oxy"
1492 -
version = "0.7.0"
1502 +
version = "0.11.0"
1493 1503
dependencies = [
1494 1504
 "anyhow",
1495 1505
 "clap",
1496 1506
 "clap_complete",
1497 1507
 "clap_mangen",
1498 1508
 "console",
1509 +
 "cylinder",
1499 1510
 "indicatif",
1500 1511
 "libc",
1501 1512
 "mimalloc",
…
6 unchanged lines
1508 1519
1509 1520
[[package]]
1510 1521
name = "oxy-core"
1511 -
version = "0.7.0"
1522 +
version = "0.11.0"
1512 1523
dependencies = [
1513 1524
 "blake3",
1514 1525
 "ed25519-dalek",
…
20 unchanged lines
1535 1546
1536 1547
[[package]]
1537 1548
name = "oxy-server"
1538 -
version = "0.7.0"
1549 +
version = "0.11.0"
1539 1550
dependencies = [
1540 1551
 "axum",
1541 1552
 "axum-server",
…
7 unchanged lines
1549 1560
1550 1561
[[package]]
1551 1562
name = "oxy-strata"
1552 -
version = "0.7.0"
1563 +
version = "0.11.0"
1553 1564
dependencies = [
1554 1565
 "async-trait",
1555 1566
 "bytes",
…
1419 unchanged lines
modified Cargo.toml +3 −2
1 1
[workspace]
2 -
members = ["crates/oxy-core", "crates/oxy-server", "crates/oxy-strata", "."]
2 +
members = ["crates/oxy-core", "crates/oxy-server", "crates/oxy-strata", "crates/cylinder", "."]
3 3
resolver = "2"
4 4
5 5
[workspace.package]
6 -
version = "0.7.0"
6 +
version = "0.11.0"
7 7
edition = "2021"
8 8
license = "MIT OR Apache-2.0"
9 9
…
13 unchanged lines
23 23
oxy-core = { path = "crates/oxy-core" }
24 24
oxy-server = { path = "crates/oxy-server" }
25 25
oxy-strata = { path = "crates/oxy-strata" }
26 +
cylinder = { path = "crates/cylinder" }
26 27
clap = { version = "4", features = ["derive"] }
27 28
anyhow = "1"
28 29
libc = "0.2"
…
19 unchanged lines
modified README.md +7 −1
…
15 unchanged lines
16 16
17 17
Every change the agent makes is attributed to you and signed by the agent. `oxy log`, `oxy verify` and the hub therefore show who did the work, such as "Andy McCall · via Claude Code", and a policy can require that a person has approved it before it lands.
18 18
19 +
An agent can also work through `oxy mcp`, an MCP server for the repository: typed tools instead of a shell, signing as the identity it was started with, and nothing destructive unless you allow it (see [docs/mcp.md](docs/mcp.md)).
20 +
19 21
See [docs/ai_native_agent_workflows.md](docs/ai_native_agent_workflows.md) for the design and [docs/guide.md](docs/guide.md) §9 for the setup.
20 22
21 23
## What is different
…
13 unchanged lines
35 37
36 38
## Install
37 39
38 -
Builds for Linux, macOS and Windows are on the [releases page](https://oxygenhub.io/andymccall/oxygen/releases). These are early releases: the macOS and Windows builds are not signed and have had little testing.
40 +
Builds for Linux, macOS, Windows and Haiku are on the [releases page](https://oxygenhub.io/andymccall/oxygen/releases). These are early releases: the macOS and Windows builds are not signed and have had little testing.
39 41
40 42
- **Debian, Ubuntu and other Linux with apt** (x86_64, arm64 or riscv64): add the repository at https://oxygenhub.io/apt and install with apt, so `apt upgrade` keeps oxy current:
41 43
…
13 unchanged lines
55 57
- **Other Linux:** unpack `oxy-VERSION-linux-x86_64.tar.gz` (or `-linux-arm64`) and put `oxy` on your `PATH`.
56 58
- **macOS** (Apple Silicon or Intel): unpack `oxy-VERSION-macos-arm64.tar.gz` (or `-macos-x86_64`), then `xattr -d com.apple.quarantine oxy`, since the build is not notarised, and put it on your `PATH`.
57 59
- **Windows:** unzip `oxy-VERSION-windows-x86_64.zip` and put the folder on your `PATH`. SmartScreen may warn on first run, since the build is not signed.
60 +
- **Haiku** (R1/beta6, x86_64): unpack `oxy-VERSION-haiku-x86_64.tar.gz` and copy `oxy` to `~/config/non-packaged/bin`. It is built in a Haiku guest rather than by OxygenFlow, so it has its own `SHA256SUMS-haiku` and no build attestation.
58 61
59 62
`oh`, the OxygenHub command line, is released the same way from [OxygenHub's releases](https://oxygenhub.io/andymccall/oxygenhub/releases).
60 63
…
8 unchanged lines
69 72
70 73
The binary is `target/release/oxy`.
71 74
75 +
On Haiku, install `rust_bin` and `zstd_devel` with pkgman and build with `ZSTD_SYS_USE_PKG_CONFIG=1 cargo build --release`: zstd-sys then links Haiku's libzstd, since a system library also references zstd and ld refuses the bundled copy's hidden symbols.
76 +
72 77
**Changelog:** a commit that changes code adds a line under `## Unreleased` in CHANGELOG.md (Added, Changed, Fixed or Security, with the issue number); versions change only at release. `scripts/changelog-check.sh [PATH...]` checks it for the paths a commit takes (the working copy's changes without any) and runs in the gate; `NO_CHANGELOG=1` for code changes users won't notice.
73 78
74 79
## Quick start
…
77 unchanged lines
152 157
| `docs/strata.md` | Keeping a repository on your own storage: S3, GCS, Azure, a directory |
153 158
| `docs/protocol.md` | Wire protocol for other implementers |
154 159
| `docs/prompt.md` | Shell prompt integration |
160 +
| `docs/mcp.md` | `oxy mcp`: the repository as tools for AI agents |
155 161
156 162
## Licence
157 163
…
1 unchanged lines
added crates/cylinder/Cargo.toml +14 −0
1 +
[package]
2 +
name = "cylinder"
3 +
version.workspace = true
4 +
edition.workspace = true
5 +
license.workspace = true
6 +
description = "Cylinder: artifacts as trees of Oxygen chunks (docs/cylinder.md)"
7 +
8 +
[dependencies]
9 +
oxy-core = { path = "../oxy-core" }
10 +
blake3 = "1"
11 +
thiserror = "1"
12 +
13 +
[dev-dependencies]
14 +
tempfile = "3"
added crates/cylinder/src/lib.rs +454 −0
1 +
//! Cylinder, step C1 (docs/cylinder.md §4.1): an **artifact** is an
2 +
//! immutable, content-addressed object naming a tree of files, each kept as
3 +
//! an ordinary Oxygen blob of FastCDC BLAKE3 chunks. An image rebuilt with
4 +
//! one changed file moves only that file's changed chunks, and a binary in
5 +
//! a release archive and in a runner image is the same chunks.
6 +
//!
7 +
//! The model is oxy-core's ([`oxy_core::artifact`]): an `artifact` object
8 +
//! in canonical CBOR, named by its object hash, whose tree is a blob. This
9 +
//! crate packs a directory into one, opens, verifies and unpacks it, and
10 +
//! keeps it: `.oxy/artifacts/NAME` records each artifact this repository
11 +
//! keeps, which garbage collection keeps with its tree and files
12 +
//! ([`roots`]).
13 +
14 +
use oxy_core::hash::{ObjectRef, ObjectType};
15 +
use oxy_core::repo::Repo;
16 +
use std::collections::BTreeMap;
17 +
use std::path::{Path, PathBuf};
18 +
19 +
pub use oxy_core::artifact::{
20 +
    Artifact, DirTree, Entry, Kind, MODE_EXEC, MODE_FILE, MODE_LINK, VERSION,
21 +
};
22 +
23 +
#[derive(Debug, thiserror::Error)]
24 +
pub enum Error {
25 +
    #[error(transparent)]
26 +
    Repo(#[from] oxy_core::repo::RepoError),
27 +
    #[error("io: {0}")]
28 +
    Io(#[from] std::io::Error),
29 +
    #[error("not an artifact: {0}")]
30 +
    Object(#[from] oxy_core::objects::ObjectError),
31 +
    #[error("store: {0}")]
32 +
    Store(#[from] oxy_core::store::StoreError),
33 +
    #[error("{0}")]
34 +
    Bad(String),
35 +
}
36 +
37 +
pub type Result<T> = std::result::Result<T, Error>;
38 +
39 +
fn bad<T>(msg: impl Into<String>) -> Result<T> {
40 +
    Err(Error::Bad(msg.into()))
41 +
}
42 +
43 +
/// What to pack a directory as.
44 +
#[derive(Clone, Debug)]
45 +
pub struct Spec {
46 +
    pub kind: Kind,
47 +
    pub arch: Option<String>,
48 +
    pub os: Option<String>,
49 +
    pub entrypoint: Option<Vec<String>>,
50 +
    pub env: BTreeMap<String, String>,
51 +
    pub labels: BTreeMap<String, String>,
52 +
}
53 +
54 +
impl Spec {
55 +
    pub fn new(kind: Kind) -> Spec {
56 +
        Spec {
57 +
            kind,
58 +
            arch: None,
59 +
            os: None,
60 +
            entrypoint: None,
61 +
            env: BTreeMap::new(),
62 +
            labels: BTreeMap::new(),
63 +
        }
64 +
    }
65 +
}
66 +
67 +
/// An artifact kept in a repository, with its name.
68 +
#[derive(Clone, Debug)]
69 +
pub struct Kept {
70 +
    pub name: String,
71 +
    pub artifact: Artifact,
72 +
    /// Its object (its name, as a reference).
73 +
    pub object: ObjectRef,
74 +
}
75 +
76 +
fn kept_dir(repo: &Repo) -> PathBuf {
77 +
    repo.oxy_dir().join("artifacts")
78 +
}
79 +
80 +
/// Every file under `dir`, by relative path: regular files and links, in
81 +
/// path order. `.oxy` and `.git` directories are not part of an artifact.
82 +
fn walk(dir: &Path) -> Result<Vec<(String, PathBuf, std::fs::Metadata)>> {
83 +
    let mut out = Vec::new();
84 +
    let mut stack = vec![(dir.to_path_buf(), String::new())];
85 +
    while let Some((d, rel)) = stack.pop() {
86 +
        for e in std::fs::read_dir(&d)? {
87 +
            let e = e?;
88 +
            let name = e
89 +
                .file_name()
90 +
                .into_string()
91 +
                .map_err(|n| Error::Bad(format!("{n:?} is not UTF-8")))?;
92 +
            let path = if rel.is_empty() {
93 +
                name.clone()
94 +
            } else {
95 +
                format!("{rel}/{name}")
96 +
            };
97 +
            let md = std::fs::symlink_metadata(e.path())?;
98 +
            if md.is_dir() {
99 +
                if name != ".oxy" && name != ".git" {
100 +
                    stack.push((e.path(), path));
101 +
                }
102 +
            } else if md.is_file() || md.file_type().is_symlink() {
103 +
                out.push((path, e.path(), md));
104 +
            }
105 +
        }
106 +
    }
107 +
    out.sort_by(|a, b| a.0.cmp(&b.0));
108 +
    Ok(out)
109 +
}
110 +
111 +
#[cfg(unix)]
112 +
fn mode_of(md: &std::fs::Metadata) -> u32 {
113 +
    use std::os::unix::fs::PermissionsExt;
114 +
    if md.file_type().is_symlink() {
115 +
        MODE_LINK
116 +
    } else if md.permissions().mode() & 0o111 != 0 {
117 +
        MODE_EXEC
118 +
    } else {
119 +
        MODE_FILE
120 +
    }
121 +
}
122 +
123 +
#[cfg(not(unix))]
124 +
fn mode_of(md: &std::fs::Metadata) -> u32 {
125 +
    if md.file_type().is_symlink() {
126 +
        MODE_LINK
127 +
    } else {
128 +
        MODE_FILE
129 +
    }
130 +
}
131 +
132 +
/// Pack the files under `dir` into an artifact of `spec`'s kind in
133 +
/// `repo`'s store: each file a blob, so files and chunks the store holds
134 +
/// already cost nothing; then its tree and its encoding, kept under its
135 +
/// name.
136 +
pub fn pack(repo: &Repo, dir: &Path, spec: &Spec) -> Result<Kept> {
137 +
    if !dir.is_dir() {
138 +
        return bad(format!("{} is not a directory", dir.display()));
139 +
    }
140 +
    let mut tree = DirTree::default();
141 +
    for (path, full, md) in walk(dir)? {
142 +
        let mode = mode_of(&md);
143 +
        let bytes = if mode == MODE_LINK {
144 +
            std::fs::read_link(&full)?
145 +
                .to_string_lossy()
146 +
                .into_owned()
147 +
                .into_bytes()
148 +
        } else {
149 +
            std::fs::read(&full)?
150 +
        };
151 +
        let blob = repo.store_blob(&bytes)?;
152 +
        tree.entries.push(Entry {
153 +
            path,
154 +
            mode,
155 +
            blob,
156 +
            size: bytes.len() as u64,
157 +
        });
158 +
    }
159 +
    let artifact = Artifact {
160 +
        kind: spec.kind,
161 +
        arch: spec.arch.clone(),
162 +
        os: spec.os.clone(),
163 +
        tree: repo.store_blob(&tree.encode())?,
164 +
        entrypoint: spec.entrypoint.clone(),
165 +
        env: spec.env.clone(),
166 +
        labels: spec.labels.clone(),
167 +
        total_size: tree.entries.iter().map(|e| e.size).sum(),
168 +
        receipt: None,
169 +
    };
170 +
    let object = repo.store().put(ObjectType::Artifact, &artifact.encode())?;
171 +
    let name = object.to_hex();
172 +
    std::fs::create_dir_all(kept_dir(repo))?;
173 +
    std::fs::write(kept_dir(repo).join(&name), b"")?;
174 +
    Ok(Kept {
175 +
        name,
176 +
        artifact,
177 +
        object,
178 +
    })
179 +
}
180 +
181 +
/// The names of the artifacts kept in `repo`, sorted.
182 +
pub fn list(repo: &Repo) -> Result<Vec<String>> {
183 +
    let mut out = Vec::new();
184 +
    match std::fs::read_dir(kept_dir(repo)) {
185 +
        Ok(rd) => {
186 +
            for e in rd {
187 +
                let n = e?.file_name().to_string_lossy().into_owned();
188 +
                if n.len() == 64 && n.bytes().all(|b| b.is_ascii_hexdigit()) {
189 +
                    out.push(n);
190 +
                }
191 +
            }
192 +
        }
193 +
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
194 +
        Err(e) => return Err(e.into()),
195 +
    }
196 +
    out.sort();
197 +
    Ok(out)
198 +
}
199 +
200 +
/// The kept artifact `name` names, or a unique prefix of it (at least six
201 +
/// characters), read from the store, which checks it against its name.
202 +
pub fn open(repo: &Repo, name: &str) -> Result<Kept> {
203 +
    let name = name.to_ascii_lowercase();
204 +
    if name.len() < 6 {
205 +
        return bad("name an artifact by at least six characters of its name");
206 +
    }
207 +
    let found: Vec<String> = list(repo)?
208 +
        .into_iter()
209 +
        .filter(|n| n.starts_with(&name))
210 +
        .collect();
211 +
    let full = match found.as_slice() {
212 +
        [one] => one.clone(),
213 +
        [] => {
214 +
            return bad(format!(
215 +
                "no artifact {name} here; `oxy artifact list` shows them"
216 +
            ))
217 +
        }
218 +
        _ => {
219 +
            return bad(format!(
220 +
                "{name} names {} artifacts: give more of the name",
221 +
                found.len()
222 +
            ))
223 +
        }
224 +
    };
225 +
    let object = ObjectRef::from_hex(&full).map_or_else(
226 +
        || bad(format!("artifact {full}: its record is damaged")),
227 +
        Ok,
228 +
    )?;
229 +
    // The store checks the bytes against the object's name as it reads.
230 +
    let bytes = repo.store().get_typed(&object, ObjectType::Artifact)?;
231 +
    Ok(Kept {
232 +
        name: full,
233 +
        artifact: Artifact::decode(&bytes)?,
234 +
        object,
235 +
    })
236 +
}
237 +
238 +
/// An artifact's tree.
239 +
pub fn tree(repo: &Repo, a: &Artifact) -> Result<DirTree> {
240 +
    Ok(DirTree::decode(&repo.read_blob(&a.tree)?)?)
241 +
}
242 +
243 +
/// Check that every file of `k` is in the store whole: its bytes read, at
244 +
/// the size its tree says. Answers how many files were read.
245 +
pub fn verify(repo: &Repo, k: &Kept) -> Result<usize> {
246 +
    let t = tree(repo, &k.artifact)?;
247 +
    let mut total = 0;
248 +
    for e in &t.entries {
249 +
        let n = repo.read_blob(&e.blob)?.len() as u64;
250 +
        if n != e.size {
251 +
            return bad(format!(
252 +
                "{}: {n} bytes, but the tree says {}",
253 +
                e.path, e.size
254 +
            ));
255 +
        }
256 +
        total += n;
257 +
    }
258 +
    if total != k.artifact.total_size {
259 +
        return bad(format!(
260 +
            "the files are {total} bytes together, but the artifact says {}",
261 +
            k.artifact.total_size
262 +
        ));
263 +
    }
264 +
    Ok(t.entries.len())
265 +
}
266 +
267 +
/// Write `k`'s files under `dest`, which must not exist or be empty.
268 +
pub fn unpack(repo: &Repo, k: &Kept, dest: &Path) -> Result<usize> {
269 +
    if dest.exists() && std::fs::read_dir(dest)?.next().is_some() {
270 +
        return bad(format!("{} is not empty", dest.display()));
271 +
    }
272 +
    let t = tree(repo, &k.artifact)?;
273 +
    for e in &t.entries {
274 +
        let out = dest.join(&e.path);
275 +
        if let Some(p) = out.parent() {
276 +
            std::fs::create_dir_all(p)?;
277 +
        }
278 +
        let bytes = repo.read_blob(&e.blob)?;
279 +
        if e.mode == MODE_LINK {
280 +
            #[cfg(unix)]
281 +
            std::os::unix::fs::symlink(String::from_utf8_lossy(&bytes).as_ref(), &out)?;
282 +
            #[cfg(not(unix))]
283 +
            std::fs::write(&out, &bytes)?;
284 +
            continue;
285 +
        }
286 +
        std::fs::write(&out, &bytes)?;
287 +
        #[cfg(unix)]
288 +
        if e.mode == MODE_EXEC {
289 +
            use std::os::unix::fs::PermissionsExt;
290 +
            std::fs::set_permissions(&out, std::fs::Permissions::from_mode(0o755))?;
291 +
        }
292 +
    }
293 +
    Ok(t.entries.len())
294 +
}
295 +
296 +
/// The blobs garbage collection must keep for the kept artifacts: each
297 +
/// one's encoding, its tree and its files (`Repo::gc_keeping`).
298 +
pub fn roots(repo: &Repo) -> Result<Vec<ObjectRef>> {
299 +
    let mut out = Vec::new();
300 +
    for name in list(repo)? {
301 +
        let k = open(repo, &name)?;
302 +
        out.push(k.object);
303 +
        out.push(k.artifact.tree);
304 +
        out.extend(tree(repo, &k.artifact)?.entries.iter().map(|e| e.blob));
305 +
    }
306 +
    Ok(out)
307 +
}
308 +
309 +
#[cfg(test)]
310 +
mod tests {
311 +
    use super::*;
312 +
    use oxy_core::cbor::{self, Value};
313 +
314 +
    fn repo(dir: &Path) -> Repo {
315 +
        Repo::init(&dir.join("repo")).unwrap()
316 +
    }
317 +
318 +
    #[test]
319 +
    fn a_directory_packs_into_an_artifact_and_back() {
320 +
        let t = tempfile::tempdir().unwrap();
321 +
        let r = repo(t.path());
322 +
        let src = t.path().join("out");
323 +
        std::fs::create_dir_all(src.join("bin")).unwrap();
324 +
        std::fs::write(src.join("README"), b"hello\n").unwrap();
325 +
        std::fs::write(src.join("bin/tool"), vec![7u8; 300_000]).unwrap();
326 +
        #[cfg(unix)]
327 +
        {
328 +
            use std::os::unix::fs::PermissionsExt;
329 +
            std::fs::set_permissions(src.join("bin/tool"), std::fs::Permissions::from_mode(0o755))
330 +
                .unwrap();
331 +
            std::os::unix::fs::symlink("bin/tool", src.join("tool")).unwrap();
332 +
        }
333 +
        let mut spec = Spec::new(Kind::Archive);
334 +
        spec.arch = Some("x86_64".into());
335 +
        spec.labels.insert("version".into(), "0.1.0".into());
336 +
        let k = pack(&r, &src, &spec).unwrap();
337 +
        assert_eq!(k.name, k.artifact.name().to_hex());
338 +
        assert_eq!(
339 +
            k.artifact.total_size,
340 +
            300_006 + if cfg!(unix) { 8 } else { 0 }
341 +
        );
342 +
        assert_eq!(list(&r).unwrap(), vec![k.name.clone()]);
343 +
344 +
        let again = open(&r, &k.name[..10]).unwrap();
345 +
        assert_eq!(again.artifact, k.artifact);
346 +
        let paths: Vec<(String, u32)> = tree(&r, &again.artifact)
347 +
            .unwrap()
348 +
            .entries
349 +
            .into_iter()
350 +
            .map(|e| (e.path, e.mode))
351 +
            .collect();
352 +
        assert_eq!(paths[0], ("README".into(), MODE_FILE));
353 +
        assert_eq!(
354 +
            paths[1],
355 +
            (
356 +
                "bin/tool".into(),
357 +
                if cfg!(unix) { MODE_EXEC } else { MODE_FILE }
358 +
            )
359 +
        );
360 +
        assert_eq!(verify(&r, &again).unwrap(), paths.len());
361 +
362 +
        let dest = t.path().join("back");
363 +
        unpack(&r, &again, &dest).unwrap();
364 +
        assert_eq!(
365 +
            std::fs::read(dest.join("bin/tool")).unwrap(),
366 +
            vec![7u8; 300_000]
367 +
        );
368 +
        #[cfg(unix)]
369 +
        assert_eq!(
370 +
            std::fs::read_link(dest.join("tool")).unwrap(),
371 +
            Path::new("bin/tool")
372 +
        );
373 +
        assert!(unpack(&r, &again, &dest)
374 +
            .unwrap_err()
375 +
            .to_string()
376 +
            .contains("not empty"));
377 +
378 +
        // The same files pack to the same name; garbage collection keeps it.
379 +
        assert_eq!(pack(&r, &src, &spec).unwrap().name, k.name);
380 +
        let keep = roots(&r).unwrap();
381 +
        assert!(keep.contains(&k.object) && keep.contains(&k.artifact.tree));
382 +
        r.gc_keeping(true, &keep, &mut |_| {}).unwrap();
383 +
        // An artifact is an object of its own, which fsck reads and accepts.
384 +
        let report = r.fsck().unwrap();
385 +
        let errors: Vec<_> = report
386 +
            .problems
387 +
            .iter()
388 +
            .filter(|p| p.severity == oxy_core::repo::Severity::Error)
389 +
            .collect();
390 +
        assert!(errors.is_empty(), "{errors:?}");
391 +
        assert_eq!(
392 +
            verify(&r, &open(&r, &k.name).unwrap()).unwrap(),
393 +
            paths.len()
394 +
        );
395 +
    }
396 +
397 +
    #[test]
398 +
    fn encodings_are_canonical_and_checked() {
399 +
        let a = Artifact {
400 +
            kind: Kind::RunnerImage,
401 +
            arch: Some("riscv64".into()),
402 +
            os: Some("linux".into()),
403 +
            tree: ObjectRef([1; 32]),
404 +
            entrypoint: Some(vec!["/sbin/init".into()]),
405 +
            env: [("PATH".to_string(), "/bin".to_string())].into(),
406 +
            labels: BTreeMap::new(),
407 +
            total_size: 42,
408 +
            receipt: Some(ObjectRef([2; 32])),
409 +
        };
410 +
        let bytes = a.encode();
411 +
        assert_eq!(Artifact::decode(&bytes).unwrap(), a);
412 +
        assert_eq!(
413 +
            a.name(),
414 +
            oxy_core::hash::hash_object(ObjectType::Artifact, &bytes)
415 +
        );
416 +
        assert!(Kind::parse("tarball")
417 +
            .unwrap_err()
418 +
            .to_string()
419 +
            .contains("runner-image"));
420 +
421 +
        let wrong = |v: Value| Artifact::decode(&cbor::encode(&v)).unwrap_err().to_string();
422 +
        assert!(wrong(Value::map([(0, Value::Uint(2))])).contains("version"));
423 +
        assert!(wrong(Value::map([(0, Value::Uint(1)), (10, Value::Null)])).contains("key 10"));
424 +
425 +
        let t = DirTree {
426 +
            entries: vec![Entry {
427 +
                path: "a".into(),
428 +
                mode: MODE_FILE,
429 +
                blob: ObjectRef([3; 32]),
430 +
                size: 1,
431 +
            }],
432 +
        };
433 +
        assert_eq!(DirTree::decode(&t.encode()).unwrap(), t);
434 +
        let entry = |p: &str, mode: u32| {
435 +
            Value::Array(vec![
436 +
                Value::Text(p.into()),
437 +
                Value::Uint(mode as u64),
438 +
                Value::Bytes(vec![0; 32]),
439 +
                Value::Uint(0),
440 +
            ])
441 +
        };
442 +
        let tree = |items: Vec<Value>| {
443 +
            DirTree::decode(&cbor::encode(&Value::map([
444 +
                (0, Value::Uint(1)),
445 +
                (1, Value::Array(items)),
446 +
            ])))
447 +
            .unwrap_err()
448 +
            .to_string()
449 +
        };
450 +
        assert!(tree(vec![entry("../x", MODE_FILE)]).contains("clean"));
451 +
        assert!(tree(vec![entry("b", MODE_FILE), entry("a", MODE_FILE)]).contains("order"));
452 +
        assert!(tree(vec![entry("a", 0o040000)]).contains("mode"));
453 +
    }
454 +
}
added crates/oxy-core/src/artifact.rs +303 −0
1 +
//! Artifacts (Cylinder, docs/cylinder.md §4.1, oxygen#108): an immutable
2 +
//! object naming a tree of files, each an ordinary blob of FastCDC BLAKE3
3 +
//! chunks, so an image rebuilt with one changed file moves only that file's
4 +
//! changed chunks, and a binary in an archive and in an image is the same
5 +
//! chunks. An artifact is an object of its own (`artifact`) in canonical
6 +
//! CBOR; its tree is a blob. The `cylinder` crate packs, unpacks and keeps
7 +
//! them.
8 +
9 +
use crate::cbor::{self, Value};
10 +
use crate::hash::{hash_object, ObjectRef, ObjectType};
11 +
use crate::objects::{schema, ObjectError};
12 +
use std::collections::BTreeMap;
13 +
14 +
type R<T> = std::result::Result<T, ObjectError>;
15 +
16 +
/// The encoding's version this crate writes and reads.
17 +
pub const VERSION: u64 = 1;
18 +
19 +
/// What an artifact is for.
20 +
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21 +
pub enum Kind {
22 +
    Container,
23 +
    RunnerImage,
24 +
    Package,
25 +
    Archive,
26 +
    Report,
27 +
}
28 +
29 +
impl Kind {
30 +
    pub const ALL: [Kind; 5] = [
31 +
        Kind::Container,
32 +
        Kind::RunnerImage,
33 +
        Kind::Package,
34 +
        Kind::Archive,
35 +
        Kind::Report,
36 +
    ];
37 +
38 +
    pub fn as_str(self) -> &'static str {
39 +
        match self {
40 +
            Kind::Container => "container",
41 +
            Kind::RunnerImage => "runner-image",
42 +
            Kind::Package => "package",
43 +
            Kind::Archive => "archive",
44 +
            Kind::Report => "report",
45 +
        }
46 +
    }
47 +
48 +
    pub fn parse(s: &str) -> R<Kind> {
49 +
        Kind::ALL.into_iter().find(|k| k.as_str() == s).map_or_else(
50 +
            || {
51 +
                schema(format!(
52 +
                    "{s:?} is not a kind of artifact: {}",
53 +
                    Kind::ALL.map(|k| k.as_str()).join(", ")
54 +
                ))
55 +
            },
56 +
            Ok,
57 +
        )
58 +
    }
59 +
}
60 +
61 +
/// A file's mode in a tree: a regular file, an executable one, or a
62 +
/// symbolic link (whose blob is its target).
63 +
pub const MODE_FILE: u32 = 0o100644;
64 +
pub const MODE_EXEC: u32 = 0o100755;
65 +
pub const MODE_LINK: u32 = 0o120000;
66 +
67 +
/// A file of an artifact's tree.
68 +
#[derive(Clone, Debug, PartialEq, Eq)]
69 +
pub struct Entry {
70 +
    /// `/`-separated, relative, never `.` or `..` parts.
71 +
    pub path: String,
72 +
    pub mode: u32,
73 +
    pub blob: ObjectRef,
74 +
    pub size: u64,
75 +
}
76 +
77 +
/// An artifact's files, in path order: paths, modes, blobs and sizes.
78 +
#[derive(Clone, Debug, Default, PartialEq, Eq)]
79 +
pub struct DirTree {
80 +
    pub entries: Vec<Entry>,
81 +
}
82 +
83 +
fn path_ok(p: &str) -> bool {
84 +
    !p.is_empty()
85 +
        && !p.starts_with('/')
86 +
        && p.split('/').all(|s| !s.is_empty() && s != "." && s != "..")
87 +
}
88 +
89 +
fn obj(r: &ObjectRef) -> Value {
90 +
    Value::Bytes(r.0.to_vec())
91 +
}
92 +
93 +
fn get_obj(v: &Value, what: &str) -> R<ObjectRef> {
94 +
    match v.as_bytes() {
95 +
        Some(b) if b.len() == 32 => {
96 +
            let mut a = [0u8; 32];
97 +
            a.copy_from_slice(b);
98 +
            Ok(ObjectRef(a))
99 +
        }
100 +
        _ => schema(format!("{what} is not an object reference")),
101 +
    }
102 +
}
103 +
104 +
fn get_text(v: &Value, what: &str) -> R<String> {
105 +
    v.as_text()
106 +
        .map(str::to_string)
107 +
        .map_or_else(|| schema(format!("{what} is not text")), Ok)
108 +
}
109 +
110 +
fn get_uint(v: &Value, what: &str) -> R<u64> {
111 +
    v.as_uint()
112 +
        .map_or_else(|| schema(format!("{what} is not a number")), Ok)
113 +
}
114 +
115 +
impl DirTree {
116 +
    pub fn encode(&self) -> Vec<u8> {
117 +
        let entries = self
118 +
            .entries
119 +
            .iter()
120 +
            .map(|e| {
121 +
                Value::Array(vec![
122 +
                    Value::Text(e.path.clone()),
123 +
                    Value::Uint(e.mode as u64),
124 +
                    obj(&e.blob),
125 +
                    Value::Uint(e.size),
126 +
                ])
127 +
            })
128 +
            .collect();
129 +
        cbor::encode(&Value::map([
130 +
            (0, Value::Uint(VERSION)),
131 +
            (1, Value::Array(entries)),
132 +
        ]))
133 +
    }
134 +
135 +
    /// Read and check a tree: a known version, clean paths, each once, in
136 +
    /// order, with a known mode.
137 +
    pub fn decode(bytes: &[u8]) -> R<DirTree> {
138 +
        let v = cbor::decode(bytes)?;
139 +
        let m = v.as_map().map_or_else(|| schema("a tree is a map"), Ok)?;
140 +
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
141 +
            return schema(format!(
142 +
                "tree version {:?} is not known; this reads {VERSION}",
143 +
                m.get(&0)
144 +
            ));
145 +
        }
146 +
        let mut entries = Vec::new();
147 +
        for item in m.get(&1).and_then(Value::as_array).unwrap_or_default() {
148 +
            let [p, mode, blob, size] = item.as_array().unwrap_or_default() else {
149 +
                return schema("a tree entry is [path, mode, blob, size]");
150 +
            };
151 +
            let path = get_text(p, "a path")?;
152 +
            let mode = get_uint(mode, "a mode")? as u32;
153 +
            if !path_ok(&path) {
154 +
                return schema(format!("{path:?} is not a clean relative path"));
155 +
            }
156 +
            if ![MODE_FILE, MODE_EXEC, MODE_LINK].contains(&mode) {
157 +
                return schema(format!(
158 +
                    "{path}: mode {mode:o} is not a file, an executable or a link"
159 +
                ));
160 +
            }
161 +
            if entries.last().is_some_and(|l: &Entry| l.path >= path) {
162 +
                return schema(format!(
163 +
                    "{path}: entries are not in path order, or one is there twice"
164 +
                ));
165 +
            }
166 +
            entries.push(Entry {
167 +
                path,
168 +
                mode,
169 +
                blob: get_obj(blob, "a blob")?,
170 +
                size: get_uint(size, "a size")?,
171 +
            });
172 +
        }
173 +
        Ok(DirTree { entries })
174 +
    }
175 +
}
176 +
177 +
/// An artifact (docs/cylinder.md §4.1).
178 +
#[derive(Clone, Debug, PartialEq, Eq)]
179 +
pub struct Artifact {
180 +
    pub kind: Kind,
181 +
    /// `x86_64`, `riscv64`, `aarch64`.
182 +
    pub arch: Option<String>,
183 +
    /// `linux`, `darwin`, `none`.
184 +
    pub os: Option<String>,
185 +
    /// The [`DirTree`]'s blob.
186 +
    pub tree: ObjectRef,
187 +
    /// The default command, for a container or an image.
188 +
    pub entrypoint: Option<Vec<String>>,
189 +
    pub env: BTreeMap<String, String>,
190 +
    pub labels: BTreeMap<String, String>,
191 +
    /// The files' bytes together.
192 +
    pub total_size: u64,
193 +
    /// The receipt of the Flow job that built it, when one did.
194 +
    pub receipt: Option<ObjectRef>,
195 +
}
196 +
197 +
fn pairs(m: &BTreeMap<String, String>) -> Value {
198 +
    if m.is_empty() {
199 +
        return Value::Null;
200 +
    }
201 +
    Value::Array(
202 +
        m.iter()
203 +
            .map(|(k, v)| Value::Array(vec![Value::Text(k.clone()), Value::Text(v.clone())]))
204 +
            .collect(),
205 +
    )
206 +
}
207 +
208 +
fn get_pairs(v: Option<&Value>, what: &str) -> R<BTreeMap<String, String>> {
209 +
    let mut out = BTreeMap::new();
210 +
    let Some(v) = v.filter(|v| !v.is_null()) else {
211 +
        return Ok(out);
212 +
    };
213 +
    for p in v
214 +
        .as_array()
215 +
        .map_or_else(|| schema(format!("{what} is a list of pairs")), Ok)?
216 +
    {
217 +
        let [k, val] = p.as_array().unwrap_or_default() else {
218 +
            return schema(format!("{what}: a pair is [key, value]"));
219 +
        };
220 +
        let k = get_text(k, what)?;
221 +
        if out.insert(k.clone(), get_text(val, what)?).is_some() {
222 +
            return schema(format!("{what}: {k} is there twice"));
223 +
        }
224 +
    }
225 +
    Ok(out)
226 +
}
227 +
228 +
fn opt_text(v: &Option<String>) -> Value {
229 +
    v.as_ref().map_or(Value::Null, |s| Value::Text(s.clone()))
230 +
}
231 +
232 +
impl Artifact {
233 +
    /// Its canonical CBOR, keys as in docs/cylinder.md §4.1.
234 +
    pub fn encode(&self) -> Vec<u8> {
235 +
        let entry = self.entrypoint.as_ref().map_or(Value::Null, |a| {
236 +
            Value::Array(a.iter().cloned().map(Value::Text).collect())
237 +
        });
238 +
        cbor::encode(&Value::map([
239 +
            (0, Value::Uint(VERSION)),
240 +
            (1, Value::Text(self.kind.as_str().into())),
241 +
            (2, opt_text(&self.arch)),
242 +
            (3, opt_text(&self.os)),
243 +
            (4, obj(&self.tree)),
244 +
            (5, entry),
245 +
            (6, pairs(&self.env)),
246 +
            (7, pairs(&self.labels)),
247 +
            (8, Value::Uint(self.total_size)),
248 +
            (9, self.receipt.as_ref().map_or(Value::Null, obj)),
249 +
        ]))
250 +
    }
251 +
252 +
    pub fn decode(bytes: &[u8]) -> R<Artifact> {
253 +
        let v = cbor::decode(bytes)?;
254 +
        let m = v
255 +
            .as_map()
256 +
            .map_or_else(|| schema("an artifact is a map"), Ok)?;
257 +
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
258 +
            return schema(format!(
259 +
                "artifact version {:?} is not known; this reads {VERSION}",
260 +
                m.get(&0)
261 +
            ));
262 +
        }
263 +
        if let Some(k) = m.keys().find(|k| **k > 9) {
264 +
            return schema(format!("key {k} is not part of an artifact"));
265 +
        }
266 +
        let text = |k: u64, what: &str| -> R<Option<String>> {
267 +
            match m.get(&k) {
268 +
                None | Some(Value::Null) => Ok(None),
269 +
                Some(v) => get_text(v, what).map(Some),
270 +
            }
271 +
        };
272 +
        let entrypoint = match m.get(&5) {
273 +
            None | Some(Value::Null) => None,
274 +
            Some(v) => Some(
275 +
                v.as_array()
276 +
                    .unwrap_or_default()
277 +
                    .iter()
278 +
                    .map(|a| get_text(a, "the entrypoint"))
279 +
                    .collect::<R<_>>()?,
280 +
            ),
281 +
        };
282 +
        let receipt = match m.get(&9) {
283 +
            None | Some(Value::Null) => None,
284 +
            Some(v) => Some(get_obj(v, "the receipt")?),
285 +
        };
286 +
        Ok(Artifact {
287 +
            kind: Kind::parse(&text(1, "the kind")?.unwrap_or_default())?,
288 +
            arch: text(2, "the architecture")?,
289 +
            os: text(3, "the system")?,
290 +
            tree: get_obj(m.get(&4).unwrap_or(&Value::Null), "the tree")?,
291 +
            entrypoint,
292 +
            env: get_pairs(m.get(&6), "env")?,
293 +
            labels: get_pairs(m.get(&7), "labels")?,
294 +
            total_size: get_uint(m.get(&8).unwrap_or(&Value::Null), "the size")?,
295 +
            receipt,
296 +
        })
297 +
    }
298 +
299 +
    /// Its name: its object's hash, as every Oxygen object is named.
300 +
    pub fn name(&self) -> ObjectRef {
301 +
        hash_object(ObjectType::Artifact, &self.encode())
302 +
    }
303 +
}
modified crates/oxy-core/src/hash.rs +6 −1
…
67 unchanged lines
68 68
    Certificate,
69 69
    /// An author's grant to a certified subject (§14.7).
70 70
    Grant,
71 +
    /// A tree of files as one immutable thing: an image, a package, an
72 +
    /// archive (Cylinder, docs/cylinder.md §4.1).
73 +
    Artifact,
71 74
}
72 75
73 76
impl ObjectType {
74 -
    pub const ALL: [ObjectType; 12] = [
77 +
    pub const ALL: [ObjectType; 13] = [
75 78
        ObjectType::Chunk,
76 79
        ObjectType::Blob,
77 80
        ObjectType::Change,
…
6 unchanged lines
84 87
        ObjectType::Receipt,
85 88
        ObjectType::Certificate,
86 89
        ObjectType::Grant,
90 +
        ObjectType::Artifact,
87 91
    ];
88 92
89 93
    pub fn name(self) -> &'static str {
…
10 unchanged lines
100 104
            ObjectType::Receipt => "receipt",
101 105
            ObjectType::Certificate => "certificate",
102 106
            ObjectType::Grant => "grant",
107 +
            ObjectType::Artifact => "artifact",
103 108
        }
104 109
    }
105 110
…
59 unchanged lines
modified crates/oxy-core/src/lib.rs +2 −0
…
6 unchanged lines
7 7
8 8
pub mod alloc_stats;
9 9
pub mod approval;
10 +
pub mod artifact;
10 11
pub mod attest;
11 12
pub mod attributes;
12 13
pub mod cbor;
…
11 unchanged lines
24 25
pub mod ids;
25 26
pub mod lanes;
26 27
pub mod lines;
28 +
pub mod mcp;
27 29
pub mod objects;
28 30
pub mod oplog;
29 31
pub mod policy;
…
13 unchanged lines
added crates/oxy-core/src/mcp.rs +353 −0
1 +
//! A Model Context Protocol server over stdio (oxygen#159): JSON-RPC 2.0,
2 +
//! one message per line, as MCP's stdio transport has it. This is only the
3 +
//! protocol: `initialize`, `ping`, `tools/list` and `tools/call`, with the
4 +
//! tools themselves supplied by the caller (`oxy mcp`, and `oh mcp` in the
5 +
//! hub, which reaches this module through its pinned oxygen).
6 +
//!
7 +
//! Nothing is written to stdout but protocol messages: a tool's own
8 +
//! output goes into its result.
9 +
10 +
use serde_json::{json, Value};
11 +
use std::io::{BufRead, Write};
12 +
13 +
/// The protocol versions this server speaks, newest first. A client asking
14 +
/// for one of them gets it back; any other gets the newest.
15 +
pub const PROTOCOL_VERSIONS: &[&str] = &["2025-06-18", "2025-03-26", "2024-11-05"];
16 +
17 +
/// One tool as `tools/list` describes it.
18 +
#[derive(Clone, Debug)]
19 +
pub struct Tool {
20 +
    pub name: String,
21 +
    pub description: String,
22 +
    /// A JSON Schema object for the arguments.
23 +
    pub input_schema: Value,
24 +
    /// Reads only: no change to the repository, the remote or the hub.
25 +
    pub read_only: bool,
26 +
    /// May lose work or history (undo, abandon, deletes).
27 +
    pub destructive: bool,
28 +
}
29 +
30 +
impl Tool {
31 +
    fn describe(&self) -> Value {
32 +
        json!({
33 +
            "name": self.name,
34 +
            "description": self.description,
35 +
            "inputSchema": self.input_schema,
36 +
            "annotations": {
37 +
                "readOnlyHint": self.read_only,
38 +
                "destructiveHint": self.destructive,
39 +
            },
40 +
        })
41 +
    }
42 +
}
43 +
44 +
/// What a tool call produced: text for the model to read, the same as JSON
45 +
/// when there is some, and whether it failed. A failed tool is a result, not
46 +
/// a protocol error, so the model sees why.
47 +
#[derive(Clone, Debug, Default, PartialEq)]
48 +
pub struct ToolOutput {
49 +
    pub text: String,
50 +
    pub structured: Option<Value>,
51 +
    pub is_error: bool,
52 +
}
53 +
54 +
impl ToolOutput {
55 +
    pub fn ok(text: impl Into<String>) -> ToolOutput {
56 +
        ToolOutput {
57 +
            text: text.into(),
58 +
            structured: None,
59 +
            is_error: false,
60 +
        }
61 +
    }
62 +
63 +
    pub fn error(text: impl Into<String>) -> ToolOutput {
64 +
        ToolOutput {
65 +
            text: text.into(),
66 +
            structured: None,
67 +
            is_error: true,
68 +
        }
69 +
    }
70 +
71 +
    fn to_json(&self) -> Value {
72 +
        let mut out = json!({
73 +
            "content": [{ "type": "text", "text": self.text }],
74 +
            "isError": self.is_error,
75 +
        });
76 +
        // Structured content must be an object; an array is wrapped.
77 +
        if let Some(s) = &self.structured {
78 +
            let obj = if s.is_object() {
79 +
                s.clone()
80 +
            } else {
81 +
                json!({ "result": s })
82 +
            };
83 +
            out["structuredContent"] = obj;
84 +
        }
85 +
        out
86 +
    }
87 +
}
88 +
89 +
/// The tools a server offers.
90 +
pub trait Tools {
91 +
    fn list(&self) -> Vec<Tool>;
92 +
    /// Run a tool. `None` when no tool has that name.
93 +
    fn call(&self, name: &str, args: &Value) -> Option<ToolOutput>;
94 +
}
95 +
96 +
/// Who the server is, for `initialize`.
97 +
#[derive(Clone, Debug)]
98 +
pub struct ServerInfo {
99 +
    pub name: String,
100 +
    pub version: String,
101 +
    /// Shown to the model: what the server is for and how to use it safely.
102 +
    pub instructions: String,
103 +
}
104 +
105 +
const PARSE_ERROR: i64 = -32700;
106 +
const INVALID_REQUEST: i64 = -32600;
107 +
const METHOD_NOT_FOUND: i64 = -32601;
108 +
const INVALID_PARAMS: i64 = -32602;
109 +
110 +
fn error(id: Value, code: i64, message: &str) -> Value {
111 +
    json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
112 +
}
113 +
114 +
fn result(id: Value, result: Value) -> Value {
115 +
    json!({ "jsonrpc": "2.0", "id": id, "result": result })
116 +
}
117 +
118 +
/// Answer one message; `None` for a notification (no reply is due).
119 +
pub fn handle(info: &ServerInfo, tools: &dyn Tools, message: &Value) -> Option<Value> {
120 +
    let Some(obj) = message.as_object() else {
121 +
        return Some(error(
122 +
            Value::Null,
123 +
            INVALID_REQUEST,
124 +
            "a message is a JSON object",
125 +
        ));
126 +
    };
127 +
    let method = obj.get("method").and_then(Value::as_str);
128 +
    let id = obj.get("id").cloned();
129 +
    let Some(method) = method else {
130 +
        // A response to something we never sent, or nonsense: a request
131 +
        // without a method gets an error, anything else is ignored.
132 +
        return id.map(|id| error(id, INVALID_REQUEST, "a request needs a method"));
133 +
    };
134 +
    // Notifications (initialized, cancelled, …) carry no id and get no reply.
135 +
    let id = id?;
136 +
    let params = obj.get("params").cloned().unwrap_or(Value::Null);
137 +
    Some(match method {
138 +
        "initialize" => {
139 +
            let asked = params.get("protocolVersion").and_then(Value::as_str);
140 +
            let version = asked
141 +
                .filter(|v| PROTOCOL_VERSIONS.contains(v))
142 +
                .unwrap_or(PROTOCOL_VERSIONS[0]);
143 +
            result(
144 +
                id,
145 +
                json!({
146 +
                    "protocolVersion": version,
147 +
                    "capabilities": { "tools": { "listChanged": false } },
148 +
                    "serverInfo": { "name": info.name, "version": info.version },
149 +
                    "instructions": info.instructions,
150 +
                }),
151 +
            )
152 +
        }
153 +
        "ping" => result(id, json!({})),
154 +
        "tools/list" => result(
155 +
            id,
156 +
            json!({ "tools": tools.list().iter().map(Tool::describe).collect::<Vec<_>>() }),
157 +
        ),
158 +
        "tools/call" => {
159 +
            let Some(name) = params.get("name").and_then(Value::as_str) else {
160 +
                return Some(error(
161 +
                    id,
162 +
                    INVALID_PARAMS,
163 +
                    "tools/call needs the tool's name",
164 +
                ));
165 +
            };
166 +
            let args = params
167 +
                .get("arguments")
168 +
                .cloned()
169 +
                .unwrap_or_else(|| json!({}));
170 +
            if !args.is_object() {
171 +
                return Some(error(
172 +
                    id,
173 +
                    INVALID_PARAMS,
174 +
                    "a tool's arguments are a JSON object",
175 +
                ));
176 +
            }
177 +
            match tools.call(name, &args) {
178 +
                Some(out) => result(id, out.to_json()),
179 +
                None => error(id, INVALID_PARAMS, &format!("no tool named {name}")),
180 +
            }
181 +
        }
182 +
        other => error(id, METHOD_NOT_FOUND, &format!("{other} is not supported")),
183 +
    })
184 +
}
185 +
186 +
/// Serve until the input ends: read a message per line, write a reply per
187 +
/// request. A line that is not JSON gets a parse error; blank lines are
188 +
/// skipped.
189 +
pub fn serve(
190 +
    info: &ServerInfo,
191 +
    tools: &dyn Tools,
192 +
    input: impl BufRead,
193 +
    mut output: impl Write,
194 +
) -> std::io::Result<()> {
195 +
    for line in input.lines() {
196 +
        let line = line?;
197 +
        if line.trim().is_empty() {
198 +
            continue;
199 +
        }
200 +
        let reply = match serde_json::from_str::<Value>(&line) {
201 +
            Ok(Value::Array(batch)) => {
202 +
                let replies: Vec<Value> = batch
203 +
                    .iter()
204 +
                    .filter_map(|m| handle(info, tools, m))
205 +
                    .collect();
206 +
                (!replies.is_empty()).then_some(Value::Array(replies))
207 +
            }
208 +
            Ok(message) => handle(info, tools, &message),
209 +
            Err(e) => Some(error(Value::Null, PARSE_ERROR, &format!("not JSON: {e}"))),
210 +
        };
211 +
        if let Some(reply) = reply {
212 +
            writeln!(output, "{reply}")?;
213 +
            output.flush()?;
214 +
        }
215 +
    }
216 +
    Ok(())
217 +
}
218 +
219 +
/// A string argument, if given and a string.
220 +
pub fn arg_str<'a>(args: &'a Value, key: &str) -> Option<&'a str> {
221 +
    args.get(key).and_then(Value::as_str)
222 +
}
223 +
224 +
/// A list of strings, if given; a single string counts as a list of one.
225 +
pub fn arg_strings(args: &Value, key: &str) -> Result<Vec<String>, String> {
226 +
    match args.get(key) {
227 +
        None | Some(Value::Null) => Ok(vec![]),
228 +
        Some(Value::String(s)) => Ok(vec![s.clone()]),
229 +
        Some(Value::Array(a)) => a
230 +
            .iter()
231 +
            .map(|v| {
232 +
                v.as_str()
233 +
                    .map(str::to_string)
234 +
                    .ok_or_else(|| format!("{key} must be a list of strings"))
235 +
            })
236 +
            .collect(),
237 +
        Some(_) => Err(format!("{key} must be a list of strings")),
238 +
    }
239 +
}
240 +
241 +
#[cfg(test)]
242 +
mod tests {
243 +
    use super::*;
244 +
245 +
    struct Echo;
246 +
247 +
    impl Tools for Echo {
248 +
        fn list(&self) -> Vec<Tool> {
249 +
            vec![Tool {
250 +
                name: "echo".into(),
251 +
                description: "says it back".into(),
252 +
                input_schema: json!({"type": "object"}),
253 +
                read_only: true,
254 +
                destructive: false,
255 +
            }]
256 +
        }
257 +
        fn call(&self, name: &str, args: &Value) -> Option<ToolOutput> {
258 +
            (name == "echo").then(|| ToolOutput {
259 +
                text: args.to_string(),
260 +
                structured: Some(json!([1, 2])),
261 +
                is_error: false,
262 +
            })
263 +
        }
264 +
    }
265 +
266 +
    fn info() -> ServerInfo {
267 +
        ServerInfo {
268 +
            name: "t".into(),
269 +
            version: "1".into(),
270 +
            instructions: "be careful".into(),
271 +
        }
272 +
    }
273 +
274 +
    fn talk(lines: &str) -> Vec<Value> {
275 +
        let mut out = Vec::new();
276 +
        serve(&info(), &Echo, lines.as_bytes(), &mut out).unwrap();
277 +
        String::from_utf8(out)
278 +
            .unwrap()
279 +
            .lines()
280 +
            .map(|l| serde_json::from_str(l).unwrap())
281 +
            .collect()
282 +
    }
283 +
284 +
    #[test]
285 +
    fn a_session_initializes_lists_and_calls() {
286 +
        let r = talk(concat!(
287 +
            r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}"#,
288 +
            "\n",
289 +
            r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
290 +
            "\n\n",
291 +
            r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#,
292 +
            "\n",
293 +
            r#"{"jsonrpc":"2.0","id":"three","method":"tools/call","params":{"name":"echo","arguments":{"a":1}}}"#,
294 +
            "\n",
295 +
        ));
296 +
        assert_eq!(r.len(), 3, "the notification gets no reply: {r:?}");
297 +
        assert_eq!(
298 +
            r[0]["result"]["protocolVersion"], "2025-03-26",
299 +
            "a version we speak is kept"
300 +
        );
301 +
        assert_eq!(r[0]["result"]["instructions"], "be careful");
302 +
        assert_eq!(r[1]["result"]["tools"][0]["name"], "echo");
303 +
        assert_eq!(
304 +
            r[1]["result"]["tools"][0]["annotations"]["readOnlyHint"],
305 +
            true
306 +
        );
307 +
        assert_eq!(r[2]["id"], "three");
308 +
        assert_eq!(r[2]["result"]["content"][0]["text"], r#"{"a":1}"#);
309 +
        assert_eq!(
310 +
            r[2]["result"]["structuredContent"],
311 +
            json!({"result": [1, 2]}),
312 +
            "an array is wrapped in an object"
313 +
        );
314 +
        assert_eq!(r[2]["result"]["isError"], false);
315 +
    }
316 +
317 +
    #[test]
318 +
    fn errors_are_protocol_errors_and_unknown_versions_get_the_newest() {
319 +
        let r = talk(concat!(
320 +
            "not json\n",
321 +
            r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1999-01-01"}}"#,
322 +
            "\n",
323 +
            r#"{"jsonrpc":"2.0","id":2,"method":"resources/list"}"#,
324 +
            "\n",
325 +
            r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"nope"}}"#,
326 +
            "\n",
327 +
            r#"{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"echo","arguments":[1]}}"#,
328 +
            "\n",
329 +
            r#"[{"jsonrpc":"2.0","id":5,"method":"ping"},{"jsonrpc":"2.0","method":"notifications/cancelled"}]"#,
330 +
            "\n",
331 +
        ));
332 +
        assert_eq!(r[0]["error"]["code"], PARSE_ERROR);
333 +
        assert_eq!(r[1]["result"]["protocolVersion"], PROTOCOL_VERSIONS[0]);
334 +
        assert_eq!(r[2]["error"]["code"], METHOD_NOT_FOUND);
335 +
        assert_eq!(r[3]["error"]["code"], INVALID_PARAMS);
336 +
        assert_eq!(r[4]["error"]["code"], INVALID_PARAMS);
337 +
        assert_eq!(
338 +
            r[5],
339 +
            json!([{"jsonrpc": "2.0", "id": 5, "result": {}}]),
340 +
            "a batch is answered as a batch"
341 +
        );
342 +
    }
343 +
344 +
    #[test]
345 +
    fn string_arguments_and_lists() {
346 +
        let a = json!({"one": "x", "many": ["a", "b"], "bad": [1]});
347 +
        assert_eq!(arg_str(&a, "one"), Some("x"));
348 +
        assert_eq!(arg_strings(&a, "one").unwrap(), vec!["x"]);
349 +
        assert_eq!(arg_strings(&a, "many").unwrap(), vec!["a", "b"]);
350 +
        assert_eq!(arg_strings(&a, "missing").unwrap(), Vec::<String>::new());
351 +
        assert!(arg_strings(&a, "bad").is_err());
352 +
    }
353 +
}
modified crates/oxy-core/src/protocol.rs +26 −5
…
261 unchanged lines
262 262
    }
263 263
}
264 264
265 +
// Bundles a local remote served on this thread, for tests that count
266 +
// round trips (oxygen#158).
267 +
#[cfg(test)]
268 +
thread_local! {
269 +
    pub(crate) static LOCAL_FETCHES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
270 +
}
271 +
265 272
impl Remote for LocalRemote {
266 273
    fn repo_id(&self) -> Result<[u8; 16]> {
267 274
        self.repo.id()
…
5 unchanged lines
273 280
        self.repo.negotiate(have, want)
274 281
    }
275 282
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
283 +
        #[cfg(test)]
284 +
        LOCAL_FETCHES.with(|n| n.set(n.get() + 1));
276 285
        self.repo.bundle_raw_for(changes, max_blob)
277 286
    }
278 287
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
…
49 unchanged lines
328 337
    /// a temporary directory.
329 338
    pub cache_dir: Option<PathBuf>,
330 339
    /// Other tokens for the same hub, tried in turn when `token` is
331 -
    /// refused (401, 403 or 404): those of enclosing checkouts, when
332 -
    /// `token` came from the credential store rather than from this
333 -
    /// remote's own configuration (oxygen#112).
340 +
    /// refused (401, 403 or 404): the credential store's sign-in when
341 +
    /// `token` is the remote's own (oxygen#155), or those of enclosing
342 +
    /// checkouts when it came from the credential store (oxygen#112).
334 343
    pub fallback_tokens: Vec<String>,
344 +
    /// `token` is the remote's own: fall back only when the hub says it is
345 +
    /// not a token at all (HTTP 401: expired or unknown), never when it is
346 +
    /// one that may not do this (403, 404), so a narrowly scoped token is
347 +
    /// not quietly swapped for a wider sign-in (oxygen#155).
348 +
    pub fallback_on_401_only: bool,
335 349
}
336 350
337 351
/// Opens a store for a URL it understands, or passes (`None`). Stores that
…
65 unchanged lines
403 417
    /// The token in use, and those still to try when it is refused.
404 418
    token: Mutex<Option<String>>,
405 419
    fallbacks: Mutex<Vec<String>>,
420 +
    fallback_on_401_only: bool,
406 421
    repo_id: Option<[u8; 16]>,
407 422
    agent: ureq::Agent,
408 423
}
…
42 unchanged lines
451 466
            base: base.trim_end_matches('/').to_string(),
452 467
            token: Mutex::new(options.token.clone()),
453 468
            fallbacks: Mutex::new(options.fallback_tokens.iter().rev().cloned().collect()),
469 +
            fallback_on_401_only: options.fallback_on_401_only,
454 470
            repo_id: options.repo_id,
455 471
            agent,
456 472
        })
…
5 unchanged lines
462 478
        loop {
463 479
            let token = self.token.lock().unwrap_or_else(|p| p.into_inner()).clone();
464 480
            match self.call_once(post, endpoint, body, token.as_deref()) {
465 -
                Err(RepoError::Msg(m)) if token.is_some() && refused(&m) => {
481 +
                Err(RepoError::Msg(m))
482 +
                    if token.is_some()
483 +
                        && refused(&m)
484 +
                        && (!self.fallback_on_401_only || m.contains(": HTTP 401")) =>
485 +
                {
466 486
                    let next = self
467 487
                        .fallbacks
468 488
                        .lock()
…
3 unchanged lines
472 492
                        return Err(RepoError::Msg(m));
473 493
                    };
474 494
                    crate::credentials::warn_once(&format!(
475 -
                        "{}: the stored token was refused; using the one an enclosing checkout has for this hub",
495 +
                        "{}: the token in use was refused; trying another this computer has for the hub \
496 +
                         (a stale remote.NAME.token goes with `oxy config --unset remote.NAME.token`)",
476 497
                        self.base
477 498
                    ));
478 499
                    *self.token.lock().unwrap_or_else(|p| p.into_inner()) = Some(next);
…
1341 unchanged lines
modified crates/oxy-core/src/repo/bookmarks.rs +1 −1
…
497 unchanged lines
498 498
                .get(p)
499 499
                .and_then(|m| m.bookmark.clone());
500 500
            super::subrepos::read_pin(&self.root.join(p), hint.as_deref())
501 -
                .is_ok_and(|have| have == want)
501 +
                .is_ok_and(|have| have.same_state(&want))
502 502
        };
503 503
        touched.retain(|p| !at_target_pin(p));
504 504
        let same = |p: &String| {
…
216 unchanged lines
modified crates/oxy-core/src/repo/checkout.rs +37 −1
…
525 unchanged lines
526 526
            if i == 0 && *comp == OXY_DIR {
527 527
                return fail(format!("refusing to write inside {OXY_DIR}: {rel:?}"));
528 528
            }
529 +
            // A `.git` path could plant hooks a side-by-side Git would run;
530 +
            // Git refuses them at any depth and in any case, and so does oxy.
531 +
            if comp.eq_ignore_ascii_case(".git") {
532 +
                return fail(format!("refusing to write a .git path: {rel:?}"));
533 +
            }
529 534
            if i + 1 < comps.len() {
530 535
                full.push(comp);
531 536
                if let Ok(m) = fs::symlink_metadata(&full) {
…
64 unchanged lines
596 601
}
597 602
598 603
/// Refuse a path that could escape the workspace (spec §7.7): an empty,
599 -
/// `.` or `..` component, a backslash, or a first component `.oxy`.
604 +
/// `.` or `..` component, a backslash, a first component `.oxy`, or a
605 +
/// `.git` component anywhere (it could plant Git hooks).
600 606
fn check_path_name(rel: &str) -> Result<()> {
601 607
    for (i, comp) in rel.split('/').enumerate() {
602 608
        if comp.is_empty() || comp == "." || comp == ".." || comp.contains('\\') {
…
2 unchanged lines
605 611
        if i == 0 && comp == OXY_DIR {
606 612
            return fail(format!("refusing to write inside {OXY_DIR}: {rel:?}"));
607 613
        }
614 +
        if comp.eq_ignore_ascii_case(".git") {
615 +
            return fail(format!("refusing to write a .git path: {rel:?}"));
616 +
        }
608 617
    }
609 618
    Ok(())
610 619
}
…
37 unchanged lines
648 657
    fs::write(full, bytes)?;
649 658
    set_executable(full, exec)
650 659
}
660 +
661 +
#[cfg(test)]
662 +
mod path_name_tests {
663 +
    use super::check_path_name;
664 +
665 +
    #[test]
666 +
    fn git_paths_are_refused_at_any_depth_and_case() {
667 +
        for bad in [
668 +
            ".git/hooks/post-checkout",
669 +
            "sub/.git/config",
670 +
            ".GIT/hooks/x",
671 +
            "a/.Git",
672 +
            ".oxy/x",
673 +
            "../x",
674 +
        ] {
675 +
            assert!(check_path_name(bad).is_err(), "{bad}");
676 +
        }
677 +
        for good in [
678 +
            "src/main.rs",
679 +
            ".gitignore",
680 +
            ".github/workflows/ci.yml",
681 +
            "a/.gitmodules",
682 +
        ] {
683 +
            assert!(check_path_name(good).is_ok(), "{good}");
684 +
        }
685 +
    }
686 +
}
modified crates/oxy-core/src/repo/config.rs +6 −0
…
45 unchanged lines
46 46
    /// `git.claim`: a Git import authors the commits whose Git author email
47 47
    /// is this repository's identity's with its key (oxygen#97).
48 48
    pub git_claim: bool,
49 +
    /// `git.colocate`: keep a `.git` beside `.oxy` with the current
50 +
    /// bookmark exported after every operation, so editors' Git features
51 +
    /// work on an Oxygen checkout (one way: oxy to Git).
52 +
    pub git_colocate: bool,
49 53
    /// `core.op-retention`: days for which operations keep their states reachable (spec §12.1, §21).
50 54
    pub op_retention_days: u64,
51 55
    /// `core.gc-grace`: days an unreachable object survives, protecting concurrent writers (spec §21).
…
56 unchanged lines
108 112
            remote_cas: BTreeMap::new(),
109 113
            default_remote: None,
110 114
            git_claim: false,
115 +
            git_colocate: false,
111 116
            op_retention_days: 90,
112 117
            gc_grace_days: 14,
113 118
            max_new_file: 100 << 20,
…
102 unchanged lines
216 221
            }
217 222
            "hooks.semantic-deps" => self.semantic_deps = matches!(value, "true" | "1" | "yes"),
218 223
            "git.claim" => self.git_claim = matches!(value, "true" | "1" | "yes"),
224 +
            "git.colocate" => self.git_colocate = matches!(value, "true" | "1" | "yes"),
219 225
            "filter.paths" => {
220 226
                self.sparse_paths = value
221 227
                    .split(',')
…
163 unchanged lines
modified crates/oxy-core/src/repo/fsck.rs +1 −0
…
102 unchanged lines
103 103
        ObjectType::Receipt => crate::receipt::Receipt::decode(body).map(|_| ()),
104 104
        ObjectType::Certificate => crate::cert::Certificate::decode(body).map(|_| ()),
105 105
        ObjectType::Grant => crate::cert::Grant::decode(body).map(|_| ()),
106 +
        ObjectType::Artifact => crate::artifact::Artifact::decode(body).map(|_| ()),
106 107
        ObjectType::State => crate::objects::StateObject::decode(body).map(|_| ()),
107 108
    };
108 109
    r.map_err(|e| e.to_string())
…
408 unchanged lines
modified crates/oxy-core/src/repo/git.rs +358 −8
…
1740 unchanged lines
1741 1741
    }
1742 1742
1743 1743
    fn git_author(&self, c: &Change) -> String {
1744 -
        match self.store.get_identity(&c.author) {
1745 -
            Ok(id) if !id.name.is_empty() => format!(
1746 -
                "{} <{}>",
1747 -
                id.name.replace(['<', '>', '\n'], " "),
1748 -
                id.email.replace(['<', '>', '\n', ' '], "")
1749 -
            ),
1750 -
            _ => "oxy <oxy@localhost>".to_string(),
1751 -
        }
1744 +
        self.git_author_key(&c.author)
1752 1745
    }
1753 1746
1754 1747
    #[allow(clippy::too_many_arguments)]
…
422 unchanged lines
2177 2170
        w.len()
2178 2171
    )
2179 2172
}
2173 +
2174 +
/// What `git_export_graph` wrote.
2175 +
#[derive(Debug, Clone, Default, PartialEq, Eq)]
2176 +
pub struct GitGraphReport {
2177 +
    /// Changes written as commits this time (the rest were exported before).
2178 +
    pub commits: usize,
2179 +
    /// Branches and tags pointed at their commits.
2180 +
    pub branches: usize,
2181 +
    pub tags: usize,
2182 +
    /// Bookmarks or tags whose names Git does not take as refs.
2183 +
    pub skipped_refs: Vec<String>,
2184 +
    /// Subrepo nodes with Oxygen pins, which Git cannot represent.
2185 +
    pub skipped_subrepos: usize,
2186 +
}
2187 +
2188 +
/// A bookmark or tag name Git takes as a ref without escaping.
2189 +
fn git_ref_ok(name: &str) -> bool {
2190 +
    !name.is_empty()
2191 +
        && !name.starts_with(['-', '/', '.'])
2192 +
        && !name.ends_with(['/', '.'])
2193 +
        && !name.contains("..")
2194 +
        && !name.contains("//")
2195 +
        && !name.ends_with(".lock")
2196 +
        && name
2197 +
            .bytes()
2198 +
            .all(|b| b.is_ascii_alphanumeric() || b"-_./+".contains(&b))
2199 +
}
2200 +
2201 +
impl Repo {
2202 +
    /// Export every bookmark and tag to the bare Git repository at
2203 +
    /// `git_dir` from the change graph (spec §23, oxygenhub D1), for a hub
2204 +
    /// to serve to Git clients. Unlike `git_export`, which follows one
2205 +
    /// checkout's operation log, this depends only on the changes, so the
2206 +
    /// same changes give the same Git commits wherever it runs:
2207 +
    ///
2208 +
    /// - each change is a commit whose tree is the state of the change and
2209 +
    ///   everything it depends on, whose parents are its dependencies'
2210 +
    ///   commits, with the change's author, time and message and its
2211 +
    ///   `Change-Id:` and `Oxy-Hash:` trailers (a change that depends on
2212 +
    ///   nothing is a root, so Git sees Oxygen's dependencies, not the order
2213 +
    ///   changes were made in);
2214 +
    /// - a bookmark with one head is a branch at that head's commit; with
2215 +
    ///   several, a merge of their commits whose tree is the bookmark's
2216 +
    ///   state, at the newest head's time;
2217 +
    /// - a tag points at its heads' commit, or such a merge.
2218 +
    ///
2219 +
    /// Commits already exported are remembered (marks), so a later run
2220 +
    /// writes only new changes. Branches for bookmarks that are gone are
2221 +
    /// deleted.
2222 +
    pub fn git_export_graph(&self, git_dir: &Path) -> Result<GitGraphReport> {
2223 +
        if !git_dir.join("HEAD").exists() {
2224 +
            fs::create_dir_all(git_dir)?;
2225 +
            git(git_dir, &["init", "--bare", "-q"])?;
2226 +
        }
2227 +
        let marks_dir = self.git_marks_dir(git_dir)?;
2228 +
        let git_marks = marks_dir.join("git-marks");
2229 +
        let graph_marks = marks_dir.join("graph-marks");
2230 +
        let mut marks = Self::read_oxy_marks(&graph_marks)?;
2231 +
        let mut next = marks.values().copied().max().unwrap_or(0) + 1;
2232 +
        let mut report = GitGraphReport::default();
2233 +
        let mut stream: Vec<u8> = Vec::new();
2234 +
2235 +
        let mut refs: Vec<(String, Vec<ChangeHash>)> = Vec::new();
2236 +
        for b in self.bookmarks()? {
2237 +
            if git_ref_ok(&b.name) {
2238 +
                refs.push((format!("refs/heads/{}", b.name), b.heads));
2239 +
            } else {
2240 +
                report.skipped_refs.push(b.name);
2241 +
            }
2242 +
        }
2243 +
        let tags = self.tags()?;
2244 +
        for t in &tags {
2245 +
            if git_ref_ok(&t.name) {
2246 +
                refs.push((format!("refs/tags/{}", t.name), t.heads.clone()));
2247 +
            } else {
2248 +
                report.skipped_refs.push(t.name.clone());
2249 +
            }
2250 +
        }
2251 +
2252 +
        // Every change the refs reach, dependencies before dependents: a
2253 +
        // depth-first walk in hash order, so the order is the same everywhere.
2254 +
        let mut order: Vec<ChangeHash> = Vec::new();
2255 +
        let mut seen: HashSet<ChangeHash> = HashSet::new();
2256 +
        let mut metas: HashMap<ChangeHash, crate::state::ChangeMeta> = HashMap::new();
2257 +
        let mut roots: Vec<ChangeHash> = refs.iter().flat_map(|(_, h)| h.iter().copied()).collect();
2258 +
        roots.sort();
2259 +
        roots.dedup();
2260 +
        for root in roots {
2261 +
            if !seen.insert(root) {
2262 +
                continue;
2263 +
            }
2264 +
            let mut stack: Vec<(ChangeHash, bool)> = vec![(root, false)];
2265 +
            while let Some((h, done)) = stack.pop() {
2266 +
                if done {
2267 +
                    order.push(h);
2268 +
                    continue;
2269 +
                }
2270 +
                stack.push((h, true));
2271 +
                let meta = self.store.get_change_meta(&h)?;
2272 +
                let mut deps = meta.deps.clone();
2273 +
                deps.sort();
2274 +
                for d in deps.into_iter().rev() {
2275 +
                    if seen.insert(d) {
2276 +
                        stack.push((d, false));
2277 +
                    }
2278 +
                }
2279 +
                metas.insert(h, meta);
2280 +
            }
2281 +
        }
2282 +
2283 +
        for h in &order {
2284 +
            if marks.contains_key(h) {
2285 +
                continue;
2286 +
            }
2287 +
            let meta = &metas[h];
2288 +
            let snapshot = self.load_state(&[*h])?.materialize();
2289 +
            let mut message = meta.message.clone();
2290 +
            if !message.ends_with('\n') {
2291 +
                message.push('\n');
2292 +
            }
2293 +
            message.push_str(&format!(
2294 +
                "\nChange-Id: {}\nOxy-Hash: {}\n",
2295 +
                meta.change_id.to_hex(),
2296 +
                h.to_hex()
2297 +
            ));
2298 +
            if snapshot.is_conflicted() {
2299 +
                message.push_str("Oxy-Conflicted: true\n");
2300 +
            }
2301 +
            let mut deps = meta.deps.clone();
2302 +
            deps.sort();
2303 +
            let parents: Vec<String> = deps
2304 +
                .iter()
2305 +
                .filter_map(|d| marks.get(d))
2306 +
                .map(|m| format!(":{m}"))
2307 +
                .collect();
2308 +
            let mark = next;
2309 +
            next += 1;
2310 +
            if parents.is_empty() {
2311 +
                // Without `from`, fast-import would make the scratch ref's
2312 +
                // tip its parent; a change that depends on nothing is a root.
2313 +
                stream.extend_from_slice(b"reset refs/oxy/export\n\n");
2314 +
            }
2315 +
            self.write_commit(
2316 +
                &mut stream,
2317 +
                "refs/oxy/export",
2318 +
                mark,
2319 +
                &self.git_author_key(&meta.author),
2320 +
                meta.hlc.wall_ms / 1000,
2321 +
                &message,
2322 +
                &parents,
2323 +
                &snapshot,
2324 +
            )?;
2325 +
            marks.insert(*h, mark);
2326 +
            report.commits += 1;
2327 +
        }
2328 +
2329 +
        let mut wanted: HashSet<String> = HashSet::new();
2330 +
        for (name, heads) in &refs {
2331 +
            let mut heads = heads.clone();
2332 +
            heads.sort();
2333 +
            heads.dedup();
2334 +
            let target = match heads.as_slice() {
2335 +
                [] => continue,
2336 +
                [one] => format!(":{}", marks[one]),
2337 +
                many => {
2338 +
                    // A merge of the heads, named by them so it is made once.
2339 +
                    let key = hash_object(
2340 +
                        ObjectType::Chunk,
2341 +
                        many.iter()
2342 +
                            .map(|h| h.to_hex())
2343 +
                            .collect::<Vec<_>>()
2344 +
                            .join(" ")
2345 +
                            .as_bytes(),
2346 +
                    );
2347 +
                    if let Some(m) = marks.get(&key) {
2348 +
                        format!(":{m}")
2349 +
                    } else {
2350 +
                        let snapshot = self.load_state(many)?.materialize();
2351 +
                        let newest = many.iter().map(|h| metas[h].hlc.wall_ms).max().unwrap_or(0);
2352 +
                        let mut message = format!(
2353 +
                            "Merge of {} heads\n\nOxy-Heads: {}\n",
2354 +
                            many.len(),
2355 +
                            many.iter()
2356 +
                                .map(|h| h.to_hex())
2357 +
                                .collect::<Vec<_>>()
2358 +
                                .join(" ")
2359 +
                        );
2360 +
                        if snapshot.is_conflicted() {
2361 +
                            message.push_str("Oxy-Conflicted: true\n");
2362 +
                        }
2363 +
                        let parents: Vec<String> =
2364 +
                            many.iter().map(|h| format!(":{}", marks[h])).collect();
2365 +
                        let mark = next;
2366 +
                        next += 1;
2367 +
                        self.write_commit(
2368 +
                            &mut stream,
2369 +
                            "refs/oxy/export",
2370 +
                            mark,
2371 +
                            "oxy <oxy@localhost>",
2372 +
                            newest / 1000,
2373 +
                            &message,
2374 +
                            &parents,
2375 +
                            &snapshot,
2376 +
                        )?;
2377 +
                        marks.insert(key, mark);
2378 +
                        format!(":{mark}")
2379 +
                    }
2380 +
                }
2381 +
            };
2382 +
            stream.extend_from_slice(format!("reset {name}\nfrom {target}\n\n").as_bytes());
2383 +
            wanted.insert(name.clone());
2384 +
            if name.starts_with("refs/tags/") {
2385 +
                report.tags += 1;
2386 +
            } else {
2387 +
                report.branches += 1;
2388 +
            }
2389 +
        }
2390 +
        stream.extend_from_slice(b"done\n");
2391 +
        run_fast_import(git_dir, &stream, &git_marks)?;
2392 +
        let text: String = marks
2393 +
            .iter()
2394 +
            .map(|(h, m)| format!("{} {m}\n", h.to_hex()))
2395 +
            .collect();
2396 +
        write_atomic(&graph_marks, text.as_bytes())?;
2397 +
        let _ = git(git_dir, &["update-ref", "-d", "refs/oxy/export"]);
2398 +
        // Branches and tags of bookmarks and tags that are gone.
2399 +
        let listed = git(
2400 +
            git_dir,
2401 +
            &[
2402 +
                "for-each-ref",
2403 +
                "--format=%(refname)",
2404 +
                "refs/heads",
2405 +
                "refs/tags",
2406 +
            ],
2407 +
        )?;
2408 +
        for r in listed
2409 +
            .lines()
2410 +
            .map(str::trim)
2411 +
            .filter(|r| !r.is_empty() && !wanted.contains(*r))
2412 +
        {
2413 +
            git(git_dir, &["update-ref", "-d", r])?;
2414 +
        }
2415 +
        // HEAD names the main bookmark when there is one, so a clone checks it out.
2416 +
        let main = ["main", "master", "trunk", "develop"]
2417 +
            .into_iter()
2418 +
            .find(|b| wanted.contains(&format!("refs/heads/{b}")));
2419 +
        if let Some(b) = main {
2420 +
            git(
2421 +
                git_dir,
2422 +
                &["symbolic-ref", "HEAD", &format!("refs/heads/{b}")],
2423 +
            )?;
2424 +
        }
2425 +
        report.skipped_subrepos = self.git_export_skipped.replace(0);
2426 +
        Ok(report)
2427 +
    }
2428 +
2429 +
    /// `Name <email>` for a Git commit, from the author's identity.
2430 +
    fn git_author_key(&self, key: &KeyId) -> String {
2431 +
        match self.store.get_identity(key) {
2432 +
            Ok(id) if !id.name.is_empty() => format!(
2433 +
                "{} <{}>",
2434 +
                id.name.replace(['<', '>', '\n'], " "),
2435 +
                id.email.replace(['<', '>', '\n', ' '], "")
2436 +
            ),
2437 +
            _ => "oxy <oxy@localhost>".to_string(),
2438 +
        }
2439 +
    }
2440 +
}
2441 +
2442 +
/// Feed a fast-import stream to `git fast-import` in `git_dir`, keeping its
2443 +
/// marks in `git_marks` so a later run can refer to commits it wrote.
2444 +
fn run_fast_import(git_dir: &Path, stream: &[u8], git_marks: &Path) -> Result<()> {
2445 +
    let mut cmd = Command::new("git");
2446 +
    // `--force`: a ref may move to an unrelated commit (a bookmark rewritten).
2447 +
    cmd.arg("-C")
2448 +
        .arg(git_dir)
2449 +
        .args(["fast-import", "--quiet", "--done", "--force"])
2450 +
        .arg(format!("--export-marks={}", git_marks.display()));
2451 +
    if git_marks.exists() {
2452 +
        cmd.arg(format!("--import-marks={}", git_marks.display()));
2453 +
    }
2454 +
    let mut child = cmd
2455 +
        .stdin(Stdio::piped())
2456 +
        .stdout(Stdio::null())
2457 +
        .stderr(Stdio::piped())
2458 +
        .spawn()
2459 +
        .map_err(|e| RepoError::Msg(format!("running git fast-import: {e}")))?;
2460 +
    child.stdin.take().unwrap().write_all(stream)?;
2461 +
    let out = child.wait_with_output()?;
2462 +
    if !out.status.success() {
2463 +
        return fail(format!(
2464 +
            "git fast-import failed: {}",
2465 +
            String::from_utf8_lossy(&out.stderr).trim()
2466 +
        ));
2467 +
    }
2468 +
    Ok(())
2469 +
}
2470 +
2471 +
impl Repo {
2472 +
    /// Bring the side-by-side Git repository up to date (`git.colocate`):
2473 +
    /// a `.git` beside `.oxy`, made with `git init` when missing and told to
2474 +
    /// leave `.oxy/` alone, holding the current bookmark as exported by
2475 +
    /// `git_export_as` (one commit per state the bookmark passed through),
2476 +
    /// with HEAD on that branch and Git's index at its tree, so `git status`
2477 +
    /// shows only the working copy's uncommitted work. Files on disk are
2478 +
    /// never touched. Does nothing when the bookmark has not moved since the
2479 +
    /// last time; `Ok(true)` when it exported.
2480 +
    pub fn git_colocate_sync(&self) -> Result<bool> {
2481 +
        if self.bare {
2482 +
            return fail("a bare repository has no working copy for a .git beside it");
2483 +
        }
2484 +
        let git_dir = self.root.join(".git");
2485 +
        if git_dir.is_file() {
2486 +
            return fail(".git is a file (a Git worktree or submodule link); remove it to keep a side-by-side Git repository here");
2487 +
        }
2488 +
        if !git_dir.join("HEAD").exists() {
2489 +
            git(&self.root, &["init", "-q"])?;
2490 +
        }
2491 +
        let exclude = git_dir.join("info").join("exclude");
2492 +
        let have = fs::read_to_string(&exclude).unwrap_or_default();
2493 +
        if !have.lines().any(|l| l.trim() == "/.oxy/") {
2494 +
            fs::create_dir_all(exclude.parent().expect("info/"))?;
2495 +
            let sep = if have.is_empty() || have.ends_with('\n') {
2496 +
                ""
2497 +
            } else {
2498 +
                "\n"
2499 +
            };
2500 +
            fs::write(
2501 +
                &exclude,
2502 +
                format!("{have}{sep}# Oxygen's own repository (oxy git colocate)\n/.oxy/\n"),
2503 +
            )?;
2504 +
        }
2505 +
        let bookmark = self.config.bookmark.clone();
2506 +
        let state = self
2507 +
            .view()?
2508 +
            .bookmarks
2509 +
            .get(&bookmark)
2510 +
            .map(|s| s.to_hex())
2511 +
            .unwrap_or_default();
2512 +
        let stamp_file = self.oxy.join("git").join("colocate-stamp");
2513 +
        let stamp = format!("{bookmark} {state}");
2514 +
        if fs::read_to_string(&stamp_file).is_ok_and(|s| s.trim() == stamp) {
2515 +
            return Ok(false);
2516 +
        }
2517 +
        if !state.is_empty() {
2518 +
            self.git_export_as(&git_dir, &bookmark)?;
2519 +
            git(
2520 +
                &self.root,
2521 +
                &["symbolic-ref", "HEAD", &format!("refs/heads/{bookmark}")],
2522 +
            )?;
2523 +
            git(&self.root, &["read-tree", "HEAD"])?;
2524 +
        }
2525 +
        fs::create_dir_all(stamp_file.parent().expect("git/"))?;
2526 +
        fs::write(&stamp_file, stamp)?;
2527 +
        Ok(!state.is_empty())
2528 +
    }
2529 +
}
modified crates/oxy-core/src/repo/identity.rs +100 −0
…
17 unchanged lines
18 18
    Invalid(String),
19 19
}
20 20
21 +
/// Make a user identity with no repository (oxygen#144), so a first
22 +
/// identity comes before any repository: its secret and a copy of it go to
23 +
/// this home's keys, and the user file names it as the author, as `oxy
24 +
/// identity create` does inside one. A repository reads it from the keys
25 +
/// when it first signs with it. `name` and `email` default to the user
26 +
/// file's author, then to `$USER` and `OXY_AUTHOR_*`.
27 +
pub fn create_user_identity(
28 +
    name: Option<&str>,
29 +
    email: Option<&str>,
30 +
    expires: Option<u64>,
31 +
) -> Result<(KeyId, Identity)> {
32 +
    let user = default_user_config();
33 +
    let defaults = Config::load(Some(&user), Path::new(""))?;
34 +
    let name = name.map(str::to_string).unwrap_or(defaults.author_name);
35 +
    let email = email.map(str::to_string).unwrap_or(defaults.author_email);
36 +
    let sk = SecretKey::generate();
37 +
    let mut id = sk.identity(&name, &email, Hlc::now().wall_ms);
38 +
    id.expires = expires;
39 +
    let key = id.hash();
40 +
    let keys = default_keys_dir();
41 +
    fs::create_dir_all(&keys)?;
42 +
    crate::durable::write_atomic_private(
43 +
        &keys.join(format!("{}.secret", key.to_hex())),
44 +
        &sk.encode(&key),
45 +
    )?;
46 +
    write_atomic(
47 +
        &keys.join(format!("{}.identity", key.to_hex())),
48 +
        &id.encode(),
49 +
    )?;
50 +
    Config::set_user(&user, "author.name", &id.name)?;
51 +
    Config::set_user(&user, "author.email", &id.email)?;
52 +
    Config::set_user(&user, "author.key", &key.to_hex())?;
53 +
    Ok((key, id))
54 +
}
55 +
21 56
/// The outcome of replacing the configured identity (spec §14.2).
22 57
#[derive(Clone, Debug)]
23 58
pub struct Rotation {
…
113 unchanged lines
137 172
        if new.previous != Some(old) {
138 173
            return fail("the successor identity must name the current key as previous");
139 174
        }
175 +
        // A key that already has a successor is not the identity's newest:
176 +
        // building another from it forks the identity into two branches
177 +
        // (oxygen#163), as a checkout whose stale `author.key` pin outlived
178 +
        // a rotation made elsewhere once did.
179 +
        if let Some(newer) = self.successors(&old)?.into_iter().next() {
180 +
            let newest = self.newest(&newer)?;
181 +
            let fix = if self.config.is_repo_set("author.key") {
182 +
                format!(
183 +
                    "This repository's config pins author.key to the older key: `oxy config author.key {}` here \
184 +
                     (or `oxy config --unset author.key` to follow the user config), then run it again.",
185 +
                    newest.to_hex()
186 +
                )
187 +
            } else {
188 +
                format!(
189 +
                    "The user config names the older key: `oxy config --user author.key {}`, then run it again.",
190 +
                    newest.to_hex()
191 +
                )
192 +
            };
193 +
            return fail(format!(
194 +
                "`{command}` would start from key {}, which is not your identity's newest ({} succeeds it, and {} is the newest). \
195 +
                 Another successor would fork the identity into two branches.\n{fix}",
196 +
                old.short(12),
197 +
                newer.short(12),
198 +
                newest.short(12),
199 +
            ));
200 +
        }
140 201
        let new_key = self.install_key(&new, &sk)?;
141 202
        let attest = Attest {
142 203
            repo: self.id()?,
…
20 unchanged lines
163 224
        })
164 225
    }
165 226
227 +
    /// Keys in this home whose identity names `key` as `previous`: the
228 +
    /// successors already made from it. More than one means a fork.
229 +
    pub fn successors(&self, key: &KeyId) -> Result<Vec<KeyId>> {
230 +
        let mut found = Vec::new();
231 +
        let entries = match fs::read_dir(&self.keys_dir) {
232 +
            Ok(e) => e,
233 +
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(found),
234 +
            Err(e) => return Err(e.into()),
235 +
        };
236 +
        for entry in entries {
237 +
            let path = entry?.path();
238 +
            if path.extension().and_then(|e| e.to_str()) != Some("identity") {
239 +
                continue;
240 +
            }
241 +
            let Ok(bytes) = fs::read(&path) else { continue };
242 +
            let Ok(id) = Identity::decode(&bytes) else {
243 +
                continue;
244 +
            };
245 +
            if id.previous == Some(*key) {
246 +
                found.push(id.hash());
247 +
            }
248 +
        }
249 +
        found.sort();
250 +
        Ok(found)
251 +
    }
252 +
253 +
    /// The newest key of the line through `key` in this home: its
254 +
    /// successors followed to the end (the first, where there is a fork).
255 +
    pub fn newest(&self, key: &KeyId) -> Result<KeyId> {
256 +
        let mut cur = *key;
257 +
        for _ in 0..256 {
258 +
            match self.successors(&cur)?.into_iter().next() {
259 +
                Some(next) => cur = next,
260 +
                None => break,
261 +
            }
262 +
        }
263 +
        Ok(cur)
264 +
    }
265 +
166 266
    /// Rotate to a fresh key pair; the new identity names the old one as
167 267
    /// `previous` and keeps its name, email, delegates, and expiry.
168 268
    pub fn rotate_identity(&mut self) -> Result<Rotation> {
…
611 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +37 −10
…
142 unchanged lines
143 143
pub use git::{GitExportReport, GitImportReport, GitVerifyReport};
144 144
pub use hooks::{json_array, json_str, HookOutcome};
145 145
pub use hosted::FileEdit;
146 -
pub use identity::{Rotation, SignatureStatus};
146 +
pub use identity::{create_user_identity, Rotation, SignatureStatus};
147 147
pub use inspect::{BisectStep, BlameLine, Description};
148 148
pub use keyless::home_key;
149 149
pub use ops::{RestoreReport, RevertReport, Undone, ViewChange};
…
550 unchanged lines
700 700
    pub fn root(&self) -> &Path {
701 701
        &self.root
702 702
    }
703 +
    /// The repository's `.oxy` directory (shared by its workspaces), for
704 +
    /// what other crates keep beside the store (Cylinder's artifacts).
705 +
    pub fn oxy_dir(&self) -> &Path {
706 +
        &self.oxy
707 +
    }
703 708
    pub fn store(&self) -> &Store {
704 709
        &self.store
705 710
    }
…
1516 unchanged lines
2222 2227
        // already tracked as subrepo nodes, they contribute their pin;
2223 2228
        // unregistered ones are left alone.
2224 2229
        let modules = self.modules_on_disk();
2225 -
        let removed = self.removed_subrepos();
2226 -
        for rel in &removed {
2227 -
            if tracked.get(rel).map(|(k, _)| *k) != Some(NodeKind::Subrepo) {
2230 +
        // A subrepo at the state its recorded pin names is unmodified,
2231 +
        // whatever bookmark it sits on (#147): the recorded pin stands.
2232 +
        let subrepo_entry = |rel: &str, pin: crate::subrepo::Pin| -> FsEntry {
2233 +
            if let Some((NodeKind::Subrepo, Some(b))) = tracked.get(rel) {
2234 +
                let recorded = self
2235 +
                    .read_blob(b)
2236 +
                    .ok()
2237 +
                    .and_then(|x| crate::subrepo::Pin::decode(&x).ok());
2238 +
                if recorded.is_some_and(|r| r.same_state(&pin)) {
2239 +
                    return FsEntry {
2240 +
                        blob: Some(*b),
2241 +
                        ..FsEntry::new(NodeKind::Subrepo, false, vec![])
2242 +
                    };
2243 +
                }
2244 +
            }
2245 +
            FsEntry::new(NodeKind::Subrepo, false, pin.encode())
2246 +
        };
2247 +
        // A removal's marker is spent once the removal is recorded (no
2248 +
        // subrepo node is tracked there) or the path is registered again:
2249 +
        // dropped before it is read, or the change registering it again
2250 +
        // would leave it out (oxygen#152).
2251 +
        let mut removed = self.removed_subrepos();
2252 +
        removed.retain(|rel| {
2253 +
            let spent = tracked.get(rel).map(|(k, _)| *k) != Some(NodeKind::Subrepo)
2254 +
                || modules.entries.contains_key(rel);
2255 +
            if spent {
2228 2256
                self.clear_removed_subrepo(rel);
2229 2257
            }
2230 -
        }
2258 +
            !spent
2259 +
        });
2231 2260
        let mut untracked_nested = Vec::new();
2232 2261
        for dir in nested {
2233 2262
            let rel = self.rel_path(&dir);
…
7 unchanged lines
2241 2270
                continue;
2242 2271
            }
2243 2272
            let pin = subrepos::read_pin(&dir, registered.and_then(|m| m.bookmark.as_deref()))?;
2244 -
            out.insert(rel, FsEntry::new(NodeKind::Subrepo, false, pin.encode()));
2273 +
            let entry = subrepo_entry(&rel, pin);
2274 +
            out.insert(rel, entry);
2245 2275
        }
2246 2276
        untracked_nested.sort();
2247 2277
        *self.untracked_nested.borrow_mut() = untracked_nested;
…
14 unchanged lines
2262 2292
                        &path,
2263 2293
                        modules.entries.get(rel).and_then(|m| m.bookmark.as_deref()),
2264 2294
                    )?;
2265 -
                    out.insert(
2266 -
                        rel.clone(),
2267 -
                        FsEntry::new(NodeKind::Subrepo, false, pin.encode()),
2268 -
                    );
2295 +
                    out.insert(rel.clone(), subrepo_entry(rel, pin));
2269 2296
                } else if let Some(b) = blob {
2270 2297
                    // Not cloned (or emptied): the pin stays as recorded, as a
2271 2298
                    // Git gitlink does when its directory is absent.
…
650 unchanged lines
modified crates/oxy-core/src/repo/subrepos.rs +1 −10
…
181 unchanged lines
182 182
            }
183 183
        }
184 184
        let actual = read_pin(&dir, module.as_ref().and_then(|m| m.bookmark.as_deref()))?;
185 -
        let same = match (pin, &actual) {
186 -
            (Pin::Oxy { heads: a, .. }, Pin::Oxy { heads: b, .. }) => {
187 -
                let (mut a, mut b) = (a.clone(), b.clone());
188 -
                a.sort();
189 -
                b.sort();
190 -
                a == b
191 -
            }
192 -
            (Pin::Git { sha: a, .. }, Pin::Git { sha: b, .. }) => a == b,
193 -
            _ => false,
194 -
        };
185 +
        let same = pin.same_state(&actual);
195 186
        if same {
196 187
            return Ok(if cloned {
197 188
                SubrepoSync::Updated
…
242 unchanged lines
modified crates/oxy-core/src/repo/sync.rs +121 −24
…
1474 unchanged lines
1475 1475
        if options.token.is_some() && options.ca.is_some() {
1476 1476
            return;
1477 1477
        }
1478 -
        let explicit =
1479 -
            options.token.is_some() || std::env::var("OXY_TOKEN").is_ok_and(|t| !t.is_empty());
1478 +
        let own = options.token.is_some();
1479 +
        let env = std::env::var("OXY_TOKEN").is_ok_and(|t| !t.is_empty());
1480 +
        let explicit = own || env;
1480 1481
        let stored = self.credentials().options_for(url);
1482 +
        // A checkout's own token refused as expired or unknown (401) falls
1483 +
        // back to this computer's sign-in for the hub, never to another
1484 +
        // checkout's, and never when it is refused for permission (#155).
1485 +
        if own && !env {
1486 +
            if let Some(t) = stored
1487 +
                .token
1488 +
                .clone()
1489 +
                .filter(|t| options.token.as_ref() != Some(t))
1490 +
            {
1491 +
                options.fallback_tokens.push(t);
1492 +
                options.fallback_on_401_only = true;
1493 +
            }
1494 +
        }
1481 1495
        options.token = options.token.take().or(stored.token);
1482 1496
        options.ca = options.ca.take().or(stored.ca);
1483 1497
        let enclosing = self.enclosing_credentials(url);
…
12 unchanged lines
1496 1510
        }
1497 1511
    }
1498 1512
1513 +
    /// What to do when `remote`'s own token was refused (`message` says
1514 +
    /// HTTP 401) and so was this computer's sign-in for its hub, which it
1515 +
    /// falls back to (oxygen#155).
1516 +
    pub fn stale_token_note(&self, remote: &str, message: &str) -> Option<String> {
1517 +
        if !message.contains("HTTP 401") {
1518 +
            return None;
1519 +
        }
1520 +
        let own = self.config.remote_tokens.get(remote)?;
1521 +
        let url = self.config.remotes.get(remote)?;
1522 +
        let stored = self.credentials().options_for(url).token?;
1523 +
        (stored != *own).then(|| {
1524 +
            format!(
1525 +
                "the hub refused both this checkout's own token for {remote} (remote.{remote}.token) and your sign-in: \
1526 +
                 `oxy auth login HUB`, then `oxy config --unset remote.{remote}.token` so the sign-in is used"
1527 +
            )
1528 +
        })
1529 +
    }
1530 +
1499 1531
    /// The tokens and CAs of remotes on `url`'s hub in enclosing
1500 1532
    /// checkouts, nearest first.
1501 1533
    fn enclosing_credentials(&self, url: &str) -> Vec<(Option<String>, Option<PathBuf>)> {
…
200 unchanged lines
1702 1734
    pub fn fetch(&self, remote_name: &str) -> Result<FetchReport> {
1703 1735
        let remote = self.open_named_remote(remote_name)?;
1704 1736
        self.check_remote_id(remote.as_ref(), remote_name, false)?;
1705 -
        let resp = remote.refs()?;
1737 +
        let resp = {
1738 +
            let _t = crate::timing::phase("fetch: refs");
1739 +
            remote.refs()?
1740 +
        };
1706 1741
        // The host's transparency log (spec §14.8): a fork fails here.
1742 +
        let log_phase = crate::timing::phase("fetch: transparency log");
1707 1743
        let log = self.check_log(
1708 1744
            &self.remote_url(remote_name)?,
1709 1745
            self.config.remote_cas.get(remote_name).map(Path::new),
1710 1746
            &resp,
1711 1747
        )?;
1748 +
        drop(log_phase);
1712 1749
        let mut log_notes = log.notes.clone();
1713 1750
        // Trust policy (spec §14.5). Adopt the remote's policy when it is newer
1714 1751
        // and signed by roots we trust, or on first contact. A remote that
…
47 unchanged lines
1762 1799
        let mut report = Vec::new();
1763 1800
        let mut skipped = Vec::new();
1764 1801
        let max_blob = self.config.max_blob;
1765 -
        for r in resp.refs {
1802 +
        // One negotiation and one bundle for every bookmark's heads at
1803 +
        // once (oxygen#158): a bookmark at a time cost a round trip each and
1804 +
        // sent again the changes bookmarks share, as `have` did not grow
1805 +
        // between them (160 bookmarks: 160 bundles, 49k changes encoded for
1806 +
        // a few hundred). Heads already here, with every attestation's
1807 +
        // signer known, need no round trip at all.
1808 +
        let mut wanted: Vec<ChangeHash> = resp
1809 +
            .refs
1810 +
            .iter()
1811 +
            .flat_map(|r| r.heads.iter().copied())
1812 +
            .filter(|h| !self.store.has(h))
1813 +
            .collect();
1814 +
        wanted.sort();
1815 +
        wanted.dedup();
1816 +
        // A bundle also carries the identities and certificates of the keys
1817 +
        // that signed the remote's attestations (spec §16.2): with no change
1818 +
        // wanted, ask anyway when one of those signers is unknown here.
1819 +
        let unknown_signer = resp
1820 +
            .refs
1821 +
            .iter()
1822 +
            .filter_map(|r| r.attestation.as_ref())
1823 +
            .chain(resp.tags.iter().filter_map(|t| t.attestation.as_ref()))
1824 +
            .flat_map(|a| a.sigs.iter().map(|s| s.key))
1825 +
            .any(|k| {
1826 +
                self.store.get_identity(&k).is_err()
1827 +
                    && !matches!(self.store.certificate_of(&k), Ok(Some(_)))
1828 +
            });
1829 +
        let shallow = self.is_shallow();
1830 +
        let imported: Vec<ChangeHash> = if wanted.is_empty() && !unknown_signer {
1831 +
            Vec::new()
1832 +
        } else {
1766 1833
            let needed = {
1767 1834
                let _t = crate::timing::phase("fetch: negotiate");
1768 -
                remote.negotiate(&have, &r.heads)?
1835 +
                remote.negotiate(&have, &wanted)?
1769 1836
            };
1770 1837
            let bundle = {
1771 1838
                let _t = crate::timing::phase("fetch: bundle");
1772 1839
                remote.fetch(&needed, max_blob)?
1773 1840
            };
1774 -
            // Changes are not individually re-verified here: the bookmark's
1841 +
            // Changes are not individually re-verified here: each bookmark's
1775 1842
            // attestation (checked below) binds its whole state, and the
1776 1843
            // receiver of the push verified the added changes at that time.
1777 1844
            // A shallow repository takes changes whose dependencies it never
1778 1845
            // held (below what it cloned), as its clone did (#137).
1779 -
            let shallow = self.is_shallow();
1780 -
            let imported = {
1781 -
                let _t = crate::timing::phase("fetch: import");
1782 -
                if shallow {
1783 -
                    self.import_bundle_shallow(&bundle, false)?
1784 -
                } else {
1785 -
                    self.import_bundle(&bundle, false)?
1786 -
                }
1787 -
            };
1846 +
            let _t = crate::timing::phase("fetch: import");
1788 1847
            if shallow {
1789 -
                self.refresh_boundary(&imported)?;
1848 +
                self.import_bundle_shallow(&bundle, false)?
1849 +
            } else {
1850 +
                self.import_bundle(&bundle, false)?
1851 +
            }
1852 +
        };
1853 +
        if shallow {
1854 +
            self.refresh_boundary(&imported)?;
1855 +
        }
1856 +
        if max_blob.is_some() {
1857 +
            self.register_missing_blobs(&imported, remote_name)?;
1858 +
        }
1859 +
        let boundary = if shallow {
1860 +
            self.boundary()
1861 +
        } else {
1862 +
            BTreeSet::new()
1863 +
        };
1864 +
        // Each bookmark's new changes: those just imported that its heads
1865 +
        // reach, for the report.
1866 +
        let mut new_deps: HashMap<ChangeHash, Vec<ChangeHash>> = HashMap::new();
1867 +
        for h in &imported {
1868 +
            if let Ok(m) = self.store.get_change_meta(h) {
1869 +
                new_deps.insert(*h, m.deps);
1790 1870
            }
1791 -
            if max_blob.is_some() {
1792 -
                self.register_missing_blobs(&imported, remote_name)?;
1871 +
        }
1872 +
        let new_in = |heads: &[ChangeHash]| -> usize {
1873 +
            let mut seen: HashSet<ChangeHash> = HashSet::new();
1874 +
            let mut stack: Vec<ChangeHash> = heads
1875 +
                .iter()
1876 +
                .filter(|h| new_deps.contains_key(*h))
1877 +
                .copied()
1878 +
                .collect();
1879 +
            while let Some(h) = stack.pop() {
1880 +
                if seen.insert(h) {
1881 +
                    stack.extend(
1882 +
                        new_deps[&h]
1883 +
                            .iter()
1884 +
                            .filter(|d| new_deps.contains_key(*d) && !seen.contains(*d)),
1885 +
                    );
1886 +
                }
1793 1887
            }
1794 -
            let boundary = if shallow {
1795 -
                self.boundary()
1796 -
            } else {
1797 -
                BTreeSet::new()
1798 -
            };
1888 +
            seen.len()
1889 +
        };
1890 +
        let checks = crate::timing::phase("fetch: check each bookmark");
1891 +
        for r in resp.refs {
1799 1892
            for h in &r.heads {
1800 1893
                if !self.store.has(h) && !boundary.contains(h) {
1801 1894
                    return fail(format!("remote did not send head {}", h.short(12)));
…
74 unchanged lines
1876 1969
                .entry(remote_name.to_string())
1877 1970
                .or_default()
1878 1971
                .insert(r.name.clone(), state);
1879 -
            report.push((r.name, r.heads, imported.len()));
1972 +
            let n = new_in(&r.heads);
1973 +
            report.push((r.name, r.heads, n));
1880 1974
        }
1975 +
        drop(checks);
1976 +
        let tags_phase = crate::timing::phase("fetch: tags");
1881 1977
        let mut new_tags = 0;
1882 1978
        for t in &resp.tags {
1883 1979
            if view.tags.contains_key(&t.name) {
…
34 unchanged lines
1918 2014
        // The remote withdrew a tag this repository has: so does this one,
1919 2015
        // once the withdrawal is checked as a push's would be; one that is
1920 2016
        // not is left as it is, said so (oxygenhub#301).
2017 +
        drop(tags_phase);
1921 2018
        let mut withdrawn_tags = Vec::new();
1922 2019
        for t in &resp.withdrawn {
1923 2020
            if !view.tags.contains_key(&t.name) {
…
1151 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +361 −0
…
5371 unchanged lines
5372 5372
        !clone.snapshot("status").unwrap().changed,
5373 5373
        "an unregistered nested repository is ignored"
5374 5374
    );
5375 +
5376 +
    // Registered again, it is pinned again; removed and committed, then
5377 +
    // registered again with no snapshot between, the change that adds it
5378 +
    // back pins it too: the removal's marker is spent (oxygen#152).
5379 +
    let modules = format!("[libs/lib]\nurl = {lib_url}\n");
5380 +
    let pinned = |c: ChangeHash| {
5381 +
        clone
5382 +
            .load_state(&[c])
5383 +
            .unwrap()
5384 +
            .materialize()
5385 +
            .files
5386 +
            .get("libs/lib")
5387 +
            .map(|n| n.kind)
5388 +
    };
5389 +
    write(&clone_dir, ".oxymodules", modules.as_bytes());
5390 +
    let c = clone.commit("lib again", &[]).unwrap();
5391 +
    assert_eq!(pinned(c), Some(NodeKind::Subrepo));
5392 +
    clone.submodule_remove("libs/lib").unwrap();
5393 +
    let c = clone.commit("drop lib again", &[]).unwrap();
5394 +
    assert_eq!(pinned(c), None);
5395 +
    write(&clone_dir, ".oxymodules", modules.as_bytes());
5396 +
    let c = clone.commit("lib back", &[]).unwrap();
5397 +
    assert_eq!(
5398 +
        pinned(c),
5399 +
        Some(NodeKind::Subrepo),
5400 +
        "pinned in the change that registers it"
5401 +
    );
5402 +
    assert!(!clone.snapshot("status").unwrap().changed);
5375 5403
}
5376 5404
5377 5405
#[test]
…
5603 unchanged lines
10981 11009
    assert!(format!("{:?}", e.unwrap_err()).contains("does not exist"));
10982 11010
}
10983 11011
11012 +
/// oxygen#147: a subrepo at its pinned state is unmodified whatever
11013 +
/// bookmark it sits on, so the parent's status is clean and a bookmark
11014 +
/// switch goes ahead; a new state in it is still a change.
11015 +
#[test]
11016 +
fn a_subrepo_at_its_pinned_state_on_another_bookmark_is_unmodified() {
11017 +
    hermetic();
11018 +
    let dir = tempfile::tempdir().unwrap();
11019 +
    let root = dir.path();
11020 +
    let _lib_origin = Repo::init(&root.join("lib-origin")).unwrap();
11021 +
    let lib_url = root.join("lib-origin").to_string_lossy().to_string();
11022 +
    let lib = Repo::clone(&lib_url, &root.join("lib"))
11023 +
        .unwrap()
11024 +
        .with_home(root.join("home"));
11025 +
    write(&root.join("lib"), "lib.txt", b"v1\n");
11026 +
    lib.commit("lib v1", &[]).unwrap();
11027 +
    assert!(lib.push("origin", false).unwrap()[0].ok);
11028 +
    let _app_origin = Repo::init(&root.join("app-origin")).unwrap();
11029 +
    let app_dir = root.join("app");
11030 +
    let mut app = Repo::clone(&root.join("app-origin").to_string_lossy(), &app_dir)
11031 +
        .unwrap()
11032 +
        .with_home(root.join("home"));
11033 +
    write(&app_dir, "main.txt", b"app\n");
11034 +
    app.submodule_add(&lib_url, "libs/lib", None).unwrap();
11035 +
    let a1 = app.commit("app with lib", &[]).unwrap();
11036 +
11037 +
    // The nested repository moves to another bookmark at the same heads.
11038 +
    let mut nested = Repo::open(&app_dir.join("libs/lib"))
11039 +
        .unwrap()
11040 +
        .with_home(root.join("home"));
11041 +
    let heads = nested.bookmark_heads().unwrap();
11042 +
    nested.bookmark_create("feature", Some(&heads)).unwrap();
11043 +
    nested.bookmark_switch("feature", false).unwrap();
11044 +
    let snap = app.snapshot("status").unwrap();
11045 +
    assert!(
11046 +
        !snap.changed,
11047 +
        "only the bookmark differs: {:?}",
11048 +
        snap.statuses
11049 +
    );
11050 +
    app.bookmark_create("other", Some(&[a1])).unwrap();
11051 +
    app.bookmark_switch("other", false).unwrap();
11052 +
11053 +
    // A new state in it is a change, as before.
11054 +
    let nested = Repo::open(&app_dir.join("libs/lib"))
11055 +
        .unwrap()
11056 +
        .with_home(root.join("home"));
11057 +
    write(&app_dir.join("libs/lib"), "lib.txt", b"v2\n");
11058 +
    nested.commit("lib v2", &[]).unwrap();
11059 +
    let snap = app.snapshot("status").unwrap();
11060 +
    assert_eq!(
11061 +
        snap.statuses
11062 +
            .iter()
11063 +
            .map(|s| (s.path.as_str(), s.status))
11064 +
            .collect::<Vec<_>>(),
11065 +
        vec![("libs/lib", ChangeStatus::Modified)]
11066 +
    );
11067 +
}
11068 +
10984 11069
/// oxygenhub#249: a handle reads the operation head once and keeps it,
10985 11070
/// so one command or request never mixes two heads; another handle's
10986 11071
/// operation shows only after `refresh`. Its own operations move it, and
…
201 unchanged lines
11188 11273
        "a repository's pin wins"
11189 11274
    );
11190 11275
}
11276 +
11277 +
#[test]
11278 +
fn git_export_graph_is_deterministic_and_merges_heads() {
11279 +
    hermetic();
11280 +
    if std::process::Command::new("git")
11281 +
        .arg("--version")
11282 +
        .output()
11283 +
        .is_err()
11284 +
    {
11285 +
        eprintln!("git not installed; skipping");
11286 +
        return;
11287 +
    }
11288 +
    let dir = tempfile::tempdir().unwrap();
11289 +
    let root = dir.path();
11290 +
    let mut repo = Repo::init(&root.join("r"))
11291 +
        .unwrap()
11292 +
        .with_home(root.join("home"));
11293 +
    repo.create_identity("Ada Lovelace", "[email protected]", IdentityScope::User)
11294 +
        .unwrap();
11295 +
    let r = root.join("r");
11296 +
    write(&r, "README.md", b"hello\n");
11297 +
    let h1 = repo.commit("first", &[]).unwrap();
11298 +
    write(&r, "README.md", b"hello\nworld\n");
11299 +
    write(&r, "src/main.rs", b"fn main() {}\n");
11300 +
    let h2 = repo.commit("second", &[]).unwrap();
11301 +
    repo.tag_create("v1", &[h2], "the first release").unwrap();
11302 +
    // An independent change: main gets two heads.
11303 +
    repo.checkout(&[h1], false, "checkout").unwrap();
11304 +
    write(&r, "other.txt", b"o\n");
11305 +
    let h3 = repo.commit("third", &[]).unwrap();
11306 +
    repo.bookmark_create("gone", Some(&[h1])).unwrap();
11307 +
11308 +
    let git = |git_dir: &Path, args: &[&str]| -> String {
11309 +
        let out = std::process::Command::new("git")
11310 +
            .arg("-C")
11311 +
            .arg(git_dir)
11312 +
            .args(args)
11313 +
            .output()
11314 +
            .unwrap();
11315 +
        assert!(
11316 +
            out.status.success(),
11317 +
            "git {args:?}: {}",
11318 +
            String::from_utf8_lossy(&out.stderr)
11319 +
        );
11320 +
        String::from_utf8_lossy(&out.stdout).trim().to_string()
11321 +
    };
11322 +
    let a = root.join("a.git");
11323 +
    let rep = repo.git_export_graph(&a).unwrap();
11324 +
    assert_eq!((rep.commits, rep.tags), (3, 1), "{rep:?}");
11325 +
    assert_eq!(rep.branches, 2, "main and gone: {rep:?}");
11326 +
11327 +
    // Each change is a commit whose parents are exactly its dependencies'
11328 +
    // commits (Oxygen's dependencies, not the order changes were made in).
11329 +
    let by_hash = |hash: ChangeHash| {
11330 +
        git(
11331 +
            &a,
11332 +
            &[
11333 +
                "log",
11334 +
                "--all",
11335 +
                "--format=%H",
11336 +
                &format!("--grep=Oxy-Hash: {}", hash.to_hex()),
11337 +
            ],
11338 +
        )
11339 +
    };
11340 +
    let (c1, c2, c3) = (by_hash(h1), by_hash(h2), by_hash(h3));
11341 +
    for (h, c) in [(h1, &c1), (h2, &c2), (h3, &c3)] {
11342 +
        let mut want: Vec<String> = repo
11343 +
            .store()
11344 +
            .get_change_meta(&h)
11345 +
            .unwrap()
11346 +
            .deps
11347 +
            .iter()
11348 +
            .map(|d| by_hash(*d))
11349 +
            .collect();
11350 +
        want.sort();
11351 +
        let mut have: Vec<String> = git(&a, &["rev-list", "--parents", "-1", c])
11352 +
            .split(' ')
11353 +
            .skip(1)
11354 +
            .map(String::from)
11355 +
            .collect();
11356 +
        have.sort();
11357 +
        assert_eq!(have, want, "{h:?}'s commit's parents");
11358 +
    }
11359 +
    assert!(
11360 +
        !repo.store().get_change_meta(&h2).unwrap().deps.is_empty(),
11361 +
        "second edits first's README"
11362 +
    );
11363 +
    assert_eq!(
11364 +
        git(&a, &["show", &format!("{c2}:src/main.rs")]),
11365 +
        "fn main() {}"
11366 +
    );
11367 +
    assert!(
11368 +
        git(&a, &["ls-tree", "--name-only", &c3])
11369 +
            .lines()
11370 +
            .all(|l| l != "src"),
11371 +
        "a change's tree is its own closure, not the bookmark's"
11372 +
    );
11373 +
    assert_eq!(
11374 +
        git(&a, &["log", "-1", "--format=%an <%ae>", &c1]),
11375 +
        "Ada Lovelace <[email protected]>"
11376 +
    );
11377 +
11378 +
    // main has two heads: a merge of both, whose tree is the union.
11379 +
    let main = git(&a, &["rev-parse", "main"]);
11380 +
    let mut parents: Vec<String> = git(&a, &["rev-list", "--parents", "-1", "main"])
11381 +
        .split(' ')
11382 +
        .skip(1)
11383 +
        .map(String::from)
11384 +
        .collect();
11385 +
    parents.sort();
11386 +
    let mut want = vec![c2.clone(), c3.clone()];
11387 +
    want.sort();
11388 +
    assert_eq!(parents, want, "the merge's parents are the heads");
11389 +
    assert_eq!(
11390 +
        git(&a, &["ls-tree", "--name-only", "main"]),
11391 +
        "README.md\nother.txt\nsrc"
11392 +
    );
11393 +
    assert!(git(&a, &["log", "-1", "--format=%B", "main"]).contains("Merge of 2 heads"));
11394 +
    assert_eq!(
11395 +
        git(&a, &["rev-parse", "v1^{commit}"]),
11396 +
        c2,
11397 +
        "the tag is its head's commit"
11398 +
    );
11399 +
    assert_eq!(git(&a, &["symbolic-ref", "HEAD"]), "refs/heads/main");
11400 +
11401 +
    // Again: nothing new. A bookmark deleted takes its branch with it.
11402 +
    repo.bookmark_delete("gone").unwrap();
11403 +
    let rep = repo.git_export_graph(&a).unwrap();
11404 +
    assert_eq!(rep.commits, 0, "{rep:?}");
11405 +
    assert!(
11406 +
        git(&a, &["for-each-ref", "--format=%(refname)", "refs/heads"])
11407 +
            .lines()
11408 +
            .all(|r| r != "refs/heads/gone")
11409 +
    );
11410 +
    assert_eq!(
11411 +
        git(&a, &["rev-parse", "main"]),
11412 +
        main,
11413 +
        "the merge is made once"
11414 +
    );
11415 +
11416 +
    // The same changes elsewhere give the same commits.
11417 +
    let other = Repo::clone(r.to_str().unwrap(), &root.join("clone"))
11418 +
        .unwrap()
11419 +
        .with_home(root.join("home"));
11420 +
    let b = root.join("b.git");
11421 +
    other.git_export_graph(&b).unwrap();
11422 +
    assert_eq!(
11423 +
        git(&b, &["rev-parse", "main"]),
11424 +
        main,
11425 +
        "deterministic across repositories"
11426 +
    );
11427 +
    assert_eq!(git(&b, &["rev-parse", "v1^{commit}"]), c2);
11428 +
}
11429 +
11430 +
#[test]
11431 +
fn a_fetch_of_many_bookmarks_takes_shared_changes_once() {
11432 +
    // oxygen#158: every bookmark's heads are fetched in one exchange, so
11433 +
    // changes bookmarks share are not sent again for each; each bookmark
11434 +
    // still says how many of its changes were new, and a fetch with
11435 +
    // nothing new imports nothing.
11436 +
    hermetic();
11437 +
    let dir = tempfile::tempdir().unwrap();
11438 +
    let root = dir.path();
11439 +
    drop(Repo::init(&root.join("origin")).unwrap());
11440 +
    let url = root.join("origin").to_string_lossy().to_string();
11441 +
    let mut a = Repo::clone(&url, &root.join("a"))
11442 +
        .unwrap()
11443 +
        .with_home(root.join("home-a"));
11444 +
    let w = root.join("a");
11445 +
    write(&w, "f.txt", b"one\n");
11446 +
    a.commit("one", &[]).unwrap();
11447 +
    write(&w, "f.txt", b"one\ntwo\n");
11448 +
    a.commit("two", &[]).unwrap();
11449 +
    a.push("origin", false).unwrap();
11450 +
    // Five bookmarks on main's history, each one change of its own.
11451 +
    for i in 0..5 {
11452 +
        a.bookmark_create(&format!("feat{i}"), None).unwrap();
11453 +
        a.bookmark_switch(&format!("feat{i}"), false).unwrap();
11454 +
        write(&w, &format!("feat{i}.txt"), format!("{i}\n").as_bytes());
11455 +
        a.commit(&format!("feat {i}"), &[]).unwrap();
11456 +
        a.push("origin", false).unwrap();
11457 +
        a.bookmark_switch("main", false).unwrap();
11458 +
    }
11459 +
11460 +
    let b = Repo::clone(&url, &root.join("b"))
11461 +
        .unwrap()
11462 +
        .with_home(root.join("home-b"));
11463 +
    let report = b.fetch("origin").unwrap();
11464 +
    assert_eq!(report.refs.len(), 6, "every bookmark: {:?}", report.refs);
11465 +
    assert!(
11466 +
        report.refs.iter().all(|(_, _, n)| *n == 0),
11467 +
        "the clone brought them all: {:?}",
11468 +
        report.refs
11469 +
    );
11470 +
11471 +
    // One new change on one bookmark: counted there, nowhere else.
11472 +
    a.bookmark_switch("feat3", false).unwrap();
11473 +
    write(&w, "feat3.txt", b"3\nmore\n");
11474 +
    a.commit("feat 3 again", &[]).unwrap();
11475 +
    a.push("origin", false).unwrap();
11476 +
    let fetches = || crate::protocol::LOCAL_FETCHES.with(|n| n.get());
11477 +
    let at = fetches();
11478 +
    let report = b.fetch("origin").unwrap();
11479 +
    assert_eq!(fetches() - at, 1, "one bundle for all six bookmarks");
11480 +
    let counts: Vec<(String, usize)> = report
11481 +
        .refs
11482 +
        .iter()
11483 +
        .map(|(n, _, c)| (n.clone(), *c))
11484 +
        .collect();
11485 +
    assert!(counts.contains(&("feat3".to_string(), 1)), "{counts:?}");
11486 +
    assert_eq!(
11487 +
        counts.iter().filter(|(_, c)| *c > 0).count(),
11488 +
        1,
11489 +
        "only feat3 had anything new: {counts:?}"
11490 +
    );
11491 +
11492 +
    // Nothing new: no bundle at all, every bookmark still listed.
11493 +
    let at = fetches();
11494 +
    let report = b.fetch("origin").unwrap();
11495 +
    assert_eq!(fetches() - at, 0, "nothing to fetch, no round trip");
11496 +
    assert_eq!(report.refs.len(), 6);
11497 +
    assert!(
11498 +
        report.refs.iter().all(|(_, _, n)| *n == 0),
11499 +
        "{:?}",
11500 +
        report.refs
11501 +
    );
11502 +
}
11503 +
11504 +
#[test]
11505 +
fn a_stale_pinned_key_cannot_fork_the_identity() {
11506 +
    // oxygen#163: a checkout pinned author.key to a key that a rotation made
11507 +
    // elsewhere had already succeeded; delegating there built a second
11508 +
    // successor of it, splitting the identity in two.
11509 +
    hermetic();
11510 +
    let dir = tempfile::tempdir().unwrap();
11511 +
    let root = dir.path();
11512 +
    let home = root.join("home");
11513 +
    let mut a = Repo::init(&root.join("a")).unwrap().with_home(&home);
11514 +
    let (first, _) = a
11515 +
        .create_identity("Ada", "[email protected]", IdentityScope::User)
11516 +
        .unwrap();
11517 +
    let r = a.rotate_identity().unwrap();
11518 +
    assert_eq!((r.old, a.config.author_key), (first, Some(r.new)));
11519 +
    assert_eq!(a.successors(&first).unwrap(), vec![r.new]);
11520 +
    assert_eq!(a.newest(&first).unwrap(), r.new);
11521 +
11522 +
    // Another checkout still pins the first key.
11523 +
    let mut b = Repo::init(&root.join("b")).unwrap().with_home(&home);
11524 +
    b.config.set("author.key", &first.to_hex()).unwrap();
11525 +
    b.save_config().unwrap();
11526 +
    let mut b = Repo::open(&root.join("b")).unwrap().with_home(&home);
11527 +
    assert_eq!(b.config.author_key, Some(first));
11528 +
    for e in [
11529 +
        b.rotate_identity().unwrap_err().to_string(),
11530 +
        b.renew_identity(None).unwrap_err().to_string(),
11531 +
    ] {
11532 +
        assert!(e.contains("fork") && e.contains(&r.new.short(12)), "{e}");
11533 +
        assert!(
11534 +
            e.contains(&format!("oxy config author.key {}", r.new.to_hex())),
11535 +
            "{e}"
11536 +
        );
11537 +
    }
11538 +
    assert_eq!(
11539 +
        b.successors(&first).unwrap(),
11540 +
        vec![r.new],
11541 +
        "nothing was made"
11542 +
    );
11543 +
11544 +
    // Pointed at the newest key, it rotates as usual.
11545 +
    b.config.set("author.key", &r.new.to_hex()).unwrap();
11546 +
    b.save_config().unwrap();
11547 +
    let mut b = Repo::open(&root.join("b")).unwrap().with_home(&home);
11548 +
    let r2 = b.rotate_identity().unwrap();
11549 +
    assert_eq!(r2.old, r.new);
11550 +
    assert_eq!(b.newest(&first).unwrap(), r2.new);
11551 +
}
modified crates/oxy-core/src/repo/walk.rs +3 −1
…
184 unchanged lines
185 185
    let mut mine = Listing::default();
186 186
    mine.dirs.push((rel.clone(), mtime));
187 187
    for (e, name) in entries.into_iter().zip(names) {
188 -
        if name == OXY_DIR {
188 +
        // `.git` is never content: a side-by-side Git repository
189 +
        // (`git.colocate`), or Git's own beside an import.
190 +
        if name == OXY_DIR || name.eq_ignore_ascii_case(".git") {
189 191
            continue;
190 192
        }
191 193
        let Ok(ft) = e.file_type() else { continue };
…
136 unchanged lines
modified crates/oxy-core/src/subrepo.rs +30 −0
…
25 unchanged lines
26 26
}
27 27
28 28
impl Pin {
29 +
    /// Whether two pins name the same state: the same repository at the
30 +
    /// same heads, in any order, or the same Git commit. The bookmark or
31 +
    /// branch each names does not count (#147): a subrepo at its pinned
32 +
    /// state is at it whatever bookmark it sits on.
33 +
    pub fn same_state(&self, other: &Pin) -> bool {
34 +
        match (self, other) {
35 +
            (
36 +
                Pin::Oxy {
37 +
                    repo: r1,
38 +
                    heads: h1,
39 +
                    ..
40 +
                },
41 +
                Pin::Oxy {
42 +
                    repo: r2,
43 +
                    heads: h2,
44 +
                    ..
45 +
                },
46 +
            ) => {
47 +
                let (mut a, mut b) = (h1.clone(), h2.clone());
48 +
                a.sort();
49 +
                a.dedup();
50 +
                b.sort();
51 +
                b.dedup();
52 +
                r1 == r2 && a == b
53 +
            }
54 +
            (Pin::Git { sha: a, .. }, Pin::Git { sha: b, .. }) => a == b,
55 +
            _ => false,
56 +
        }
57 +
    }
58 +
29 59
    pub fn encode(&self) -> Vec<u8> {
30 60
        let v = match self {
31 61
            Pin::Oxy {
…
278 unchanged lines
modified crates/oxy-server/tests/http.rs +27 −1
…
757 unchanged lines
758 758
        vec!["good".to_string()],
759 759
        "the parent's, to fall back on"
760 760
    );
761 +
    assert!(!options.fallback_on_401_only);
761 762
    write(&nested, "c.txt", b"child\n");
762 763
    child.commit("child work", &[]).unwrap();
763 764
    let pushed = child.push("origin", false).unwrap();
…
7 unchanged lines
771 772
        1
772 773
    );
773 774
774 -
    // Its own token, if wrong, stays wrong: what a checkout sets is its choice.
775 +
    // Its own token, if refused, falls back to this computer's sign-in,
776 +
    // never to the parent's (#155): stale too here, so the push fails and
777 +
    // says what to do.
775 778
    child.config.set("remote.origin.token", "wrong").unwrap();
776 779
    child.save_config().unwrap();
780 +
    let options = child.options_for_url(&format!("{base}/child"));
781 +
    assert_eq!(
782 +
        options.fallback_tokens,
783 +
        vec!["stale".to_string()],
784 +
        "the sign-in only"
785 +
    );
786 +
    assert!(
787 +
        options.fallback_on_401_only,
788 +
        "and only for a token the hub does not know: one refused for permission is not swapped"
789 +
    );
777 790
    write(&nested, "d.txt", b"more\n");
778 791
    child.commit("more", &[]).unwrap();
779 792
    let err = child.push("origin", false).err().unwrap().to_string();
780 793
    assert!(err.contains("401"), "{err}");
794 +
    let note = child.stale_token_note("origin", &err).expect("a note");
795 +
    assert!(
796 +
        note.contains("oxy auth login") && note.contains("oxy config --unset remote.origin.token"),
797 +
        "{note}"
798 +
    );
799 +
    assert!(child.stale_token_note("origin", "HTTP 500").is_none());
800 +
    // With a good sign-in, an expired checkout token no longer blocks.
801 +
    oxy_core::credentials::Credentials::at(&home)
802 +
        .save(&base, "good", Some("andy"), None)
803 +
        .unwrap();
804 +
    let child = Repo::open(&nested).unwrap().with_home(&home);
805 +
    let pushed = child.push("origin", false).unwrap();
806 +
    assert!(pushed[0].ok, "{}", pushed[0].reason);
781 807
}
782 808
783 809
/// A host refuses a push by what it adds (`Backend::vet_push`): the client
…
450 unchanged lines
modified docs/cylinder.md +11 −0
…
395 unchanged lines
396 396
- **C1. The service boundary and artifact model.** Implement the
397 397
  `Artifact` CBOR object type and FastCDC chunk engine in `cylinder`;
398 398
  unify runner images with artifacts.
399 +
  *Started 2026-10-28 (oxygen#108):* `crates/cylinder` has the
400 +
  `Artifact` and `DirTree` encodings of §4.1 (canonical CBOR; a name is
401 +
  the BLAKE3 of the encoding; `env` and `labels` as sorted pairs, since
402 +
  the profile's maps take integer keys), packing a directory into the
403 +
  repository's own blobs and chunks, verifying and unpacking, and the
404 +
  roots garbage collection keeps; `oxy artifact pack | list | show |
405 +
  unpack | verify`. The artifact is an object type of its own
406 +
  (`artifact`, in oxy-core's `artifact` module, checked by `oxy fsck`),
407 +
  named by its object hash like every object; its tree is a blob, and
408 +
  `.oxy/artifacts/NAME` records the artifacts a repository keeps. Next:
409 +
  runner images as artifacts, then moving them between repositories (C2).
399 410
- **C2. Channels, provenance and consensus.** Flow publishes artifacts;
400 411
  signed channel move attestations; multi-party `reproduced(N)` consensus;
401 412
  revset predicates.
…
44 unchanged lines
added docs/mcp.md +78 −0
1 +
# oxy mcp: Oxygen for AI agents
2 +
3 +
`oxy mcp` serves one repository to an AI agent over the
4 +
[Model Context Protocol](https://modelcontextprotocol.io) (MCP), on stdio
5 +
(oxygen#159). The agent calls typed tools (`status`, `log`, `commit`, …)
6 +
and gets JSON back, instead of running `oxy` in a shell and reading its
7 +
output.
8 +
9 +
## Who changes are signed as
10 +
11 +
The server signs as whatever identity its environment configures, exactly
12 +
as `oxy` would: it runs each tool as this same `oxy` binary, in the
13 +
repository, with the server's environment. Start it from an agent home and
14 +
every change the agent makes is sealed **via the agent**, with no
15 +
`OXY_HOME` to remember on each command:
16 +
17 +
```sh
18 +
OXY_HOME=~/.oxy-agents/claude oxy mcp ~/development/myproject
19 +
```
20 +
21 +
The server says who it signs as in its `initialize` reply (the first line
22 +
of `oxy identity show`), and the `identity` tool shows the rest. Without an
23 +
identity, reading works and committing is refused with oxy's own message.
24 +
25 +
## Tools
26 +
27 +
Each tool returns the command's `--json` output as structured content (an
28 +
array comes wrapped as `{"result": […]}`) and as text. A failed command is a
29 +
tool result with `isError: true` and oxy's error, so the agent sees why.
30 +
31 +
| Tool | Runs | Arguments |
32 +
|---|---|---|
33 +
| `status` | `oxy status --json` | |
34 +
| `diff` | `oxy diff [CHANGE] --json` | `change` |
35 +
| `log` | `oxy log --json` | `revset`, `path`, `limit` (default 20, at most 1000) |
36 +
| `show` | `oxy show CHANGE --json` | `change` (default `@`), `stat` |
37 +
| `cat` | `oxy cat PATH [-r CHANGE]` | `path`, `change` |
38 +
| `blame` | `oxy blame PATH --json` | `path` |
39 +
| `bookmarks` | `oxy bookmark list --json` | `all` |
40 +
| `tags` | `oxy tag list --json` | |
41 +
| `conflicts` | `oxy resolve --json` (report only) | |
42 +
| `op_log` | `oxy op log --json` | `limit` |
43 +
| `identity` | `oxy identity show` | |
44 +
| `commit` | `oxy commit -m MESSAGE PATHS…` | `message`, and `paths` or `all: true` |
45 +
| `amend` | `oxy amend [-m] PATHS…` | `paths` or `all: true`, `message` |
46 +
| `describe` | `oxy describe [CHANGE] -m MESSAGE` | `message`, `change` |
47 +
| `fetch`, `pull` | `oxy fetch\|pull [REMOTE] --json` | `remote` |
48 +
| `push` | `oxy push [REMOTE] --json` | `remote`, `bookmark`, `force` |
49 +
| `tlog_verify` | `oxy tlog verify --json` | |
50 +
| `undo` | `oxy undo` | only with `--allow-destructive` |
51 +
52 +
## Safety
53 +
54 +
- **Commits are deliberate.** `commit` and `amend` take the paths to
55 +
  include, or `all: true`; never neither.
56 +
- **Nothing destructive by default.** `undo` is not offered, and `push`
57 +
  refuses `force`, unless the server is started with `--allow-destructive`.
58 +
  Tag withdrawal, bookmark deletion and history rewriting are not tools at
59 +
  all. Each tool carries MCP's `readOnlyHint` and `destructiveHint`.
60 +
- **Values are never flags.** Arguments are passed as `--flag=value` and
61 +
  after `--`, so a path or message starting with `-` stays a value.
62 +
- **Your policy still applies.** Pushes go through the remote's checks and
63 +
  the repository's trust policy like any other; an agent's delegation
64 +
  scope limits what its seals may do.
65 +
- **Output is bounded.** A tool's text stops at 256 KiB, saying to narrow
66 +
  the request.
67 +
68 +
## Adding it to an agent
69 +
70 +
Claude Code, for one project (run in the project's root):
71 +
72 +
```sh
73 +
claude mcp add oxy -- env OXY_HOME=$HOME/.oxy-agents/claude oxy mcp
74 +
```
75 +
76 +
Any MCP client that starts stdio servers takes the same command: `oxy mcp
77 +
[DIR] [--allow-destructive]`, with `OXY_HOME` set for an agent's identity.
78 +
`DIR` defaults to the current directory and must be a repository's root.
modified docs/spec.md +5 −0
…
1012 unchanged lines
1013 1013
- **Fetch and push against a Git remote.** `oxy git fetch URL` keeps a bare mirror under `.oxy/git/` for the remote, fetches the branch into it, and imports the commits the bookmark does not have yet, seeding the import with the commits already known: an imported commit maps to its change, and a commit this repository exported maps to the bookmark state it was made from (so a Git commit on top of exported work is based on everything that work contained, including changes Git never saw). The bookmark moves to the union of its heads and the new changes, like a pull; the working copy follows when it was clean. A fetched state whose previous state was itself a Git tree is remembered as equal to the fetched tip. `oxy git push URL` refuses until the remote's tip is known, exports the bookmark's later states as commits chained onto that tip, and pushes the branch, so the push is always a fast-forward.
1014 1014
- **Submodules.** A Git gitlink entry (mode `160000`) imports as a `subrepo` node with a Git pin (§7.8) and the matching `.gitmodules` entry becomes a `.oxymodules` entry with `branch` set to the submodule's configured branch when there is one. Export writes a Git pin back as a gitlink, keeps a tracked `.gitmodules` as is, and otherwise derives `.gitmodules` from `.oxymodules`; an Oxygen pin has no Git form and is omitted from the exported tree, which the export report counts.
1015 1015
- **Merges of opaque files.** When the union of a merge commit's parents holds several live blobs for a file or submodule, the merge's change replaces each of them with the commit's blob and is a resolution: Git's merge is the authority, unlike a working copy, where a candidate on disk is not yet a decision.
1016 +
- **A side-by-side `.git`.** `oxy git colocate` (config `git.colocate`) keeps a `.git` beside `.oxy` so editors' and other tools' Git features work on an Oxygen checkout: made with `git init` when missing, `/.oxy/` added to its `info/exclude`, the current bookmark exported into it (as `oxy git export` does) after every oxy command that leaves the bookmark somewhere new, HEAD on that branch and the index at its tree, so `git status` shows exactly the working copy's uncommitted work. It is one way: commits made with Git are not read back. oxy never takes a `.git` directory as content, and checkout refuses to write a path with a `.git` component at any depth or case (as Git does), so a change cannot plant hooks there. `oxy git colocate --off` stops the updates and leaves `.git` in place.
1017 +
- **Export by change graph.** For a server serving Git clients (a hub's Git view), `git_export_graph` exports every bookmark and tag from the changes alone, so the same changes give the same commits wherever it runs: each change is a commit whose tree is the state of the change and its dependency closure and whose parents are its dependencies' commits (a change that depends on nothing is a root), with the change's author, time, message and `Change-Id:`/`Oxy-Hash:` trailers; a bookmark with one head is a branch at its commit, with several a merge of their commits (the bookmark's state, at the newest head's time, `Oxy-Heads:`); tags likewise. Marks make it incremental; branches of deleted bookmarks are removed.
1016 1018
- **Round trip.** Import followed by export MUST reproduce every tree hash. Export followed by import MUST reproduce every ChangeId. `oxy git verify GIT_DIR` checks an import commit by commit: for each commit, the state formed by the changes imported for it and all its ancestors must materialize to exactly that commit's tree (content, executable bits, symlink targets, submodule pins; `.oxymodules` is allowed beside submodules). `--last N` rebuilds only the last N commits, following the others, to look again at where an import went wrong.
1017 1019
1018 1020
## 24. Command-line interface
…
26 unchanged lines
1045 1047
oxy doctor [--offline] [--json]     check identity, config and remotes (§21.2)
1046 1048
oxy completions SHELL               shell completion script     oxy man [--dir DIR]   man pages
1047 1049
oxy hunks                           oxy discard PATH.. | --hunks N,M
1050 +
oxy mcp [DIR] [--allow-destructive] serve the repository to an AI agent (MCP, stdio)
1048 1051
```
1049 1052
1050 1053
**Machine interface.** `status`, `log` (with `--path`), `show`, `diff`, `hunks`, `blame`, `resolve`, `op log`, `bookmark list`, `remote list`, `fetch`, `pull`, `push`, `submodule status`, `bisect status`, `verify`, `policy show`, `root` and `version` take `--json` and then print exactly one JSON document on stdout, with ids as hex strings and times in milliseconds since the epoch; errors go to stderr with a non-zero status. `commit --hunks N,M` and `discard --hunks N,M` take hunk numbers as `hunks` prints them. Editors and other tools MUST use these rather than reading `.oxy/`.
1051 1054
1055 +
**Agents.** `oxy mcp` serves the repository to an AI agent over the Model Context Protocol on stdio (docs/mcp.md): each tool runs one of the commands above with `--json`, in the repository and with the server's environment, so it signs as the identity `OXY_HOME` configures (an agent home seals every change via the agent). `commit` and `amend` take explicit paths or `all: true`; `undo` and a forced push are offered only with `--allow-destructive`.
1056 +
1052 1057
Change arguments accept a ChangeId prefix, a ChangeHash prefix (with `#`), a bookmark or tag name, and `@` for the working-copy change. Ambiguous prefixes are an error.
1053 1058
1054 1059
Reserved for a later version, with these names: `oxy new`, `oxy edit`, `oxy redo`, revsets (`log -r`), and `oxy remote trust` (with the Noise transport, §15).
…
140 unchanged lines
added packaging/haikuports/README.md +40 −0
1 +
# oxy and oh on HaikuDepot
2 +
3 +
[haikuports](https://github.com/haikuports/haikuports) recipes for oxy
4 +
(`dev-vcs/oxy`) and OxygenHub's `oh` (`dev-vcs/oh`) are written by
5 +
`make-recipe.py` from a release's source tarball (oxygen#156, #157).
6 +
7 +
Each release's dist job attaches `NAME-VERSION-src.tar.gz`: the source as
8 +
released, reproducible (sorted, fixed times and owners, gzip without a
9 +
timestamp). oh's holds the oxygenhub tree with its subrepos inside, so it
10 +
builds alone. The recipe fetches it from the release's stable address,
11 +
`https://oxygenhub.io/andymccall/REPO/releases/download/vVERSION/NAME-VERSION-src.tar.gz`,
12 +
and every crates.io crate in its `Cargo.lock` as a source of its own with its
13 +
checksum, building offline as haikuports' other Rust recipes (ripgrep) do.
14 +
15 +
For a release (oxy shown; oh is the same with `oh` and `oxygenhub`):
16 +
17 +
```sh
18 +
curl -LO https://oxygenhub.io/andymccall/oxygen/releases/download/vX.Y.Z/oxy-X.Y.Z-src.tar.gz
19 +
python3 packaging/haikuports/make-recipe.py oxy oxy-X.Y.Z-src.tar.gz X.Y.Z > oxy-X.Y.Z.recipe
20 +
```
21 +
22 +
Test it on Haiku R1/beta6 with haikuports checked out and `haikuporter`
23 +
installed: copy the recipe to `haikuports/dev-vcs/oxy/` and run
24 +
`haikuporter --no-source-packages -j1 -y oxy`. Submit it as a pull request
25 +
to haikuports (`oxy: new recipe`, then `oxy: bump to X.Y.Z`).
26 +
27 +
Haiku needs the system's zstd (`ZSTD_SYS_USE_PKG_CONFIG=1`: the bundled
28 +
zstd's hidden symbols break the link), which the recipe sets. oh's tests
29 +
pull in the hub, whose pipeline parser needs a newer Rust than HaikuPorts'
30 +
1.94, so its recipe tests `hub-api` only.
31 +
32 +
## Icons
33 +
34 +
HaikuDepot takes a package's icon from HaikuDepotServer, not from the
35 +
recipe. `icons/` holds them, written by `make-icons.py icons/` (the PNGs
36 +
rendered from the SVGs): Oxygen (`oxy`) is the mark on a pale tile,
37 +
OxygenHub (`oh`) the mark on the hub's dark tile, as the VS Code
38 +
extensions' icons are. Once the packages are listed on
39 +
depot.haiku-os.org, upload `NAME.hvif` with `NAME-16.png`, `-32` and `-64`
40 +
as each package's icon.
added packaging/haikuports/dev-vcs/oh/oh-0.9.16.recipe +1472 −0
1 +
SUMMARY="The command line of OxygenHub, the forge for Oxygen repositories"
2 +
DESCRIPTION="oh talks to an OxygenHub (such as https://oxygenhub.io): issues, proposals \
3 +
and their reviews, releases, Flow runs, their logs and reports, and checks, \
4 +
from the terminal. It works alongside oxy, the Oxygen version control \
5 +
client."
6 +
HOMEPAGE="https://oxygenhub.io/andymccall/oxygenhub"
7 +
COPYRIGHT="2026 Andy McCall"
8 +
LICENSE="MIT
9 +
	Apache v2"
10 +
REVISION="1"
11 +
SOURCE_URI="$HOMEPAGE/releases/download/v$portVersion/oh-$portVersion-src.tar.gz"
12 +
CHECKSUM_SHA256="55770a8668647a44e57ed085315dc11bd97028d2a5c1a1640cd85af87b461200"
13 +
SOURCE_DIR="oxygenhub-$portVersion"
14 +
SOURCE_URI_2="https://static.crates.io/crates/adler2/adler2-2.0.1.crate"
15 +
CHECKSUM_SHA256_2="320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
16 +
SOURCE_FILENAME_2="adler2-2.0.1.tar.gz"
17 +
SOURCE_URI_3="https://static.crates.io/crates/aho-corasick/aho-corasick-1.1.5.crate"
18 +
CHECKSUM_SHA256_3="c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
19 +
SOURCE_FILENAME_3="aho-corasick-1.1.5.tar.gz"
20 +
SOURCE_URI_4="https://static.crates.io/crates/allocator-api2/allocator-api2-0.2.21.crate"
21 +
CHECKSUM_SHA256_4="683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
22 +
SOURCE_FILENAME_4="allocator-api2-0.2.21.tar.gz"
23 +
SOURCE_URI_5="https://static.crates.io/crates/ammonia/ammonia-4.2.0.crate"
24 +
CHECKSUM_SHA256_5="3f7ed3ac4252be4d3e36fb589ed5659356842587b21a31596a9dc89d3b2f5e2a"
25 +
SOURCE_FILENAME_5="ammonia-4.2.0.tar.gz"
26 +
SOURCE_URI_6="https://static.crates.io/crates/android_system_properties/android_system_properties-0.1.6.crate"
27 +
CHECKSUM_SHA256_6="ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
28 +
SOURCE_FILENAME_6="android_system_properties-0.1.6.tar.gz"
29 +
SOURCE_URI_7="https://static.crates.io/crates/anstream/anstream-1.0.0.crate"
30 +
CHECKSUM_SHA256_7="824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
31 +
SOURCE_FILENAME_7="anstream-1.0.0.tar.gz"
32 +
SOURCE_URI_8="https://static.crates.io/crates/anstyle/anstyle-1.0.14.crate"
33 +
CHECKSUM_SHA256_8="940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
34 +
SOURCE_FILENAME_8="anstyle-1.0.14.tar.gz"
35 +
SOURCE_URI_9="https://static.crates.io/crates/anstyle-parse/anstyle-parse-1.0.0.crate"
36 +
CHECKSUM_SHA256_9="52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
37 +
SOURCE_FILENAME_9="anstyle-parse-1.0.0.tar.gz"
38 +
SOURCE_URI_10="https://static.crates.io/crates/anstyle-query/anstyle-query-1.1.5.crate"
39 +
CHECKSUM_SHA256_10="40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
40 +
SOURCE_FILENAME_10="anstyle-query-1.1.5.tar.gz"
41 +
SOURCE_URI_11="https://static.crates.io/crates/anstyle-wincon/anstyle-wincon-3.0.11.crate"
42 +
CHECKSUM_SHA256_11="291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
43 +
SOURCE_FILENAME_11="anstyle-wincon-3.0.11.tar.gz"
44 +
SOURCE_URI_12="https://static.crates.io/crates/anyhow/anyhow-1.0.104.crate"
45 +
CHECKSUM_SHA256_12="330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
46 +
SOURCE_FILENAME_12="anyhow-1.0.104.tar.gz"
47 +
SOURCE_URI_13="https://static.crates.io/crates/arbitrary/arbitrary-1.4.2.crate"
48 +
CHECKSUM_SHA256_13="c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
49 +
SOURCE_FILENAME_13="arbitrary-1.4.2.tar.gz"
50 +
SOURCE_URI_14="https://static.crates.io/crates/arc-swap/arc-swap-1.9.2.crate"
51 +
CHECKSUM_SHA256_14="c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
52 +
SOURCE_FILENAME_14="arc-swap-1.9.2.tar.gz"
53 +
SOURCE_URI_15="https://static.crates.io/crates/argon2/argon2-0.5.3.crate"
54 +
CHECKSUM_SHA256_15="3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
55 +
SOURCE_FILENAME_15="argon2-0.5.3.tar.gz"
56 +
SOURCE_URI_16="https://static.crates.io/crates/arrayvec/arrayvec-0.7.8.crate"
57 +
CHECKSUM_SHA256_16="d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
58 +
SOURCE_FILENAME_16="arrayvec-0.7.8.tar.gz"
59 +
SOURCE_URI_17="https://static.crates.io/crates/askama/askama-0.14.0.crate"
60 +
CHECKSUM_SHA256_17="f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
61 +
SOURCE_FILENAME_17="askama-0.14.0.tar.gz"
62 +
SOURCE_URI_18="https://static.crates.io/crates/askama_derive/askama_derive-0.14.0.crate"
63 +
CHECKSUM_SHA256_18="129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
64 +
SOURCE_FILENAME_18="askama_derive-0.14.0.tar.gz"
65 +
SOURCE_URI_19="https://static.crates.io/crates/askama_parser/askama_parser-0.14.0.crate"
66 +
CHECKSUM_SHA256_19="d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
67 +
SOURCE_FILENAME_19="askama_parser-0.14.0.tar.gz"
68 +
SOURCE_URI_20="https://static.crates.io/crates/asn1-rs/asn1-rs-0.7.2.crate"
69 +
CHECKSUM_SHA256_20="b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
70 +
SOURCE_FILENAME_20="asn1-rs-0.7.2.tar.gz"
71 +
SOURCE_URI_21="https://static.crates.io/crates/asn1-rs-derive/asn1-rs-derive-0.6.0.crate"
72 +
CHECKSUM_SHA256_21="3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
73 +
SOURCE_FILENAME_21="asn1-rs-derive-0.6.0.tar.gz"
74 +
SOURCE_URI_22="https://static.crates.io/crates/asn1-rs-impl/asn1-rs-impl-0.2.0.crate"
75 +
CHECKSUM_SHA256_22="7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
76 +
SOURCE_FILENAME_22="asn1-rs-impl-0.2.0.tar.gz"
77 +
SOURCE_URI_23="https://static.crates.io/crates/async-compression/async-compression-0.4.48.crate"
78 +
CHECKSUM_SHA256_23="fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd"
79 +
SOURCE_FILENAME_23="async-compression-0.4.48.tar.gz"
80 +
SOURCE_URI_24="https://static.crates.io/crates/async-trait/async-trait-0.1.92.crate"
81 +
CHECKSUM_SHA256_24="82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
82 +
SOURCE_FILENAME_24="async-trait-0.1.92.tar.gz"
83 +
SOURCE_URI_25="https://static.crates.io/crates/atoi/atoi-2.0.0.crate"
84 +
CHECKSUM_SHA256_25="f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
85 +
SOURCE_FILENAME_25="atoi-2.0.0.tar.gz"
86 +
SOURCE_URI_26="https://static.crates.io/crates/atomic-waker/atomic-waker-1.1.2.crate"
87 +
CHECKSUM_SHA256_26="1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
88 +
SOURCE_FILENAME_26="atomic-waker-1.1.2.tar.gz"
89 +
SOURCE_URI_27="https://static.crates.io/crates/autocfg/autocfg-1.5.1.crate"
90 +
CHECKSUM_SHA256_27="f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
91 +
SOURCE_FILENAME_27="autocfg-1.5.1.tar.gz"
92 +
SOURCE_URI_28="https://static.crates.io/crates/axum/axum-0.8.9.crate"
93 +
CHECKSUM_SHA256_28="31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
94 +
SOURCE_FILENAME_28="axum-0.8.9.tar.gz"
95 +
SOURCE_URI_29="https://static.crates.io/crates/axum-core/axum-core-0.5.6.crate"
96 +
CHECKSUM_SHA256_29="08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
97 +
SOURCE_FILENAME_29="axum-core-0.5.6.tar.gz"
98 +
SOURCE_URI_30="https://static.crates.io/crates/axum-extra/axum-extra-0.10.3.crate"
99 +
CHECKSUM_SHA256_30="9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
100 +
SOURCE_FILENAME_30="axum-extra-0.10.3.tar.gz"
101 +
SOURCE_URI_31="https://static.crates.io/crates/axum-macros/axum-macros-0.5.1.crate"
102 +
CHECKSUM_SHA256_31="7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
103 +
SOURCE_FILENAME_31="axum-macros-0.5.1.tar.gz"
104 +
SOURCE_URI_32="https://static.crates.io/crates/axum-server/axum-server-0.8.0.crate"
105 +
CHECKSUM_SHA256_32="b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc"
106 +
SOURCE_FILENAME_32="axum-server-0.8.0.tar.gz"
107 +
SOURCE_URI_33="https://static.crates.io/crates/base32/base32-0.5.1.crate"
108 +
CHECKSUM_SHA256_33="022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076"
109 +
SOURCE_FILENAME_33="base32-0.5.1.tar.gz"
110 +
SOURCE_URI_34="https://static.crates.io/crates/base64/base64-0.22.1.crate"
111 +
CHECKSUM_SHA256_34="72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
112 +
SOURCE_FILENAME_34="base64-0.22.1.tar.gz"
113 +
SOURCE_URI_35="https://static.crates.io/crates/base64/base64-0.23.1.crate"
114 +
CHECKSUM_SHA256_35="ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
115 +
SOURCE_FILENAME_35="base64-0.23.1.tar.gz"
116 +
SOURCE_URI_36="https://static.crates.io/crates/base64ct/base64ct-1.8.3.crate"
117 +
CHECKSUM_SHA256_36="2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
118 +
SOURCE_FILENAME_36="base64ct-1.8.3.tar.gz"
119 +
SOURCE_URI_37="https://static.crates.io/crates/basic-toml/basic-toml-0.1.10.crate"
120 +
CHECKSUM_SHA256_37="ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
121 +
SOURCE_FILENAME_37="basic-toml-0.1.10.tar.gz"
122 +
SOURCE_URI_38="https://static.crates.io/crates/bincode/bincode-1.3.3.crate"
123 +
CHECKSUM_SHA256_38="b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
124 +
SOURCE_FILENAME_38="bincode-1.3.3.tar.gz"
125 +
SOURCE_URI_39="https://static.crates.io/crates/bit-set/bit-set-0.8.0.crate"
126 +
CHECKSUM_SHA256_39="08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
127 +
SOURCE_FILENAME_39="bit-set-0.8.0.tar.gz"
128 +
SOURCE_URI_40="https://static.crates.io/crates/bit-vec/bit-vec-0.8.0.crate"
129 +
CHECKSUM_SHA256_40="5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
130 +
SOURCE_FILENAME_40="bit-vec-0.8.0.tar.gz"
131 +
SOURCE_URI_41="https://static.crates.io/crates/bit-vec/bit-vec-0.9.1.crate"
132 +
CHECKSUM_SHA256_41="b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
133 +
SOURCE_FILENAME_41="bit-vec-0.9.1.tar.gz"
134 +
SOURCE_URI_42="https://static.crates.io/crates/bitflags/bitflags-2.13.2.crate"
135 +
CHECKSUM_SHA256_42="3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
136 +
SOURCE_FILENAME_42="bitflags-2.13.2.tar.gz"
137 +
SOURCE_URI_43="https://static.crates.io/crates/blake2/blake2-0.10.6.crate"
138 +
CHECKSUM_SHA256_43="46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
139 +
SOURCE_FILENAME_43="blake2-0.10.6.tar.gz"
140 +
SOURCE_URI_44="https://static.crates.io/crates/blake3/blake3-1.8.7.crate"
141 +
CHECKSUM_SHA256_44="6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
142 +
SOURCE_FILENAME_44="blake3-1.8.7.tar.gz"
143 +
SOURCE_URI_45="https://static.crates.io/crates/block-buffer/block-buffer-0.10.4.crate"
144 +
CHECKSUM_SHA256_45="3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
145 +
SOURCE_FILENAME_45="block-buffer-0.10.4.tar.gz"
146 +
SOURCE_URI_46="https://static.crates.io/crates/block-buffer/block-buffer-0.12.1.crate"
147 +
CHECKSUM_SHA256_46="d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
148 +
SOURCE_FILENAME_46="block-buffer-0.12.1.tar.gz"
149 +
SOURCE_URI_47="https://static.crates.io/crates/bstr/bstr-1.13.1.crate"
150 +
CHECKSUM_SHA256_47="6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
151 +
SOURCE_FILENAME_47="bstr-1.13.1.tar.gz"
152 +
SOURCE_URI_48="https://static.crates.io/crates/bumpalo/bumpalo-3.20.3.crate"
153 +
CHECKSUM_SHA256_48="72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
154 +
SOURCE_FILENAME_48="bumpalo-3.20.3.tar.gz"
155 +
SOURCE_URI_49="https://static.crates.io/crates/bytemuck/bytemuck-1.25.2.crate"
156 +
CHECKSUM_SHA256_49="95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
157 +
SOURCE_FILENAME_49="bytemuck-1.25.2.tar.gz"
158 +
SOURCE_URI_50="https://static.crates.io/crates/byteorder/byteorder-1.5.0.crate"
159 +
CHECKSUM_SHA256_50="1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
160 +
SOURCE_FILENAME_50="byteorder-1.5.0.tar.gz"
161 +
SOURCE_URI_51="https://static.crates.io/crates/byteorder-lite/byteorder-lite-0.1.0.crate"
162 +
CHECKSUM_SHA256_51="8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
163 +
SOURCE_FILENAME_51="byteorder-lite-0.1.0.tar.gz"
164 +
SOURCE_URI_52="https://static.crates.io/crates/bytes/bytes-1.12.1.crate"
165 +
CHECKSUM_SHA256_52="fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
166 +
SOURCE_FILENAME_52="bytes-1.12.1.tar.gz"
167 +
SOURCE_URI_53="https://static.crates.io/crates/cc/cc-1.5.1.crate"
168 +
CHECKSUM_SHA256_53="f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
169 +
SOURCE_FILENAME_53="cc-1.5.1.tar.gz"
170 +
SOURCE_URI_54="https://static.crates.io/crates/cfg-if/cfg-if-1.0.5.crate"
171 +
CHECKSUM_SHA256_54="4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
172 +
SOURCE_FILENAME_54="cfg-if-1.0.5.tar.gz"
173 +
SOURCE_URI_55="https://static.crates.io/crates/cfg_aliases/cfg_aliases-0.2.2.crate"
174 +
CHECKSUM_SHA256_55="f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
175 +
SOURCE_FILENAME_55="cfg_aliases-0.2.2.tar.gz"
176 +
SOURCE_URI_56="https://static.crates.io/crates/chacha20/chacha20-0.10.2.crate"
177 +
CHECKSUM_SHA256_56="65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
178 +
SOURCE_FILENAME_56="chacha20-0.10.2.tar.gz"
179 +
SOURCE_URI_57="https://static.crates.io/crates/chrono/chrono-0.4.45.crate"
180 +
CHECKSUM_SHA256_57="1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
181 +
SOURCE_FILENAME_57="chrono-0.4.45.tar.gz"
182 +
SOURCE_URI_58="https://static.crates.io/crates/clap/clap-4.6.7.crate"
183 +
CHECKSUM_SHA256_58="aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
184 +
SOURCE_FILENAME_58="clap-4.6.7.tar.gz"
185 +
SOURCE_URI_59="https://static.crates.io/crates/clap_builder/clap_builder-4.6.7.crate"
186 +
CHECKSUM_SHA256_59="ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
187 +
SOURCE_FILENAME_59="clap_builder-4.6.7.tar.gz"
188 +
SOURCE_URI_60="https://static.crates.io/crates/clap_derive/clap_derive-4.6.7.crate"
189 +
CHECKSUM_SHA256_60="f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
190 +
SOURCE_FILENAME_60="clap_derive-4.6.7.tar.gz"
191 +
SOURCE_URI_61="https://static.crates.io/crates/clap_lex/clap_lex-1.1.1.crate"
192 +
CHECKSUM_SHA256_61="1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
193 +
SOURCE_FILENAME_61="clap_lex-1.1.1.tar.gz"
194 +
SOURCE_URI_62="https://static.crates.io/crates/cmov/cmov-0.5.4.crate"
195 +
CHECKSUM_SHA256_62="0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
196 +
SOURCE_FILENAME_62="cmov-0.5.4.tar.gz"
197 +
SOURCE_URI_63="https://static.crates.io/crates/colorchoice/colorchoice-1.0.5.crate"
198 +
CHECKSUM_SHA256_63="1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
199 +
SOURCE_FILENAME_63="colorchoice-1.0.5.tar.gz"
200 +
SOURCE_URI_64="https://static.crates.io/crates/compression-codecs/compression-codecs-0.4.43.crate"
201 +
CHECKSUM_SHA256_64="bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9"
202 +
SOURCE_FILENAME_64="compression-codecs-0.4.43.tar.gz"
203 +
SOURCE_URI_65="https://static.crates.io/crates/compression-core/compression-core-0.4.33.crate"
204 +
CHECKSUM_SHA256_65="6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414"
205 +
SOURCE_FILENAME_65="compression-core-0.4.33.tar.gz"
206 +
SOURCE_URI_66="https://static.crates.io/crates/const-oid/const-oid-0.10.2.crate"
207 +
CHECKSUM_SHA256_66="a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
208 +
SOURCE_FILENAME_66="const-oid-0.10.2.tar.gz"
209 +
SOURCE_URI_67="https://static.crates.io/crates/const-oid/const-oid-0.9.6.crate"
210 +
CHECKSUM_SHA256_67="c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
211 +
SOURCE_FILENAME_67="const-oid-0.9.6.tar.gz"
212 +
SOURCE_URI_68="https://static.crates.io/crates/constant_time_eq/constant_time_eq-0.4.2.crate"
213 +
CHECKSUM_SHA256_68="3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
214 +
SOURCE_FILENAME_68="constant_time_eq-0.4.2.tar.gz"
215 +
SOURCE_URI_69="https://static.crates.io/crates/cookie/cookie-0.18.2.crate"
216 +
CHECKSUM_SHA256_69="1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
217 +
SOURCE_FILENAME_69="cookie-0.18.2.tar.gz"
218 +
SOURCE_URI_70="https://static.crates.io/crates/core_detect/core_detect-1.0.0.crate"
219 +
CHECKSUM_SHA256_70="7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
220 +
SOURCE_FILENAME_70="core_detect-1.0.0.tar.gz"
221 +
SOURCE_URI_71="https://static.crates.io/crates/cpufeatures/cpufeatures-0.2.17.crate"
222 +
CHECKSUM_SHA256_71="59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
223 +
SOURCE_FILENAME_71="cpufeatures-0.2.17.tar.gz"
224 +
SOURCE_URI_72="https://static.crates.io/crates/cpufeatures/cpufeatures-0.3.1.crate"
225 +
CHECKSUM_SHA256_72="5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
226 +
SOURCE_FILENAME_72="cpufeatures-0.3.1.tar.gz"
227 +
SOURCE_URI_73="https://static.crates.io/crates/crc/crc-3.4.0.crate"
228 +
CHECKSUM_SHA256_73="5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
229 +
SOURCE_FILENAME_73="crc-3.4.0.tar.gz"
230 +
SOURCE_URI_74="https://static.crates.io/crates/crc-catalog/crc-catalog-2.5.0.crate"
231 +
CHECKSUM_SHA256_74="217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
232 +
SOURCE_FILENAME_74="crc-catalog-2.5.0.tar.gz"
233 +
SOURCE_URI_75="https://static.crates.io/crates/crc32fast/crc32fast-1.5.2.crate"
234 +
CHECKSUM_SHA256_75="01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
235 +
SOURCE_FILENAME_75="crc32fast-1.5.2.tar.gz"
236 +
SOURCE_URI_76="https://static.crates.io/crates/crossbeam-deque/crossbeam-deque-0.8.8.crate"
237 +
CHECKSUM_SHA256_76="622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
238 +
SOURCE_FILENAME_76="crossbeam-deque-0.8.8.tar.gz"
239 +
SOURCE_URI_77="https://static.crates.io/crates/crossbeam-epoch/crossbeam-epoch-0.9.21.crate"
240 +
CHECKSUM_SHA256_77="dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
241 +
SOURCE_FILENAME_77="crossbeam-epoch-0.9.21.tar.gz"
242 +
SOURCE_URI_78="https://static.crates.io/crates/crossbeam-queue/crossbeam-queue-0.3.14.crate"
243 +
CHECKSUM_SHA256_78="03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae"
244 +
SOURCE_FILENAME_78="crossbeam-queue-0.3.14.tar.gz"
245 +
SOURCE_URI_79="https://static.crates.io/crates/crossbeam-utils/crossbeam-utils-0.8.23.crate"
246 +
CHECKSUM_SHA256_79="a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
247 +
SOURCE_FILENAME_79="crossbeam-utils-0.8.23.tar.gz"
248 +
SOURCE_URI_80="https://static.crates.io/crates/crypto-common/crypto-common-0.1.7.crate"
249 +
CHECKSUM_SHA256_80="78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
250 +
SOURCE_FILENAME_80="crypto-common-0.1.7.tar.gz"
251 +
SOURCE_URI_81="https://static.crates.io/crates/crypto-common/crypto-common-0.2.2.crate"
252 +
CHECKSUM_SHA256_81="ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
253 +
SOURCE_FILENAME_81="crypto-common-0.2.2.tar.gz"
254 +
SOURCE_URI_82="https://static.crates.io/crates/cssparser/cssparser-0.38.0.crate"
255 +
CHECKSUM_SHA256_82="11119743ad110e8c1bdccd930d7f5c30c99e5fc76a7b63ec9807e84eef0c5f59"
256 +
SOURCE_FILENAME_82="cssparser-0.38.0.tar.gz"
257 +
SOURCE_URI_83="https://static.crates.io/crates/ctutils/ctutils-0.4.2.crate"
258 +
CHECKSUM_SHA256_83="7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
259 +
SOURCE_FILENAME_83="ctutils-0.4.2.tar.gz"
260 +
SOURCE_URI_84="https://static.crates.io/crates/curve25519-dalek/curve25519-dalek-5.0.0.crate"
261 +
CHECKSUM_SHA256_84="b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
262 +
SOURCE_FILENAME_84="curve25519-dalek-5.0.0.tar.gz"
263 +
SOURCE_URI_85="https://static.crates.io/crates/curve25519-dalek-derive/curve25519-dalek-derive-0.1.1.crate"
264 +
CHECKSUM_SHA256_85="f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
265 +
SOURCE_FILENAME_85="curve25519-dalek-derive-0.1.1.tar.gz"
266 +
SOURCE_URI_86="https://static.crates.io/crates/data-encoding/data-encoding-2.11.1.crate"
267 +
CHECKSUM_SHA256_86="4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
268 +
SOURCE_FILENAME_86="data-encoding-2.11.1.tar.gz"
269 +
SOURCE_URI_87="https://static.crates.io/crates/der/der-0.7.10.crate"
270 +
CHECKSUM_SHA256_87="e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
271 +
SOURCE_FILENAME_87="der-0.7.10.tar.gz"
272 +
SOURCE_URI_88="https://static.crates.io/crates/der/der-0.8.2.crate"
273 +
CHECKSUM_SHA256_88="a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a"
274 +
SOURCE_FILENAME_88="der-0.8.2.tar.gz"
275 +
SOURCE_URI_89="https://static.crates.io/crates/der-parser/der-parser-10.0.0.crate"
276 +
CHECKSUM_SHA256_89="07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
277 +
SOURCE_FILENAME_89="der-parser-10.0.0.tar.gz"
278 +
SOURCE_URI_90="https://static.crates.io/crates/deranged/deranged-0.5.8.crate"
279 +
CHECKSUM_SHA256_90="7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
280 +
SOURCE_FILENAME_90="deranged-0.5.8.tar.gz"
281 +
SOURCE_URI_91="https://static.crates.io/crates/derive_arbitrary/derive_arbitrary-1.4.2.crate"
282 +
CHECKSUM_SHA256_91="1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
283 +
SOURCE_FILENAME_91="derive_arbitrary-1.4.2.tar.gz"
284 +
SOURCE_URI_92="https://static.crates.io/crates/digest/digest-0.10.7.crate"
285 +
CHECKSUM_SHA256_92="9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
286 +
SOURCE_FILENAME_92="digest-0.10.7.tar.gz"
287 +
SOURCE_URI_93="https://static.crates.io/crates/digest/digest-0.11.3.crate"
288 +
CHECKSUM_SHA256_93="f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
289 +
SOURCE_FILENAME_93="digest-0.11.3.tar.gz"
290 +
SOURCE_URI_94="https://static.crates.io/crates/displaydoc/displaydoc-0.2.7.crate"
291 +
CHECKSUM_SHA256_94="c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
292 +
SOURCE_FILENAME_94="displaydoc-0.2.7.tar.gz"
293 +
SOURCE_URI_95="https://static.crates.io/crates/dotenvy/dotenvy-0.15.7.crate"
294 +
CHECKSUM_SHA256_95="1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
295 +
SOURCE_FILENAME_95="dotenvy-0.15.7.tar.gz"
296 +
SOURCE_URI_96="https://static.crates.io/crates/dtoa/dtoa-1.0.11.crate"
297 +
CHECKSUM_SHA256_96="4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
298 +
SOURCE_FILENAME_96="dtoa-1.0.11.tar.gz"
299 +
SOURCE_URI_97="https://static.crates.io/crates/dtoa-short/dtoa-short-0.3.5.crate"
300 +
CHECKSUM_SHA256_97="cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
301 +
SOURCE_FILENAME_97="dtoa-short-0.3.5.tar.gz"
302 +
SOURCE_URI_98="https://static.crates.io/crates/dyn-clone/dyn-clone-1.0.20.crate"
303 +
CHECKSUM_SHA256_98="d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
304 +
SOURCE_FILENAME_98="dyn-clone-1.0.20.tar.gz"
305 +
SOURCE_URI_99="https://static.crates.io/crates/ed25519/ed25519-3.0.0.crate"
306 +
CHECKSUM_SHA256_99="29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"
307 +
SOURCE_FILENAME_99="ed25519-3.0.0.tar.gz"
308 +
SOURCE_URI_100="https://static.crates.io/crates/ed25519-dalek/ed25519-dalek-3.0.0.crate"
309 +
CHECKSUM_SHA256_100="6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
310 +
SOURCE_FILENAME_100="ed25519-dalek-3.0.0.tar.gz"
311 +
SOURCE_URI_101="https://static.crates.io/crates/either/either-1.18.0.crate"
312 +
CHECKSUM_SHA256_101="252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
313 +
SOURCE_FILENAME_101="either-1.18.0.tar.gz"
314 +
SOURCE_URI_102="https://static.crates.io/crates/email-encoding/email-encoding-0.4.2.crate"
315 +
CHECKSUM_SHA256_102="420b9da095f052ea597503e39073b5b3c522f7db933fbac202d91d24492693fd"
316 +
SOURCE_FILENAME_102="email-encoding-0.4.2.tar.gz"
317 +
SOURCE_URI_103="https://static.crates.io/crates/email_address/email_address-0.2.9.crate"
318 +
CHECKSUM_SHA256_103="e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
319 +
SOURCE_FILENAME_103="email_address-0.2.9.tar.gz"
320 +
SOURCE_URI_104="https://static.crates.io/crates/encoding_rs/encoding_rs-0.8.41.crate"
321 +
CHECKSUM_SHA256_104="7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a"
322 +
SOURCE_FILENAME_104="encoding_rs-0.8.41.tar.gz"
323 +
SOURCE_URI_105="https://static.crates.io/crates/equivalent/equivalent-1.0.2.crate"
324 +
CHECKSUM_SHA256_105="877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
325 +
SOURCE_FILENAME_105="equivalent-1.0.2.tar.gz"
326 +
SOURCE_URI_106="https://static.crates.io/crates/errno/errno-0.3.14.crate"
327 +
CHECKSUM_SHA256_106="39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
328 +
SOURCE_FILENAME_106="errno-0.3.14.tar.gz"
329 +
SOURCE_URI_107="https://static.crates.io/crates/etcetera/etcetera-0.8.0.crate"
330 +
CHECKSUM_SHA256_107="136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
331 +
SOURCE_FILENAME_107="etcetera-0.8.0.tar.gz"
332 +
SOURCE_URI_108="https://static.crates.io/crates/event-listener/event-listener-5.4.2.crate"
333 +
CHECKSUM_SHA256_108="5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
334 +
SOURCE_FILENAME_108="event-listener-5.4.2.tar.gz"
335 +
SOURCE_URI_109="https://static.crates.io/crates/fancy-regex/fancy-regex-0.16.2.crate"
336 +
CHECKSUM_SHA256_109="998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
337 +
SOURCE_FILENAME_109="fancy-regex-0.16.2.tar.gz"
338 +
SOURCE_URI_110="https://static.crates.io/crates/fastrand/fastrand-2.5.0.crate"
339 +
CHECKSUM_SHA256_110="da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
340 +
SOURCE_FILENAME_110="fastrand-2.5.0.tar.gz"
341 +
SOURCE_URI_111="https://static.crates.io/crates/fdeflate/fdeflate-0.3.7.crate"
342 +
CHECKSUM_SHA256_111="1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
343 +
SOURCE_FILENAME_111="fdeflate-0.3.7.tar.gz"
344 +
SOURCE_URI_112="https://static.crates.io/crates/fiat-crypto/fiat-crypto-0.3.0.crate"
345 +
CHECKSUM_SHA256_112="64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
346 +
SOURCE_FILENAME_112="fiat-crypto-0.3.0.tar.gz"
347 +
SOURCE_URI_113="https://static.crates.io/crates/filetime/filetime-0.2.29.crate"
348 +
CHECKSUM_SHA256_113="5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
349 +
SOURCE_FILENAME_113="filetime-0.2.29.tar.gz"
350 +
SOURCE_URI_114="https://static.crates.io/crates/find-msvc-tools/find-msvc-tools-0.1.14.crate"
351 +
CHECKSUM_SHA256_114="aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
352 +
SOURCE_FILENAME_114="find-msvc-tools-0.1.14.tar.gz"
353 +
SOURCE_URI_115="https://static.crates.io/crates/flate2/flate2-1.1.10.crate"
354 +
CHECKSUM_SHA256_115="6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
355 +
SOURCE_FILENAME_115="flate2-1.1.10.tar.gz"
356 +
SOURCE_URI_116="https://static.crates.io/crates/flume/flume-0.11.1.crate"
357 +
CHECKSUM_SHA256_116="da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
358 +
SOURCE_FILENAME_116="flume-0.11.1.tar.gz"
359 +
SOURCE_URI_117="https://static.crates.io/crates/fnv/fnv-1.0.7.crate"
360 +
CHECKSUM_SHA256_117="3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
361 +
SOURCE_FILENAME_117="fnv-1.0.7.tar.gz"
362 +
SOURCE_URI_118="https://static.crates.io/crates/foldhash/foldhash-0.1.5.crate"
363 +
CHECKSUM_SHA256_118="d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
364 +
SOURCE_FILENAME_118="foldhash-0.1.5.tar.gz"
365 +
SOURCE_URI_119="https://static.crates.io/crates/form_urlencoded/form_urlencoded-1.2.2.crate"
366 +
CHECKSUM_SHA256_119="cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
367 +
SOURCE_FILENAME_119="form_urlencoded-1.2.2.tar.gz"
368 +
SOURCE_URI_120="https://static.crates.io/crates/fs-err/fs-err-3.3.1.crate"
369 +
CHECKSUM_SHA256_120="b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a"
370 +
SOURCE_FILENAME_120="fs-err-3.3.1.tar.gz"
371 +
SOURCE_URI_121="https://static.crates.io/crates/futures/futures-0.3.34.crate"
372 +
CHECKSUM_SHA256_121="9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
373 +
SOURCE_FILENAME_121="futures-0.3.34.tar.gz"
374 +
SOURCE_URI_122="https://static.crates.io/crates/futures-channel/futures-channel-0.3.34.crate"
375 +
CHECKSUM_SHA256_122="b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
376 +
SOURCE_FILENAME_122="futures-channel-0.3.34.tar.gz"
377 +
SOURCE_URI_123="https://static.crates.io/crates/futures-core/futures-core-0.3.34.crate"
378 +
CHECKSUM_SHA256_123="92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
379 +
SOURCE_FILENAME_123="futures-core-0.3.34.tar.gz"
380 +
SOURCE_URI_124="https://static.crates.io/crates/futures-executor/futures-executor-0.3.34.crate"
381 +
CHECKSUM_SHA256_124="031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
382 +
SOURCE_FILENAME_124="futures-executor-0.3.34.tar.gz"
383 +
SOURCE_URI_125="https://static.crates.io/crates/futures-intrusive/futures-intrusive-0.5.0.crate"
384 +
CHECKSUM_SHA256_125="1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
385 +
SOURCE_FILENAME_125="futures-intrusive-0.5.0.tar.gz"
386 +
SOURCE_URI_126="https://static.crates.io/crates/futures-io/futures-io-0.3.34.crate"
387 +
CHECKSUM_SHA256_126="53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
388 +
SOURCE_FILENAME_126="futures-io-0.3.34.tar.gz"
389 +
SOURCE_URI_127="https://static.crates.io/crates/futures-macro/futures-macro-0.3.34.crate"
390 +
CHECKSUM_SHA256_127="9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
391 +
SOURCE_FILENAME_127="futures-macro-0.3.34.tar.gz"
392 +
SOURCE_URI_128="https://static.crates.io/crates/futures-sink/futures-sink-0.3.34.crate"
393 +
CHECKSUM_SHA256_128="1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
394 +
SOURCE_FILENAME_128="futures-sink-0.3.34.tar.gz"
395 +
SOURCE_URI_129="https://static.crates.io/crates/futures-task/futures-task-0.3.34.crate"
396 +
CHECKSUM_SHA256_129="cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
397 +
SOURCE_FILENAME_129="futures-task-0.3.34.tar.gz"
398 +
SOURCE_URI_130="https://static.crates.io/crates/futures-util/futures-util-0.3.34.crate"
399 +
CHECKSUM_SHA256_130="0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
400 +
SOURCE_FILENAME_130="futures-util-0.3.34.tar.gz"
401 +
SOURCE_URI_131="https://static.crates.io/crates/generic-array/generic-array-0.14.7.crate"
402 +
CHECKSUM_SHA256_131="85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
403 +
SOURCE_FILENAME_131="generic-array-0.14.7.tar.gz"
404 +
SOURCE_URI_132="https://static.crates.io/crates/getrandom/getrandom-0.2.17.crate"
405 +
CHECKSUM_SHA256_132="ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
406 +
SOURCE_FILENAME_132="getrandom-0.2.17.tar.gz"
407 +
SOURCE_URI_133="https://static.crates.io/crates/getrandom/getrandom-0.4.3.crate"
408 +
CHECKSUM_SHA256_133="300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
409 +
SOURCE_FILENAME_133="getrandom-0.4.3.tar.gz"
410 +
SOURCE_URI_134="https://static.crates.io/crates/globset/globset-0.4.20.crate"
411 +
CHECKSUM_SHA256_134="07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
412 +
SOURCE_FILENAME_134="globset-0.4.20.tar.gz"
413 +
SOURCE_URI_135="https://static.crates.io/crates/h2/h2-0.4.19.crate"
414 +
CHECKSUM_SHA256_135="ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
415 +
SOURCE_FILENAME_135="h2-0.4.19.tar.gz"
416 +
SOURCE_URI_136="https://static.crates.io/crates/hashbrown/hashbrown-0.15.5.crate"
417 +
CHECKSUM_SHA256_136="9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
418 +
SOURCE_FILENAME_136="hashbrown-0.15.5.tar.gz"
419 +
SOURCE_URI_137="https://static.crates.io/crates/hashbrown/hashbrown-0.17.1.crate"
420 +
CHECKSUM_SHA256_137="ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
421 +
SOURCE_FILENAME_137="hashbrown-0.17.1.tar.gz"
422 +
SOURCE_URI_138="https://static.crates.io/crates/hashlink/hashlink-0.10.0.crate"
423 +
CHECKSUM_SHA256_138="7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
424 +
SOURCE_FILENAME_138="hashlink-0.10.0.tar.gz"
425 +
SOURCE_URI_139="https://static.crates.io/crates/heck/heck-0.5.0.crate"
426 +
CHECKSUM_SHA256_139="2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
427 +
SOURCE_FILENAME_139="heck-0.5.0.tar.gz"
428 +
SOURCE_URI_140="https://static.crates.io/crates/hex/hex-0.4.3.crate"
429 +
CHECKSUM_SHA256_140="7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
430 +
SOURCE_FILENAME_140="hex-0.4.3.tar.gz"
431 +
SOURCE_URI_141="https://static.crates.io/crates/hkdf/hkdf-0.12.4.crate"
432 +
CHECKSUM_SHA256_141="7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
433 +
SOURCE_FILENAME_141="hkdf-0.12.4.tar.gz"
434 +
SOURCE_URI_142="https://static.crates.io/crates/hmac/hmac-0.12.1.crate"
435 +
CHECKSUM_SHA256_142="6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
436 +
SOURCE_FILENAME_142="hmac-0.12.1.tar.gz"
437 +
SOURCE_URI_143="https://static.crates.io/crates/home/home-0.5.12.crate"
438 +
CHECKSUM_SHA256_143="cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
439 +
SOURCE_FILENAME_143="home-0.5.12.tar.gz"
440 +
SOURCE_URI_144="https://static.crates.io/crates/html5ever/html5ever-0.40.1.crate"
441 +
CHECKSUM_SHA256_144="456a1a377e608e555d22ddab27ac0114bc7a7b4199078108e34c2aeae6c9b130"
442 +
SOURCE_FILENAME_144="html5ever-0.40.1.tar.gz"
443 +
SOURCE_URI_145="https://static.crates.io/crates/http/http-1.5.0.crate"
444 +
CHECKSUM_SHA256_145="918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
445 +
SOURCE_FILENAME_145="http-1.5.0.tar.gz"
446 +
SOURCE_URI_146="https://static.crates.io/crates/http-body/http-body-1.1.0.crate"
447 +
CHECKSUM_SHA256_146="ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
448 +
SOURCE_FILENAME_146="http-body-1.1.0.tar.gz"
449 +
SOURCE_URI_147="https://static.crates.io/crates/http-body-util/http-body-util-0.1.5.crate"
450 +
CHECKSUM_SHA256_147="23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
451 +
SOURCE_FILENAME_147="http-body-util-0.1.5.tar.gz"
452 +
SOURCE_URI_148="https://static.crates.io/crates/httparse/httparse-1.10.1.crate"
453 +
CHECKSUM_SHA256_148="6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
454 +
SOURCE_FILENAME_148="httparse-1.10.1.tar.gz"
455 +
SOURCE_URI_149="https://static.crates.io/crates/httpdate/httpdate-1.0.3.crate"
456 +
CHECKSUM_SHA256_149="df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
457 +
SOURCE_FILENAME_149="httpdate-1.0.3.tar.gz"
458 +
SOURCE_URI_150="https://static.crates.io/crates/humantime/humantime-2.4.0.crate"
459 +
CHECKSUM_SHA256_150="15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
460 +
SOURCE_FILENAME_150="humantime-2.4.0.tar.gz"
461 +
SOURCE_URI_151="https://static.crates.io/crates/hybrid-array/hybrid-array-0.4.15.crate"
462 +
CHECKSUM_SHA256_151="27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
463 +
SOURCE_FILENAME_151="hybrid-array-0.4.15.tar.gz"
464 +
SOURCE_URI_152="https://static.crates.io/crates/hyper/hyper-1.11.1.crate"
465 +
CHECKSUM_SHA256_152="27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
466 +
SOURCE_FILENAME_152="hyper-1.11.1.tar.gz"
467 +
SOURCE_URI_153="https://static.crates.io/crates/hyper-rustls/hyper-rustls-0.27.10.crate"
468 +
CHECKSUM_SHA256_153="dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
469 +
SOURCE_FILENAME_153="hyper-rustls-0.27.10.tar.gz"
470 +
SOURCE_URI_154="https://static.crates.io/crates/hyper-util/hyper-util-0.1.21.crate"
471 +
CHECKSUM_SHA256_154="ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
472 +
SOURCE_FILENAME_154="hyper-util-0.1.21.tar.gz"
473 +
SOURCE_URI_155="https://static.crates.io/crates/iana-time-zone-haiku/iana-time-zone-haiku-0.1.2.crate"
474 +
CHECKSUM_SHA256_155="f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
475 +
SOURCE_FILENAME_155="iana-time-zone-haiku-0.1.2.tar.gz"
476 +
SOURCE_URI_156="https://static.crates.io/crates/icu_collections/icu_collections-2.3.0.crate"
477 +
CHECKSUM_SHA256_156="fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
478 +
SOURCE_FILENAME_156="icu_collections-2.3.0.tar.gz"
479 +
SOURCE_URI_157="https://static.crates.io/crates/icu_locale_core/icu_locale_core-2.3.0.crate"
480 +
CHECKSUM_SHA256_157="d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
481 +
SOURCE_FILENAME_157="icu_locale_core-2.3.0.tar.gz"
482 +
SOURCE_URI_158="https://static.crates.io/crates/icu_normalizer/icu_normalizer-2.3.0.crate"
483 +
CHECKSUM_SHA256_158="12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
484 +
SOURCE_FILENAME_158="icu_normalizer-2.3.0.tar.gz"
485 +
SOURCE_URI_159="https://static.crates.io/crates/icu_normalizer_data/icu_normalizer_data-2.3.0.crate"
486 +
CHECKSUM_SHA256_159="1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
487 +
SOURCE_FILENAME_159="icu_normalizer_data-2.3.0.tar.gz"
488 +
SOURCE_URI_160="https://static.crates.io/crates/icu_properties/icu_properties-2.3.0.crate"
489 +
CHECKSUM_SHA256_160="7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
490 +
SOURCE_FILENAME_160="icu_properties-2.3.0.tar.gz"
491 +
SOURCE_URI_161="https://static.crates.io/crates/icu_properties_data/icu_properties_data-2.3.0.crate"
492 +
CHECKSUM_SHA256_161="e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
493 +
SOURCE_FILENAME_161="icu_properties_data-2.3.0.tar.gz"
494 +
SOURCE_URI_162="https://static.crates.io/crates/icu_provider/icu_provider-2.3.1.crate"
495 +
CHECKSUM_SHA256_162="d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
496 +
SOURCE_FILENAME_162="icu_provider-2.3.1.tar.gz"
497 +
SOURCE_URI_163="https://static.crates.io/crates/idna/idna-1.1.0.crate"
498 +
CHECKSUM_SHA256_163="3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
499 +
SOURCE_FILENAME_163="idna-1.1.0.tar.gz"
500 +
SOURCE_URI_164="https://static.crates.io/crates/idna_adapter/idna_adapter-1.2.2.crate"
501 +
CHECKSUM_SHA256_164="cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
502 +
SOURCE_FILENAME_164="idna_adapter-1.2.2.tar.gz"
503 +
SOURCE_URI_165="https://static.crates.io/crates/ignore/ignore-0.4.33.crate"
504 +
CHECKSUM_SHA256_165="00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f"
505 +
SOURCE_FILENAME_165="ignore-0.4.33.tar.gz"
506 +
SOURCE_URI_166="https://static.crates.io/crates/image/image-0.25.10.crate"
507 +
CHECKSUM_SHA256_166="85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
508 +
SOURCE_FILENAME_166="image-0.25.10.tar.gz"
509 +
SOURCE_URI_167="https://static.crates.io/crates/image-webp/image-webp-0.2.4.crate"
510 +
CHECKSUM_SHA256_167="525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
511 +
SOURCE_FILENAME_167="image-webp-0.2.4.tar.gz"
512 +
SOURCE_URI_168="https://static.crates.io/crates/indexmap/indexmap-2.14.2.crate"
513 +
CHECKSUM_SHA256_168="cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
514 +
SOURCE_FILENAME_168="indexmap-2.14.2.tar.gz"
515 +
SOURCE_URI_169="https://static.crates.io/crates/ipnet/ipnet-2.12.2.crate"
516 +
CHECKSUM_SHA256_169="791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
517 +
SOURCE_FILENAME_169="ipnet-2.12.2.tar.gz"
518 +
SOURCE_URI_170="https://static.crates.io/crates/is_terminal_polyfill/is_terminal_polyfill-1.70.2.crate"
519 +
CHECKSUM_SHA256_170="a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
520 +
SOURCE_FILENAME_170="is_terminal_polyfill-1.70.2.tar.gz"
521 +
SOURCE_URI_171="https://static.crates.io/crates/itertools/itertools-0.15.0.crate"
522 +
CHECKSUM_SHA256_171="8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
523 +
SOURCE_FILENAME_171="itertools-0.15.0.tar.gz"
524 +
SOURCE_URI_172="https://static.crates.io/crates/itoa/itoa-1.0.18.crate"
525 +
CHECKSUM_SHA256_172="8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
526 +
SOURCE_FILENAME_172="itoa-1.0.18.tar.gz"
527 +
SOURCE_URI_173="https://static.crates.io/crates/jobserver/jobserver-0.1.35.crate"
528 +
CHECKSUM_SHA256_173="1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
529 +
SOURCE_FILENAME_173="jobserver-0.1.35.tar.gz"
530 +
SOURCE_URI_174="https://static.crates.io/crates/js-sys/js-sys-0.3.106.crate"
531 +
CHECKSUM_SHA256_174="7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
532 +
SOURCE_FILENAME_174="js-sys-0.3.106.tar.gz"
533 +
SOURCE_URI_175="https://static.crates.io/crates/kdl/kdl-6.7.1.crate"
534 +
CHECKSUM_SHA256_175="082ddf81b2acd76fe04412655d17befedfff1837db772f8e74c38050d25ed670"
535 +
SOURCE_FILENAME_175="kdl-6.7.1.tar.gz"
536 +
SOURCE_URI_176="https://static.crates.io/crates/keccak/keccak-0.2.2.crate"
537 +
CHECKSUM_SHA256_176="d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598"
538 +
SOURCE_FILENAME_176="keccak-0.2.2.tar.gz"
539 +
SOURCE_URI_177="https://static.crates.io/crates/lazy_static/lazy_static-1.5.0.crate"
540 +
CHECKSUM_SHA256_177="bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
541 +
SOURCE_FILENAME_177="lazy_static-1.5.0.tar.gz"
542 +
SOURCE_URI_178="https://static.crates.io/crates/lettre/lettre-0.11.23.crate"
543 +
CHECKSUM_SHA256_178="f2c646bd5cc763b1087b15493e29a64be6147ba8f19342004fa52048ee596eae"
544 +
SOURCE_FILENAME_178="lettre-0.11.23.tar.gz"
545 +
SOURCE_URI_179="https://static.crates.io/crates/libc/libc-0.2.189.crate"
546 +
CHECKSUM_SHA256_179="3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
547 +
SOURCE_FILENAME_179="libc-0.2.189.tar.gz"
548 +
SOURCE_URI_180="https://static.crates.io/crates/libm/libm-0.2.16.crate"
549 +
CHECKSUM_SHA256_180="b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
550 +
SOURCE_FILENAME_180="libm-0.2.16.tar.gz"
551 +
SOURCE_URI_181="https://static.crates.io/crates/libredox/libredox-0.1.25.crate"
552 +
CHECKSUM_SHA256_181="61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
553 +
SOURCE_FILENAME_181="libredox-0.1.25.tar.gz"
554 +
SOURCE_URI_182="https://static.crates.io/crates/libsqlite3-sys/libsqlite3-sys-0.30.1.crate"
555 +
CHECKSUM_SHA256_182="2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
556 +
SOURCE_FILENAME_182="libsqlite3-sys-0.30.1.tar.gz"
557 +
SOURCE_URI_183="https://static.crates.io/crates/linked-hash-map/linked-hash-map-0.5.6.crate"
558 +
CHECKSUM_SHA256_183="0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
559 +
SOURCE_FILENAME_183="linked-hash-map-0.5.6.tar.gz"
560 +
SOURCE_URI_184="https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.12.1.crate"
561 +
CHECKSUM_SHA256_184="32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
562 +
SOURCE_FILENAME_184="linux-raw-sys-0.12.1.tar.gz"
563 +
SOURCE_URI_185="https://static.crates.io/crates/litemap/litemap-0.8.3.crate"
564 +
CHECKSUM_SHA256_185="47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
565 +
SOURCE_FILENAME_185="litemap-0.8.3.tar.gz"
566 +
SOURCE_URI_186="https://static.crates.io/crates/lock_api/lock_api-0.4.14.crate"
567 +
CHECKSUM_SHA256_186="224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
568 +
SOURCE_FILENAME_186="lock_api-0.4.14.tar.gz"
569 +
SOURCE_URI_187="https://static.crates.io/crates/log/log-0.4.34.crate"
570 +
CHECKSUM_SHA256_187="f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
571 +
SOURCE_FILENAME_187="log-0.4.34.tar.gz"
572 +
SOURCE_URI_188="https://static.crates.io/crates/lru-slab/lru-slab-0.1.3.crate"
573 +
CHECKSUM_SHA256_188="4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
574 +
SOURCE_FILENAME_188="lru-slab-0.1.3.tar.gz"
575 +
SOURCE_URI_189="https://static.crates.io/crates/lzma-rs/lzma-rs-0.3.0.crate"
576 +
CHECKSUM_SHA256_189="297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
577 +
SOURCE_FILENAME_189="lzma-rs-0.3.0.tar.gz"
578 +
SOURCE_URI_190="https://static.crates.io/crates/maplit/maplit-1.0.2.crate"
579 +
CHECKSUM_SHA256_190="3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
580 +
SOURCE_FILENAME_190="maplit-1.0.2.tar.gz"
581 +
SOURCE_URI_191="https://static.crates.io/crates/markup5ever/markup5ever-0.40.0.crate"
582 +
CHECKSUM_SHA256_191="0ab3dc68ac4a0f5719e560136778c1ee716e296030d75dbd4484e37e39e3a842"
583 +
SOURCE_FILENAME_191="markup5ever-0.40.0.tar.gz"
584 +
SOURCE_URI_192="https://static.crates.io/crates/matchers/matchers-0.2.0.crate"
585 +
CHECKSUM_SHA256_192="d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
586 +
SOURCE_FILENAME_192="matchers-0.2.0.tar.gz"
587 +
SOURCE_URI_193="https://static.crates.io/crates/matchit/matchit-0.8.4.crate"
588 +
CHECKSUM_SHA256_193="47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
589 +
SOURCE_FILENAME_193="matchit-0.8.4.tar.gz"
590 +
SOURCE_URI_194="https://static.crates.io/crates/md-5/md-5-0.10.6.crate"
591 +
CHECKSUM_SHA256_194="d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
592 +
SOURCE_FILENAME_194="md-5-0.10.6.tar.gz"
593 +
SOURCE_URI_195="https://static.crates.io/crates/md-5/md-5-0.11.0.crate"
594 +
CHECKSUM_SHA256_195="69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
595 +
SOURCE_FILENAME_195="md-5-0.11.0.tar.gz"
596 +
SOURCE_URI_196="https://static.crates.io/crates/memchr/memchr-2.8.3.crate"
597 +
CHECKSUM_SHA256_196="cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
598 +
SOURCE_FILENAME_196="memchr-2.8.3.tar.gz"
599 +
SOURCE_URI_197="https://static.crates.io/crates/miette/miette-7.6.0.crate"
600 +
CHECKSUM_SHA256_197="5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
601 +
SOURCE_FILENAME_197="miette-7.6.0.tar.gz"
602 +
SOURCE_URI_198="https://static.crates.io/crates/miette-derive/miette-derive-7.6.0.crate"
603 +
CHECKSUM_SHA256_198="db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
604 +
SOURCE_FILENAME_198="miette-derive-7.6.0.tar.gz"
605 +
SOURCE_URI_199="https://static.crates.io/crates/mime/mime-0.3.17.crate"
606 +
CHECKSUM_SHA256_199="6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
607 +
SOURCE_FILENAME_199="mime-0.3.17.tar.gz"
608 +
SOURCE_URI_200="https://static.crates.io/crates/minimal-lexical/minimal-lexical-0.2.1.crate"
609 +
CHECKSUM_SHA256_200="68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
610 +
SOURCE_FILENAME_200="minimal-lexical-0.2.1.tar.gz"
611 +
SOURCE_URI_201="https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.8.9.crate"
612 +
CHECKSUM_SHA256_201="1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
613 +
SOURCE_FILENAME_201="miniz_oxide-0.8.9.tar.gz"
614 +
SOURCE_URI_202="https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.9.1.crate"
615 +
CHECKSUM_SHA256_202="b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
616 +
SOURCE_FILENAME_202="miniz_oxide-0.9.1.tar.gz"
617 +
SOURCE_URI_203="https://static.crates.io/crates/mio/mio-1.2.3.crate"
618 +
CHECKSUM_SHA256_203="4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
619 +
SOURCE_FILENAME_203="mio-1.2.3.tar.gz"
620 +
SOURCE_URI_204="https://static.crates.io/crates/ml-dsa/ml-dsa-0.1.1.crate"
621 +
CHECKSUM_SHA256_204="add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d"
622 +
SOURCE_FILENAME_204="ml-dsa-0.1.1.tar.gz"
623 +
SOURCE_URI_205="https://static.crates.io/crates/module-lattice/module-lattice-0.2.3.crate"
624 +
CHECKSUM_SHA256_205="0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe"
625 +
SOURCE_FILENAME_205="module-lattice-0.2.3.tar.gz"
626 +
SOURCE_URI_206="https://static.crates.io/crates/moxcms/moxcms-0.8.1.crate"
627 +
CHECKSUM_SHA256_206="bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
628 +
SOURCE_FILENAME_206="moxcms-0.8.1.tar.gz"
629 +
SOURCE_URI_207="https://static.crates.io/crates/multer/multer-3.1.0.crate"
630 +
CHECKSUM_SHA256_207="83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
631 +
SOURCE_FILENAME_207="multer-3.1.0.tar.gz"
632 +
SOURCE_URI_208="https://static.crates.io/crates/multiversion/multiversion-0.9.0.crate"
633 +
CHECKSUM_SHA256_208="b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c"
634 +
SOURCE_FILENAME_208="multiversion-0.9.0.tar.gz"
635 +
SOURCE_URI_209="https://static.crates.io/crates/multiversion-macros/multiversion-macros-0.9.0.crate"
636 +
CHECKSUM_SHA256_209="0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83"
637 +
SOURCE_FILENAME_209="multiversion-macros-0.9.0.tar.gz"
638 +
SOURCE_URI_210="https://static.crates.io/crates/multiversion_no_op/multiversion_no_op-1.0.0.crate"
639 +
CHECKSUM_SHA256_210="743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
640 +
SOURCE_FILENAME_210="multiversion_no_op-1.0.0.tar.gz"
641 +
SOURCE_URI_211="https://static.crates.io/crates/new_debug_unreachable/new_debug_unreachable-1.0.6.crate"
642 +
CHECKSUM_SHA256_211="650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
643 +
SOURCE_FILENAME_211="new_debug_unreachable-1.0.6.tar.gz"
644 +
SOURCE_URI_212="https://static.crates.io/crates/nom/nom-7.1.3.crate"
645 +
CHECKSUM_SHA256_212="d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
646 +
SOURCE_FILENAME_212="nom-7.1.3.tar.gz"
647 +
SOURCE_URI_213="https://static.crates.io/crates/nom/nom-8.0.0.crate"
648 +
CHECKSUM_SHA256_213="df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
649 +
SOURCE_FILENAME_213="nom-8.0.0.tar.gz"
650 +
SOURCE_URI_214="https://static.crates.io/crates/nu-ansi-term/nu-ansi-term-0.50.3.crate"
651 +
CHECKSUM_SHA256_214="7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
652 +
SOURCE_FILENAME_214="nu-ansi-term-0.50.3.tar.gz"
653 +
SOURCE_URI_215="https://static.crates.io/crates/num-bigint/num-bigint-0.4.8.crate"
654 +
CHECKSUM_SHA256_215="c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
655 +
SOURCE_FILENAME_215="num-bigint-0.4.8.tar.gz"
656 +
SOURCE_URI_216="https://static.crates.io/crates/num-bigint-dig/num-bigint-dig-0.8.6.crate"
657 +
CHECKSUM_SHA256_216="e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
658 +
SOURCE_FILENAME_216="num-bigint-dig-0.8.6.tar.gz"
659 +
SOURCE_URI_217="https://static.crates.io/crates/num-conv/num-conv-0.2.2.crate"
660 +
CHECKSUM_SHA256_217="521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
661 +
SOURCE_FILENAME_217="num-conv-0.2.2.tar.gz"
662 +
SOURCE_URI_218="https://static.crates.io/crates/num-integer/num-integer-0.1.47.crate"
663 +
CHECKSUM_SHA256_218="7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
664 +
SOURCE_FILENAME_218="num-integer-0.1.47.tar.gz"
665 +
SOURCE_URI_219="https://static.crates.io/crates/num-iter/num-iter-0.1.46.crate"
666 +
CHECKSUM_SHA256_219="c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
667 +
SOURCE_FILENAME_219="num-iter-0.1.46.tar.gz"
668 +
SOURCE_URI_220="https://static.crates.io/crates/num-traits/num-traits-0.2.19.crate"
669 +
CHECKSUM_SHA256_220="071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
670 +
SOURCE_FILENAME_220="num-traits-0.2.19.tar.gz"
671 +
SOURCE_URI_221="https://static.crates.io/crates/object_store/object_store-0.14.2.crate"
672 +
CHECKSUM_SHA256_221="f1796bc93603f78c5760a69f2d58badc9618d22adade0a95385bb2adbae4eb94"
673 +
SOURCE_FILENAME_221="object_store-0.14.2.tar.gz"
674 +
SOURCE_URI_222="https://static.crates.io/crates/oid-registry/oid-registry-0.8.1.crate"
675 +
CHECKSUM_SHA256_222="12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
676 +
SOURCE_FILENAME_222="oid-registry-0.8.1.tar.gz"
677 +
SOURCE_URI_223="https://static.crates.io/crates/once_cell/once_cell-1.21.4.crate"
678 +
CHECKSUM_SHA256_223="9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
679 +
SOURCE_FILENAME_223="once_cell-1.21.4.tar.gz"
680 +
SOURCE_URI_224="https://static.crates.io/crates/once_cell_polyfill/once_cell_polyfill-1.70.2.crate"
681 +
CHECKSUM_SHA256_224="384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
682 +
SOURCE_FILENAME_224="once_cell_polyfill-1.70.2.tar.gz"
683 +
SOURCE_URI_225="https://static.crates.io/crates/parking/parking-2.2.1.crate"
684 +
CHECKSUM_SHA256_225="f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
685 +
SOURCE_FILENAME_225="parking-2.2.1.tar.gz"
686 +
SOURCE_URI_226="https://static.crates.io/crates/parking_lot/parking_lot-0.12.5.crate"
687 +
CHECKSUM_SHA256_226="93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
688 +
SOURCE_FILENAME_226="parking_lot-0.12.5.tar.gz"
689 +
SOURCE_URI_227="https://static.crates.io/crates/parking_lot_core/parking_lot_core-0.9.12.crate"
690 +
CHECKSUM_SHA256_227="2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
691 +
SOURCE_FILENAME_227="parking_lot_core-0.9.12.tar.gz"
692 +
SOURCE_URI_228="https://static.crates.io/crates/password-hash/password-hash-0.5.0.crate"
693 +
CHECKSUM_SHA256_228="346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
694 +
SOURCE_FILENAME_228="password-hash-0.5.0.tar.gz"
695 +
SOURCE_URI_229="https://static.crates.io/crates/pem/pem-4.0.0.crate"
696 +
CHECKSUM_SHA256_229="d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
697 +
SOURCE_FILENAME_229="pem-4.0.0.tar.gz"
698 +
SOURCE_URI_230="https://static.crates.io/crates/pem-rfc7468/pem-rfc7468-0.7.0.crate"
699 +
CHECKSUM_SHA256_230="88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
700 +
SOURCE_FILENAME_230="pem-rfc7468-0.7.0.tar.gz"
701 +
SOURCE_URI_231="https://static.crates.io/crates/percent-encoding/percent-encoding-2.3.2.crate"
702 +
CHECKSUM_SHA256_231="9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
703 +
SOURCE_FILENAME_231="percent-encoding-2.3.2.tar.gz"
704 +
SOURCE_URI_232="https://static.crates.io/crates/phf/phf-0.14.0.crate"
705 +
CHECKSUM_SHA256_232="010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
706 +
SOURCE_FILENAME_232="phf-0.14.0.tar.gz"
707 +
SOURCE_URI_233="https://static.crates.io/crates/phf_codegen/phf_codegen-0.14.0.crate"
708 +
CHECKSUM_SHA256_233="41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe"
709 +
SOURCE_FILENAME_233="phf_codegen-0.14.0.tar.gz"
710 +
SOURCE_URI_234="https://static.crates.io/crates/phf_generator/phf_generator-0.14.0.crate"
711 +
CHECKSUM_SHA256_234="aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
712 +
SOURCE_FILENAME_234="phf_generator-0.14.0.tar.gz"
713 +
SOURCE_URI_235="https://static.crates.io/crates/phf_shared/phf_shared-0.14.0.crate"
714 +
CHECKSUM_SHA256_235="c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
715 +
SOURCE_FILENAME_235="phf_shared-0.14.0.tar.gz"
716 +
SOURCE_URI_236="https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.17.crate"
717 +
CHECKSUM_SHA256_236="a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
718 +
SOURCE_FILENAME_236="pin-project-lite-0.2.17.tar.gz"
719 +
SOURCE_URI_237="https://static.crates.io/crates/pkcs1/pkcs1-0.7.5.crate"
720 +
CHECKSUM_SHA256_237="c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
721 +
SOURCE_FILENAME_237="pkcs1-0.7.5.tar.gz"
722 +
SOURCE_URI_238="https://static.crates.io/crates/pkcs8/pkcs8-0.10.2.crate"
723 +
CHECKSUM_SHA256_238="f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
724 +
SOURCE_FILENAME_238="pkcs8-0.10.2.tar.gz"
725 +
SOURCE_URI_239="https://static.crates.io/crates/pkcs8/pkcs8-0.11.0.crate"
726 +
CHECKSUM_SHA256_239="451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
727 +
SOURCE_FILENAME_239="pkcs8-0.11.0.tar.gz"
728 +
SOURCE_URI_240="https://static.crates.io/crates/pkg-config/pkg-config-0.3.34.crate"
729 +
CHECKSUM_SHA256_240="f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
730 +
SOURCE_FILENAME_240="pkg-config-0.3.34.tar.gz"
731 +
SOURCE_URI_241="https://static.crates.io/crates/plain/plain-0.2.3.crate"
732 +
CHECKSUM_SHA256_241="b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
733 +
SOURCE_FILENAME_241="plain-0.2.3.tar.gz"
734 +
SOURCE_URI_242="https://static.crates.io/crates/plist/plist-1.10.1.crate"
735 +
CHECKSUM_SHA256_242="2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb"
736 +
SOURCE_FILENAME_242="plist-1.10.1.tar.gz"
737 +
SOURCE_URI_243="https://static.crates.io/crates/png/png-0.18.1.crate"
738 +
CHECKSUM_SHA256_243="60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
739 +
SOURCE_FILENAME_243="png-0.18.1.tar.gz"
740 +
SOURCE_URI_244="https://static.crates.io/crates/potential_utf/potential_utf-0.1.6.crate"
741 +
CHECKSUM_SHA256_244="d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
742 +
SOURCE_FILENAME_244="potential_utf-0.1.6.tar.gz"
743 +
SOURCE_URI_245="https://static.crates.io/crates/powerfmt/powerfmt-0.2.0.crate"
744 +
CHECKSUM_SHA256_245="439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
745 +
SOURCE_FILENAME_245="powerfmt-0.2.0.tar.gz"
746 +
SOURCE_URI_246="https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.21.crate"
747 +
CHECKSUM_SHA256_246="85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
748 +
SOURCE_FILENAME_246="ppv-lite86-0.2.21.tar.gz"
749 +
SOURCE_URI_247="https://static.crates.io/crates/precomputed-hash/precomputed-hash-0.1.1.crate"
750 +
CHECKSUM_SHA256_247="925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
751 +
SOURCE_FILENAME_247="precomputed-hash-0.1.1.tar.gz"
752 +
SOURCE_URI_248="https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.107.crate"
753 +
CHECKSUM_SHA256_248="985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
754 +
SOURCE_FILENAME_248="proc-macro2-1.0.107.tar.gz"
755 +
SOURCE_URI_249="https://static.crates.io/crates/pulldown-cmark/pulldown-cmark-0.13.4.crate"
756 +
CHECKSUM_SHA256_249="e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
757 +
SOURCE_FILENAME_249="pulldown-cmark-0.13.4.tar.gz"
758 +
SOURCE_URI_250="https://static.crates.io/crates/pulldown-cmark-escape/pulldown-cmark-escape-0.11.0.crate"
759 +
CHECKSUM_SHA256_250="007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
760 +
SOURCE_FILENAME_250="pulldown-cmark-escape-0.11.0.tar.gz"
761 +
SOURCE_URI_251="https://static.crates.io/crates/pxfm/pxfm-0.1.30.crate"
762 +
CHECKSUM_SHA256_251="d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
763 +
SOURCE_FILENAME_251="pxfm-0.1.30.tar.gz"
764 +
SOURCE_URI_252="https://static.crates.io/crates/qrcode/qrcode-0.14.1.crate"
765 +
CHECKSUM_SHA256_252="d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
766 +
SOURCE_FILENAME_252="qrcode-0.14.1.tar.gz"
767 +
SOURCE_URI_253="https://static.crates.io/crates/quick-error/quick-error-2.0.1.crate"
768 +
CHECKSUM_SHA256_253="a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
769 +
SOURCE_FILENAME_253="quick-error-2.0.1.tar.gz"
770 +
SOURCE_URI_254="https://static.crates.io/crates/quick-xml/quick-xml-0.41.0.crate"
771 +
CHECKSUM_SHA256_254="e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
772 +
SOURCE_FILENAME_254="quick-xml-0.41.0.tar.gz"
773 +
SOURCE_URI_255="https://static.crates.io/crates/quick-xml/quick-xml-0.42.0.crate"
774 +
CHECKSUM_SHA256_255="41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
775 +
SOURCE_FILENAME_255="quick-xml-0.42.0.tar.gz"
776 +
SOURCE_URI_256="https://static.crates.io/crates/quinn/quinn-0.11.12.crate"
777 +
CHECKSUM_SHA256_256="4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
778 +
SOURCE_FILENAME_256="quinn-0.11.12.tar.gz"
779 +
SOURCE_URI_257="https://static.crates.io/crates/quinn-proto/quinn-proto-0.11.18.crate"
780 +
CHECKSUM_SHA256_257="a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
781 +
SOURCE_FILENAME_257="quinn-proto-0.11.18.tar.gz"
782 +
SOURCE_URI_258="https://static.crates.io/crates/quinn-udp/quinn-udp-0.5.15.crate"
783 +
CHECKSUM_SHA256_258="35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
784 +
SOURCE_FILENAME_258="quinn-udp-0.5.15.tar.gz"
785 +
SOURCE_URI_259="https://static.crates.io/crates/quote/quote-1.0.47.crate"
786 +
CHECKSUM_SHA256_259="1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
787 +
SOURCE_FILENAME_259="quote-1.0.47.tar.gz"
788 +
SOURCE_URI_260="https://static.crates.io/crates/quoted_printable/quoted_printable-0.5.2.crate"
789 +
CHECKSUM_SHA256_260="478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972"
790 +
SOURCE_FILENAME_260="quoted_printable-0.5.2.tar.gz"
791 +
SOURCE_URI_261="https://static.crates.io/crates/r-efi/r-efi-6.0.0.crate"
792 +
CHECKSUM_SHA256_261="f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
793 +
SOURCE_FILENAME_261="r-efi-6.0.0.tar.gz"
794 +
SOURCE_URI_262="https://static.crates.io/crates/rand/rand-0.10.3.crate"
795 +
CHECKSUM_SHA256_262="65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
796 +
SOURCE_FILENAME_262="rand-0.10.3.tar.gz"
797 +
SOURCE_URI_263="https://static.crates.io/crates/rand/rand-0.8.8.crate"
798 +
CHECKSUM_SHA256_263="e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
799 +
SOURCE_FILENAME_263="rand-0.8.8.tar.gz"
800 +
SOURCE_URI_264="https://static.crates.io/crates/rand_chacha/rand_chacha-0.3.1.crate"
801 +
CHECKSUM_SHA256_264="e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
802 +
SOURCE_FILENAME_264="rand_chacha-0.3.1.tar.gz"
803 +
SOURCE_URI_265="https://static.crates.io/crates/rand_core/rand_core-0.10.1.crate"
804 +
CHECKSUM_SHA256_265="63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
805 +
SOURCE_FILENAME_265="rand_core-0.10.1.tar.gz"
806 +
SOURCE_URI_266="https://static.crates.io/crates/rand_core/rand_core-0.6.4.crate"
807 +
CHECKSUM_SHA256_266="ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
808 +
SOURCE_FILENAME_266="rand_core-0.6.4.tar.gz"
809 +
SOURCE_URI_267="https://static.crates.io/crates/rand_pcg/rand_pcg-0.10.2.crate"
810 +
CHECKSUM_SHA256_267="caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
811 +
SOURCE_FILENAME_267="rand_pcg-0.10.2.tar.gz"
812 +
SOURCE_URI_268="https://static.crates.io/crates/rayon/rayon-1.12.0.crate"
813 +
CHECKSUM_SHA256_268="fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
814 +
SOURCE_FILENAME_268="rayon-1.12.0.tar.gz"
815 +
SOURCE_URI_269="https://static.crates.io/crates/rayon-core/rayon-core-1.13.0.crate"
816 +
CHECKSUM_SHA256_269="22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
817 +
SOURCE_FILENAME_269="rayon-core-1.13.0.tar.gz"
818 +
SOURCE_URI_270="https://static.crates.io/crates/rcgen/rcgen-0.14.10.crate"
819 +
CHECKSUM_SHA256_270="8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8"
820 +
SOURCE_FILENAME_270="rcgen-0.14.10.tar.gz"
821 +
SOURCE_URI_271="https://static.crates.io/crates/redox_syscall/redox_syscall-0.5.18.crate"
822 +
CHECKSUM_SHA256_271="ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
823 +
SOURCE_FILENAME_271="redox_syscall-0.5.18.tar.gz"
824 +
SOURCE_URI_272="https://static.crates.io/crates/redox_syscall/redox_syscall-0.9.4.crate"
825 +
CHECKSUM_SHA256_272="737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
826 +
SOURCE_FILENAME_272="redox_syscall-0.9.4.tar.gz"
827 +
SOURCE_URI_273="https://static.crates.io/crates/ref-cast/ref-cast-1.0.27.crate"
828 +
CHECKSUM_SHA256_273="7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
829 +
SOURCE_FILENAME_273="ref-cast-1.0.27.tar.gz"
830 +
SOURCE_URI_274="https://static.crates.io/crates/ref-cast-impl/ref-cast-impl-1.0.27.crate"
831 +
CHECKSUM_SHA256_274="92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
832 +
SOURCE_FILENAME_274="ref-cast-impl-1.0.27.tar.gz"
833 +
SOURCE_URI_275="https://static.crates.io/crates/regex/regex-1.13.1.crate"
834 +
CHECKSUM_SHA256_275="f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
835 +
SOURCE_FILENAME_275="regex-1.13.1.tar.gz"
836 +
SOURCE_URI_276="https://static.crates.io/crates/regex-automata/regex-automata-0.4.18.crate"
837 +
CHECKSUM_SHA256_276="ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
838 +
SOURCE_FILENAME_276="regex-automata-0.4.18.tar.gz"
839 +
SOURCE_URI_277="https://static.crates.io/crates/regex-syntax/regex-syntax-0.8.11.crate"
840 +
CHECKSUM_SHA256_277="d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
841 +
SOURCE_FILENAME_277="regex-syntax-0.8.11.tar.gz"
842 +
SOURCE_URI_278="https://static.crates.io/crates/reqwest/reqwest-0.12.28.crate"
843 +
CHECKSUM_SHA256_278="eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
844 +
SOURCE_FILENAME_278="reqwest-0.12.28.tar.gz"
845 +
SOURCE_URI_279="https://static.crates.io/crates/ring/ring-0.17.14.crate"
846 +
CHECKSUM_SHA256_279="a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
847 +
SOURCE_FILENAME_279="ring-0.17.14.tar.gz"
848 +
SOURCE_URI_280="https://static.crates.io/crates/rsa/rsa-0.9.10.crate"
849 +
CHECKSUM_SHA256_280="b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
850 +
SOURCE_FILENAME_280="rsa-0.9.10.tar.gz"
851 +
SOURCE_URI_281="https://static.crates.io/crates/rustc-hash/rustc-hash-2.1.3.crate"
852 +
CHECKSUM_SHA256_281="6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
853 +
SOURCE_FILENAME_281="rustc-hash-2.1.3.tar.gz"
854 +
SOURCE_URI_282="https://static.crates.io/crates/rustc_version/rustc_version-0.4.1.crate"
855 +
CHECKSUM_SHA256_282="cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
856 +
SOURCE_FILENAME_282="rustc_version-0.4.1.tar.gz"
857 +
SOURCE_URI_283="https://static.crates.io/crates/rusticata-macros/rusticata-macros-4.1.0.crate"
858 +
CHECKSUM_SHA256_283="faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
859 +
SOURCE_FILENAME_283="rusticata-macros-4.1.0.tar.gz"
860 +
SOURCE_URI_284="https://static.crates.io/crates/rustix/rustix-1.1.5.crate"
861 +
CHECKSUM_SHA256_284="891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
862 +
SOURCE_FILENAME_284="rustix-1.1.5.tar.gz"
863 +
SOURCE_URI_285="https://static.crates.io/crates/rustls/rustls-0.23.45.crate"
864 +
CHECKSUM_SHA256_285="0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
865 +
SOURCE_FILENAME_285="rustls-0.23.45.tar.gz"
866 +
SOURCE_URI_286="https://static.crates.io/crates/rustls-pemfile/rustls-pemfile-2.2.0.crate"
867 +
CHECKSUM_SHA256_286="dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
868 +
SOURCE_FILENAME_286="rustls-pemfile-2.2.0.tar.gz"
869 +
SOURCE_URI_287="https://static.crates.io/crates/rustls-pki-types/rustls-pki-types-1.15.1.crate"
870 +
CHECKSUM_SHA256_287="2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
871 +
SOURCE_FILENAME_287="rustls-pki-types-1.15.1.tar.gz"
872 +
SOURCE_URI_288="https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.103.15.crate"
873 +
CHECKSUM_SHA256_288="f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
874 +
SOURCE_FILENAME_288="rustls-webpki-0.103.15.tar.gz"
875 +
SOURCE_URI_289="https://static.crates.io/crates/rustversion/rustversion-1.0.23.crate"
876 +
CHECKSUM_SHA256_289="cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
877 +
SOURCE_FILENAME_289="rustversion-1.0.23.tar.gz"
878 +
SOURCE_URI_290="https://static.crates.io/crates/ryu/ryu-1.0.23.crate"
879 +
CHECKSUM_SHA256_290="9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
880 +
SOURCE_FILENAME_290="ryu-1.0.23.tar.gz"
881 +
SOURCE_URI_291="https://static.crates.io/crates/same-file/same-file-1.0.6.crate"
882 +
CHECKSUM_SHA256_291="93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
883 +
SOURCE_FILENAME_291="same-file-1.0.6.tar.gz"
884 +
SOURCE_URI_292="https://static.crates.io/crates/schemars/schemars-1.2.2.crate"
885 +
CHECKSUM_SHA256_292="687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
886 +
SOURCE_FILENAME_292="schemars-1.2.2.tar.gz"
887 +
SOURCE_URI_293="https://static.crates.io/crates/schemars_derive/schemars_derive-1.2.2.crate"
888 +
CHECKSUM_SHA256_293="d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
889 +
SOURCE_FILENAME_293="schemars_derive-1.2.2.tar.gz"
890 +
SOURCE_URI_294="https://static.crates.io/crates/scopeguard/scopeguard-1.2.0.crate"
891 +
CHECKSUM_SHA256_294="94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
892 +
SOURCE_FILENAME_294="scopeguard-1.2.0.tar.gz"
893 +
SOURCE_URI_295="https://static.crates.io/crates/semver/semver-1.0.28.crate"
894 +
CHECKSUM_SHA256_295="8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
895 +
SOURCE_FILENAME_295="semver-1.0.28.tar.gz"
896 +
SOURCE_URI_296="https://static.crates.io/crates/serde/serde-1.0.229.crate"
897 +
CHECKSUM_SHA256_296="4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
898 +
SOURCE_FILENAME_296="serde-1.0.229.tar.gz"
899 +
SOURCE_URI_297="https://static.crates.io/crates/serde_core/serde_core-1.0.229.crate"
900 +
CHECKSUM_SHA256_297="67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
901 +
SOURCE_FILENAME_297="serde_core-1.0.229.tar.gz"
902 +
SOURCE_URI_298="https://static.crates.io/crates/serde_derive/serde_derive-1.0.229.crate"
903 +
CHECKSUM_SHA256_298="e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
904 +
SOURCE_FILENAME_298="serde_derive-1.0.229.tar.gz"
905 +
SOURCE_URI_299="https://static.crates.io/crates/serde_derive_internals/serde_derive_internals-0.30.0.crate"
906 +
CHECKSUM_SHA256_299="f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
907 +
SOURCE_FILENAME_299="serde_derive_internals-0.30.0.tar.gz"
908 +
SOURCE_URI_300="https://static.crates.io/crates/serde_html_form/serde_html_form-0.2.8.crate"
909 +
CHECKSUM_SHA256_300="b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
910 +
SOURCE_FILENAME_300="serde_html_form-0.2.8.tar.gz"
911 +
SOURCE_URI_301="https://static.crates.io/crates/serde_json/serde_json-1.0.151.crate"
912 +
CHECKSUM_SHA256_301="c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
913 +
SOURCE_FILENAME_301="serde_json-1.0.151.tar.gz"
914 +
SOURCE_URI_302="https://static.crates.io/crates/serde_path_to_error/serde_path_to_error-0.1.20.crate"
915 +
CHECKSUM_SHA256_302="10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
916 +
SOURCE_FILENAME_302="serde_path_to_error-0.1.20.tar.gz"
917 +
SOURCE_URI_303="https://static.crates.io/crates/serde_spanned/serde_spanned-0.6.9.crate"
918 +
CHECKSUM_SHA256_303="bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
919 +
SOURCE_FILENAME_303="serde_spanned-0.6.9.tar.gz"
920 +
SOURCE_URI_304="https://static.crates.io/crates/serde_urlencoded/serde_urlencoded-0.7.1.crate"
921 +
CHECKSUM_SHA256_304="d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
922 +
SOURCE_FILENAME_304="serde_urlencoded-0.7.1.tar.gz"
923 +
SOURCE_URI_305="https://static.crates.io/crates/sha1/sha1-0.10.7.crate"
924 +
CHECKSUM_SHA256_305="a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
925 +
SOURCE_FILENAME_305="sha1-0.10.7.tar.gz"
926 +
SOURCE_URI_306="https://static.crates.io/crates/sha2/sha2-0.10.9.crate"
927 +
CHECKSUM_SHA256_306="a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
928 +
SOURCE_FILENAME_306="sha2-0.10.9.tar.gz"
929 +
SOURCE_URI_307="https://static.crates.io/crates/sha2/sha2-0.11.0.crate"
930 +
CHECKSUM_SHA256_307="446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
931 +
SOURCE_FILENAME_307="sha2-0.11.0.tar.gz"
932 +
SOURCE_URI_308="https://static.crates.io/crates/shake/shake-0.1.0.crate"
933 +
CHECKSUM_SHA256_308="09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a"
934 +
SOURCE_FILENAME_308="shake-0.1.0.tar.gz"
935 +
SOURCE_URI_309="https://static.crates.io/crates/sharded-slab/sharded-slab-0.1.7.crate"
936 +
CHECKSUM_SHA256_309="f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
937 +
SOURCE_FILENAME_309="sharded-slab-0.1.7.tar.gz"
938 +
SOURCE_URI_310="https://static.crates.io/crates/shlex/shlex-2.0.1.crate"
939 +
CHECKSUM_SHA256_310="f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
940 +
SOURCE_FILENAME_310="shlex-2.0.1.tar.gz"
941 +
SOURCE_URI_311="https://static.crates.io/crates/signal-hook-registry/signal-hook-registry-1.4.8.crate"
942 +
CHECKSUM_SHA256_311="c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
943 +
SOURCE_FILENAME_311="signal-hook-registry-1.4.8.tar.gz"
944 +
SOURCE_URI_312="https://static.crates.io/crates/signature/signature-2.2.0.crate"
945 +
CHECKSUM_SHA256_312="77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
946 +
SOURCE_FILENAME_312="signature-2.2.0.tar.gz"
947 +
SOURCE_URI_313="https://static.crates.io/crates/signature/signature-3.0.0.crate"
948 +
CHECKSUM_SHA256_313="28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
949 +
SOURCE_FILENAME_313="signature-3.0.0.tar.gz"
950 +
SOURCE_URI_314="https://static.crates.io/crates/simd-adler32/simd-adler32-0.3.10.crate"
951 +
CHECKSUM_SHA256_314="3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
952 +
SOURCE_FILENAME_314="simd-adler32-0.3.10.tar.gz"
953 +
SOURCE_URI_315="https://static.crates.io/crates/simdutf8/simdutf8-0.1.5.crate"
954 +
CHECKSUM_SHA256_315="e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
955 +
SOURCE_FILENAME_315="simdutf8-0.1.5.tar.gz"
956 +
SOURCE_URI_316="https://static.crates.io/crates/similar/similar-2.7.0.crate"
957 +
CHECKSUM_SHA256_316="bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
958 +
SOURCE_FILENAME_316="similar-2.7.0.tar.gz"
959 +
SOURCE_URI_317="https://static.crates.io/crates/siphasher/siphasher-1.0.4.crate"
960 +
CHECKSUM_SHA256_317="33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
961 +
SOURCE_FILENAME_317="siphasher-1.0.4.tar.gz"
962 +
SOURCE_URI_318="https://static.crates.io/crates/slab/slab-0.4.12.crate"
963 +
CHECKSUM_SHA256_318="0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
964 +
SOURCE_FILENAME_318="slab-0.4.12.tar.gz"
965 +
SOURCE_URI_319="https://static.crates.io/crates/smallvec/smallvec-1.16.2.crate"
966 +
CHECKSUM_SHA256_319="f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
967 +
SOURCE_FILENAME_319="smallvec-1.16.2.tar.gz"
968 +
SOURCE_URI_320="https://static.crates.io/crates/socket2/socket2-0.6.5.crate"
969 +
CHECKSUM_SHA256_320="c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
970 +
SOURCE_FILENAME_320="socket2-0.6.5.tar.gz"
971 +
SOURCE_URI_321="https://static.crates.io/crates/spin/spin-0.10.1.crate"
972 +
CHECKSUM_SHA256_321="023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
973 +
SOURCE_FILENAME_321="spin-0.10.1.tar.gz"
974 +
SOURCE_URI_322="https://static.crates.io/crates/spin/spin-0.9.9.crate"
975 +
CHECKSUM_SHA256_322="3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
976 +
SOURCE_FILENAME_322="spin-0.9.9.tar.gz"
977 +
SOURCE_URI_323="https://static.crates.io/crates/spki/spki-0.7.3.crate"
978 +
CHECKSUM_SHA256_323="d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
979 +
SOURCE_FILENAME_323="spki-0.7.3.tar.gz"
980 +
SOURCE_URI_324="https://static.crates.io/crates/spki/spki-0.8.0.crate"
981 +
CHECKSUM_SHA256_324="1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
982 +
SOURCE_FILENAME_324="spki-0.8.0.tar.gz"
983 +
SOURCE_URI_325="https://static.crates.io/crates/sponge-cursor/sponge-cursor-0.1.0.crate"
984 +
CHECKSUM_SHA256_325="3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
985 +
SOURCE_FILENAME_325="sponge-cursor-0.1.0.tar.gz"
986 +
SOURCE_URI_326="https://static.crates.io/crates/sqlx/sqlx-0.8.6.crate"
987 +
CHECKSUM_SHA256_326="1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
988 +
SOURCE_FILENAME_326="sqlx-0.8.6.tar.gz"
989 +
SOURCE_URI_327="https://static.crates.io/crates/sqlx-core/sqlx-core-0.8.6.crate"
990 +
CHECKSUM_SHA256_327="ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
991 +
SOURCE_FILENAME_327="sqlx-core-0.8.6.tar.gz"
992 +
SOURCE_URI_328="https://static.crates.io/crates/sqlx-macros/sqlx-macros-0.8.6.crate"
993 +
CHECKSUM_SHA256_328="a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
994 +
SOURCE_FILENAME_328="sqlx-macros-0.8.6.tar.gz"
995 +
SOURCE_URI_329="https://static.crates.io/crates/sqlx-macros-core/sqlx-macros-core-0.8.6.crate"
996 +
CHECKSUM_SHA256_329="19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
997 +
SOURCE_FILENAME_329="sqlx-macros-core-0.8.6.tar.gz"
998 +
SOURCE_URI_330="https://static.crates.io/crates/sqlx-mysql/sqlx-mysql-0.8.6.crate"
999 +
CHECKSUM_SHA256_330="aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
1000 +
SOURCE_FILENAME_330="sqlx-mysql-0.8.6.tar.gz"
1001 +
SOURCE_URI_331="https://static.crates.io/crates/sqlx-postgres/sqlx-postgres-0.8.6.crate"
1002 +
CHECKSUM_SHA256_331="db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
1003 +
SOURCE_FILENAME_331="sqlx-postgres-0.8.6.tar.gz"
1004 +
SOURCE_URI_332="https://static.crates.io/crates/sqlx-sqlite/sqlx-sqlite-0.8.6.crate"
1005 +
CHECKSUM_SHA256_332="c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
1006 +
SOURCE_FILENAME_332="sqlx-sqlite-0.8.6.tar.gz"
1007 +
SOURCE_URI_333="https://static.crates.io/crates/stable_deref_trait/stable_deref_trait-1.2.1.crate"
1008 +
CHECKSUM_SHA256_333="6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1009 +
SOURCE_FILENAME_333="stable_deref_trait-1.2.1.tar.gz"
1010 +
SOURCE_URI_334="https://static.crates.io/crates/streaming-iterator/streaming-iterator-0.1.9.crate"
1011 +
CHECKSUM_SHA256_334="2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
1012 +
SOURCE_FILENAME_334="streaming-iterator-0.1.9.tar.gz"
1013 +
SOURCE_URI_335="https://static.crates.io/crates/string_cache/string_cache-0.11.0.crate"
1014 +
CHECKSUM_SHA256_335="ffa8a5dbe8b3f0bbe29d4c3225daafaeead63afdc1b65fc4c01a1384166038e6"
1015 +
SOURCE_FILENAME_335="string_cache-0.11.0.tar.gz"
1016 +
SOURCE_URI_336="https://static.crates.io/crates/string_cache_codegen/string_cache_codegen-0.11.2.crate"
1017 +
CHECKSUM_SHA256_336="928dcdf75e47626b3617a976ec205d9f057584c371c1f23b782129268d0e6edc"
1018 +
SOURCE_FILENAME_336="string_cache_codegen-0.11.2.tar.gz"
1019 +
SOURCE_URI_337="https://static.crates.io/crates/stringprep/stringprep-0.1.5.crate"
1020 +
CHECKSUM_SHA256_337="7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
1021 +
SOURCE_FILENAME_337="stringprep-0.1.5.tar.gz"
1022 +
SOURCE_URI_338="https://static.crates.io/crates/strsim/strsim-0.11.1.crate"
1023 +
CHECKSUM_SHA256_338="7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1024 +
SOURCE_FILENAME_338="strsim-0.11.1.tar.gz"
1025 +
SOURCE_URI_339="https://static.crates.io/crates/subtle/subtle-2.6.1.crate"
1026 +
CHECKSUM_SHA256_339="13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
1027 +
SOURCE_FILENAME_339="subtle-2.6.1.tar.gz"
1028 +
SOURCE_URI_340="https://static.crates.io/crates/syn/syn-2.0.119.crate"
1029 +
CHECKSUM_SHA256_340="872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
1030 +
SOURCE_FILENAME_340="syn-2.0.119.tar.gz"
1031 +
SOURCE_URI_341="https://static.crates.io/crates/syn/syn-3.0.6.crate"
1032 +
CHECKSUM_SHA256_341="8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
1033 +
SOURCE_FILENAME_341="syn-3.0.6.tar.gz"
1034 +
SOURCE_URI_342="https://static.crates.io/crates/sync_wrapper/sync_wrapper-1.0.2.crate"
1035 +
CHECKSUM_SHA256_342="0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
1036 +
SOURCE_FILENAME_342="sync_wrapper-1.0.2.tar.gz"
1037 +
SOURCE_URI_343="https://static.crates.io/crates/synstructure/synstructure-0.13.2.crate"
1038 +
CHECKSUM_SHA256_343="728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
1039 +
SOURCE_FILENAME_343="synstructure-0.13.2.tar.gz"
1040 +
SOURCE_URI_344="https://static.crates.io/crates/synstructure/synstructure-0.14.0.crate"
1041 +
CHECKSUM_SHA256_344="901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
1042 +
SOURCE_FILENAME_344="synstructure-0.14.0.tar.gz"
1043 +
SOURCE_URI_345="https://static.crates.io/crates/syntect/syntect-5.3.0.crate"
1044 +
CHECKSUM_SHA256_345="656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
1045 +
SOURCE_FILENAME_345="syntect-5.3.0.tar.gz"
1046 +
SOURCE_URI_346="https://static.crates.io/crates/tar/tar-0.4.46.crate"
1047 +
CHECKSUM_SHA256_346="3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
1048 +
SOURCE_FILENAME_346="tar-0.4.46.tar.gz"
1049 +
SOURCE_URI_347="https://static.crates.io/crates/tempfile/tempfile-3.27.0.crate"
1050 +
CHECKSUM_SHA256_347="32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
1051 +
SOURCE_FILENAME_347="tempfile-3.27.0.tar.gz"
1052 +
SOURCE_URI_348="https://static.crates.io/crates/tendril/tendril-0.5.1.crate"
1053 +
CHECKSUM_SHA256_348="5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
1054 +
SOURCE_FILENAME_348="tendril-0.5.1.tar.gz"
1055 +
SOURCE_URI_349="https://static.crates.io/crates/thiserror/thiserror-1.0.69.crate"
1056 +
CHECKSUM_SHA256_349="b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1057 +
SOURCE_FILENAME_349="thiserror-1.0.69.tar.gz"
1058 +
SOURCE_URI_350="https://static.crates.io/crates/thiserror/thiserror-2.0.21.crate"
1059 +
CHECKSUM_SHA256_350="09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
1060 +
SOURCE_FILENAME_350="thiserror-2.0.21.tar.gz"
1061 +
SOURCE_URI_351="https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.69.crate"
1062 +
CHECKSUM_SHA256_351="4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1063 +
SOURCE_FILENAME_351="thiserror-impl-1.0.69.tar.gz"
1064 +
SOURCE_URI_352="https://static.crates.io/crates/thiserror-impl/thiserror-impl-2.0.21.crate"
1065 +
CHECKSUM_SHA256_352="fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
1066 +
SOURCE_FILENAME_352="thiserror-impl-2.0.21.tar.gz"
1067 +
SOURCE_URI_353="https://static.crates.io/crates/thread_local/thread_local-1.1.10.crate"
1068 +
CHECKSUM_SHA256_353="1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
1069 +
SOURCE_FILENAME_353="thread_local-1.1.10.tar.gz"
1070 +
SOURCE_URI_354="https://static.crates.io/crates/time/time-0.3.55.crate"
1071 +
CHECKSUM_SHA256_354="cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
1072 +
SOURCE_FILENAME_354="time-0.3.55.tar.gz"
1073 +
SOURCE_URI_355="https://static.crates.io/crates/time-core/time-core-0.1.9.crate"
1074 +
CHECKSUM_SHA256_355="9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
1075 +
SOURCE_FILENAME_355="time-core-0.1.9.tar.gz"
1076 +
SOURCE_URI_356="https://static.crates.io/crates/time-macros/time-macros-0.2.32.crate"
1077 +
CHECKSUM_SHA256_356="7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
1078 +
SOURCE_FILENAME_356="time-macros-0.2.32.tar.gz"
1079 +
SOURCE_URI_357="https://static.crates.io/crates/tinystr/tinystr-0.8.4.crate"
1080 +
CHECKSUM_SHA256_357="b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
1081 +
SOURCE_FILENAME_357="tinystr-0.8.4.tar.gz"
1082 +
SOURCE_URI_358="https://static.crates.io/crates/tinyvec/tinyvec-1.13.3.crate"
1083 +
CHECKSUM_SHA256_358="fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
1084 +
SOURCE_FILENAME_358="tinyvec-1.13.3.tar.gz"
1085 +
SOURCE_URI_359="https://static.crates.io/crates/tokio/tokio-1.53.1.crate"
1086 +
CHECKSUM_SHA256_359="202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
1087 +
SOURCE_FILENAME_359="tokio-1.53.1.tar.gz"
1088 +
SOURCE_URI_360="https://static.crates.io/crates/tokio-macros/tokio-macros-2.7.2.crate"
1089 +
CHECKSUM_SHA256_360="78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
1090 +
SOURCE_FILENAME_360="tokio-macros-2.7.2.tar.gz"
1091 +
SOURCE_URI_361="https://static.crates.io/crates/tokio-rustls/tokio-rustls-0.26.5.crate"
1092 +
CHECKSUM_SHA256_361="b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
1093 +
SOURCE_FILENAME_361="tokio-rustls-0.26.5.tar.gz"
1094 +
SOURCE_URI_362="https://static.crates.io/crates/tokio-stream/tokio-stream-0.1.19.crate"
1095 +
CHECKSUM_SHA256_362="a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
1096 +
SOURCE_FILENAME_362="tokio-stream-0.1.19.tar.gz"
1097 +
SOURCE_URI_363="https://static.crates.io/crates/tokio-util/tokio-util-0.7.19.crate"
1098 +
CHECKSUM_SHA256_363="494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
1099 +
SOURCE_FILENAME_363="tokio-util-0.7.19.tar.gz"
1100 +
SOURCE_URI_364="https://static.crates.io/crates/toml/toml-0.8.23.crate"
1101 +
CHECKSUM_SHA256_364="dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
1102 +
SOURCE_FILENAME_364="toml-0.8.23.tar.gz"
1103 +
SOURCE_URI_365="https://static.crates.io/crates/toml_datetime/toml_datetime-0.6.11.crate"
1104 +
CHECKSUM_SHA256_365="22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
1105 +
SOURCE_FILENAME_365="toml_datetime-0.6.11.tar.gz"
1106 +
SOURCE_URI_366="https://static.crates.io/crates/toml_edit/toml_edit-0.22.27.crate"
1107 +
CHECKSUM_SHA256_366="41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
1108 +
SOURCE_FILENAME_366="toml_edit-0.22.27.tar.gz"
1109 +
SOURCE_URI_367="https://static.crates.io/crates/toml_write/toml_write-0.1.2.crate"
1110 +
CHECKSUM_SHA256_367="5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
1111 +
SOURCE_FILENAME_367="toml_write-0.1.2.tar.gz"
1112 +
SOURCE_URI_368="https://static.crates.io/crates/tower/tower-0.5.3.crate"
1113 +
CHECKSUM_SHA256_368="ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
1114 +
SOURCE_FILENAME_368="tower-0.5.3.tar.gz"
1115 +
SOURCE_URI_369="https://static.crates.io/crates/tower-http/tower-http-0.6.11.crate"
1116 +
CHECKSUM_SHA256_369="4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
1117 +
SOURCE_FILENAME_369="tower-http-0.6.11.tar.gz"
1118 +
SOURCE_URI_370="https://static.crates.io/crates/tower-layer/tower-layer-0.3.3.crate"
1119 +
CHECKSUM_SHA256_370="121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1120 +
SOURCE_FILENAME_370="tower-layer-0.3.3.tar.gz"
1121 +
SOURCE_URI_371="https://static.crates.io/crates/tower-service/tower-service-0.3.3.crate"
1122 +
CHECKSUM_SHA256_371="8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1123 +
SOURCE_FILENAME_371="tower-service-0.3.3.tar.gz"
1124 +
SOURCE_URI_372="https://static.crates.io/crates/tracing/tracing-0.1.44.crate"
1125 +
CHECKSUM_SHA256_372="63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
1126 +
SOURCE_FILENAME_372="tracing-0.1.44.tar.gz"
1127 +
SOURCE_URI_373="https://static.crates.io/crates/tracing-attributes/tracing-attributes-0.1.31.crate"
1128 +
CHECKSUM_SHA256_373="7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
1129 +
SOURCE_FILENAME_373="tracing-attributes-0.1.31.tar.gz"
1130 +
SOURCE_URI_374="https://static.crates.io/crates/tracing-core/tracing-core-0.1.36.crate"
1131 +
CHECKSUM_SHA256_374="db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
1132 +
SOURCE_FILENAME_374="tracing-core-0.1.36.tar.gz"
1133 +
SOURCE_URI_375="https://static.crates.io/crates/tracing-log/tracing-log-0.2.0.crate"
1134 +
CHECKSUM_SHA256_375="ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
1135 +
SOURCE_FILENAME_375="tracing-log-0.2.0.tar.gz"
1136 +
SOURCE_URI_376="https://static.crates.io/crates/tracing-subscriber/tracing-subscriber-0.3.23.crate"
1137 +
CHECKSUM_SHA256_376="cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
1138 +
SOURCE_FILENAME_376="tracing-subscriber-0.3.23.tar.gz"
1139 +
SOURCE_URI_377="https://static.crates.io/crates/tree-sitter/tree-sitter-0.27.0.crate"
1140 +
CHECKSUM_SHA256_377="2038684e0058edba0d17302619f62eabce4a8e11c6ac59506996a8d79848851d"
1141 +
SOURCE_FILENAME_377="tree-sitter-0.27.0.tar.gz"
1142 +
SOURCE_URI_378="https://static.crates.io/crates/tree-sitter-bash/tree-sitter-bash-0.25.1.crate"
1143 +
CHECKSUM_SHA256_378="9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062"
1144 +
SOURCE_FILENAME_378="tree-sitter-bash-0.25.1.tar.gz"
1145 +
SOURCE_URI_379="https://static.crates.io/crates/tree-sitter-language/tree-sitter-language-0.1.8.crate"
1146 +
CHECKSUM_SHA256_379="ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
1147 +
SOURCE_FILENAME_379="tree-sitter-language-0.1.8.tar.gz"
1148 +
SOURCE_URI_380="https://static.crates.io/crates/tree-sitter-python/tree-sitter-python-0.25.0.crate"
1149 +
CHECKSUM_SHA256_380="6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c"
1150 +
SOURCE_FILENAME_380="tree-sitter-python-0.25.0.tar.gz"
1151 +
SOURCE_URI_381="https://static.crates.io/crates/tree-sitter-rust/tree-sitter-rust-0.24.2.crate"
1152 +
CHECKSUM_SHA256_381="439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45"
1153 +
SOURCE_FILENAME_381="tree-sitter-rust-0.24.2.tar.gz"
1154 +
SOURCE_URI_382="https://static.crates.io/crates/tree-sitter-typescript/tree-sitter-typescript-0.23.2.crate"
1155 +
CHECKSUM_SHA256_382="6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
1156 +
SOURCE_FILENAME_382="tree-sitter-typescript-0.23.2.tar.gz"
1157 +
SOURCE_URI_383="https://static.crates.io/crates/try-lock/try-lock-0.2.5.crate"
1158 +
CHECKSUM_SHA256_383="e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
1159 +
SOURCE_FILENAME_383="try-lock-0.2.5.tar.gz"
1160 +
SOURCE_URI_384="https://static.crates.io/crates/typenum/typenum-1.20.1.crate"
1161 +
CHECKSUM_SHA256_384="b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
1162 +
SOURCE_FILENAME_384="typenum-1.20.1.tar.gz"
1163 +
SOURCE_URI_385="https://static.crates.io/crates/unicase/unicase-2.9.0.crate"
1164 +
CHECKSUM_SHA256_385="dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
1165 +
SOURCE_FILENAME_385="unicase-2.9.0.tar.gz"
1166 +
SOURCE_URI_386="https://static.crates.io/crates/unicode-bidi/unicode-bidi-0.3.18.crate"
1167 +
CHECKSUM_SHA256_386="5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
1168 +
SOURCE_FILENAME_386="unicode-bidi-0.3.18.tar.gz"
1169 +
SOURCE_URI_387="https://static.crates.io/crates/unicode-ident/unicode-ident-1.0.26.crate"
1170 +
CHECKSUM_SHA256_387="d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
1171 +
SOURCE_FILENAME_387="unicode-ident-1.0.26.tar.gz"
1172 +
SOURCE_URI_388="https://static.crates.io/crates/unicode-normalization/unicode-normalization-0.1.25.crate"
1173 +
CHECKSUM_SHA256_388="5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
1174 +
SOURCE_FILENAME_388="unicode-normalization-0.1.25.tar.gz"
1175 +
SOURCE_URI_389="https://static.crates.io/crates/unicode-properties/unicode-properties-0.1.4.crate"
1176 +
CHECKSUM_SHA256_389="7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
1177 +
SOURCE_FILENAME_389="unicode-properties-0.1.4.tar.gz"
1178 +
SOURCE_URI_390="https://static.crates.io/crates/unicode-width/unicode-width-0.1.14.crate"
1179 +
CHECKSUM_SHA256_390="7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
1180 +
SOURCE_FILENAME_390="unicode-width-0.1.14.tar.gz"
1181 +
SOURCE_URI_391="https://static.crates.io/crates/untrusted/untrusted-0.9.0.crate"
1182 +
CHECKSUM_SHA256_391="8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1183 +
SOURCE_FILENAME_391="untrusted-0.9.0.tar.gz"
1184 +
SOURCE_URI_392="https://static.crates.io/crates/ureq/ureq-2.12.1.crate"
1185 +
CHECKSUM_SHA256_392="02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
1186 +
SOURCE_FILENAME_392="ureq-2.12.1.tar.gz"
1187 +
SOURCE_URI_393="https://static.crates.io/crates/url/url-2.5.8.crate"
1188 +
CHECKSUM_SHA256_393="ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
1189 +
SOURCE_FILENAME_393="url-2.5.8.tar.gz"
1190 +
SOURCE_URI_394="https://static.crates.io/crates/utf8_iter/utf8_iter-1.0.4.crate"
1191 +
CHECKSUM_SHA256_394="b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1192 +
SOURCE_FILENAME_394="utf8_iter-1.0.4.tar.gz"
1193 +
SOURCE_URI_395="https://static.crates.io/crates/utf8parse/utf8parse-0.2.2.crate"
1194 +
CHECKSUM_SHA256_395="06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1195 +
SOURCE_FILENAME_395="utf8parse-0.2.2.tar.gz"
1196 +
SOURCE_URI_396="https://static.crates.io/crates/valuable/valuable-0.1.1.crate"
1197 +
CHECKSUM_SHA256_396="ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1198 +
SOURCE_FILENAME_396="valuable-0.1.1.tar.gz"
1199 +
SOURCE_URI_397="https://static.crates.io/crates/vcpkg/vcpkg-0.2.15.crate"
1200 +
CHECKSUM_SHA256_397="accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1201 +
SOURCE_FILENAME_397="vcpkg-0.2.15.tar.gz"
1202 +
SOURCE_URI_398="https://static.crates.io/crates/version_check/version_check-0.9.5.crate"
1203 +
CHECKSUM_SHA256_398="0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1204 +
SOURCE_FILENAME_398="version_check-0.9.5.tar.gz"
1205 +
SOURCE_URI_399="https://static.crates.io/crates/walkdir/walkdir-2.5.0.crate"
1206 +
CHECKSUM_SHA256_399="29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1207 +
SOURCE_FILENAME_399="walkdir-2.5.0.tar.gz"
1208 +
SOURCE_URI_400="https://static.crates.io/crates/want/want-0.3.1.crate"
1209 +
CHECKSUM_SHA256_400="bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
1210 +
SOURCE_FILENAME_400="want-0.3.1.tar.gz"
1211 +
SOURCE_URI_401="https://static.crates.io/crates/wasi/wasi-0.11.1+wasi-snapshot-preview1.crate"
1212 +
CHECKSUM_SHA256_401="ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1213 +
SOURCE_FILENAME_401="wasi-0.11.1+wasi-snapshot-preview1.tar.gz"
1214 +
SOURCE_URI_402="https://static.crates.io/crates/wasite/wasite-0.1.0.crate"
1215 +
CHECKSUM_SHA256_402="b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
1216 +
SOURCE_FILENAME_402="wasite-0.1.0.tar.gz"
1217 +
SOURCE_URI_403="https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.129.crate"
1218 +
CHECKSUM_SHA256_403="9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
1219 +
SOURCE_FILENAME_403="wasm-bindgen-0.2.129.tar.gz"
1220 +
SOURCE_URI_404="https://static.crates.io/crates/wasm-bindgen-futures/wasm-bindgen-futures-0.4.79.crate"
1221 +
CHECKSUM_SHA256_404="3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
1222 +
SOURCE_FILENAME_404="wasm-bindgen-futures-0.4.79.tar.gz"
1223 +
SOURCE_URI_405="https://static.crates.io/crates/wasm-bindgen-macro/wasm-bindgen-macro-0.2.129.crate"
1224 +
CHECKSUM_SHA256_405="2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
1225 +
SOURCE_FILENAME_405="wasm-bindgen-macro-0.2.129.tar.gz"
1226 +
SOURCE_URI_406="https://static.crates.io/crates/wasm-bindgen-macro-support/wasm-bindgen-macro-support-0.2.129.crate"
1227 +
CHECKSUM_SHA256_406="6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
1228 +
SOURCE_FILENAME_406="wasm-bindgen-macro-support-0.2.129.tar.gz"
1229 +
SOURCE_URI_407="https://static.crates.io/crates/wasm-bindgen-shared/wasm-bindgen-shared-0.2.129.crate"
1230 +
CHECKSUM_SHA256_407="23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
1231 +
SOURCE_FILENAME_407="wasm-bindgen-shared-0.2.129.tar.gz"
1232 +
SOURCE_URI_408="https://static.crates.io/crates/web-sys/web-sys-0.3.106.crate"
1233 +
CHECKSUM_SHA256_408="88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
1234 +
SOURCE_FILENAME_408="web-sys-0.3.106.tar.gz"
1235 +
SOURCE_URI_409="https://static.crates.io/crates/web-time/web-time-1.1.0.crate"
1236 +
CHECKSUM_SHA256_409="5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
1237 +
SOURCE_FILENAME_409="web-time-1.1.0.tar.gz"
1238 +
SOURCE_URI_410="https://static.crates.io/crates/web_atoms/web_atoms-0.3.0.crate"
1239 +
CHECKSUM_SHA256_410="7572660c8890448ba236b7376f27e389c6a7e1c70195622faced601f855c0ada"
1240 +
SOURCE_FILENAME_410="web_atoms-0.3.0.tar.gz"
1241 +
SOURCE_URI_411="https://static.crates.io/crates/webpki-roots/webpki-roots-0.26.11.crate"
1242 +
CHECKSUM_SHA256_411="521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
1243 +
SOURCE_FILENAME_411="webpki-roots-0.26.11.tar.gz"
1244 +
SOURCE_URI_412="https://static.crates.io/crates/webpki-roots/webpki-roots-1.0.9.crate"
1245 +
CHECKSUM_SHA256_412="7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
1246 +
SOURCE_FILENAME_412="webpki-roots-1.0.9.tar.gz"
1247 +
SOURCE_URI_413="https://static.crates.io/crates/whoami/whoami-1.6.1.crate"
1248 +
CHECKSUM_SHA256_413="5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
1249 +
SOURCE_FILENAME_413="whoami-1.6.1.tar.gz"
1250 +
SOURCE_URI_414="https://static.crates.io/crates/winapi-util/winapi-util-0.1.11.crate"
1251 +
CHECKSUM_SHA256_414="c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
1252 +
SOURCE_FILENAME_414="winapi-util-0.1.11.tar.gz"
1253 +
SOURCE_URI_415="https://static.crates.io/crates/windows-core/windows-core-0.62.2.crate"
1254 +
CHECKSUM_SHA256_415="b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
1255 +
SOURCE_FILENAME_415="windows-core-0.62.2.tar.gz"
1256 +
SOURCE_URI_416="https://static.crates.io/crates/windows-implement/windows-implement-0.60.2.crate"
1257 +
CHECKSUM_SHA256_416="053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
1258 +
SOURCE_FILENAME_416="windows-implement-0.60.2.tar.gz"
1259 +
SOURCE_URI_417="https://static.crates.io/crates/windows-interface/windows-interface-0.59.3.crate"
1260 +
CHECKSUM_SHA256_417="3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
1261 +
SOURCE_FILENAME_417="windows-interface-0.59.3.tar.gz"
1262 +
SOURCE_URI_418="https://static.crates.io/crates/windows-link/windows-link-0.2.1.crate"
1263 +
CHECKSUM_SHA256_418="f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1264 +
SOURCE_FILENAME_418="windows-link-0.2.1.tar.gz"
1265 +
SOURCE_URI_419="https://static.crates.io/crates/windows-result/windows-result-0.4.1.crate"
1266 +
CHECKSUM_SHA256_419="7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
1267 +
SOURCE_FILENAME_419="windows-result-0.4.1.tar.gz"
1268 +
SOURCE_URI_420="https://static.crates.io/crates/windows-strings/windows-strings-0.5.1.crate"
1269 +
CHECKSUM_SHA256_420="7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
1270 +
SOURCE_FILENAME_420="windows-strings-0.5.1.tar.gz"
1271 +
SOURCE_URI_421="https://static.crates.io/crates/windows-sys/windows-sys-0.48.0.crate"
1272 +
CHECKSUM_SHA256_421="677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1273 +
SOURCE_FILENAME_421="windows-sys-0.48.0.tar.gz"
1274 +
SOURCE_URI_422="https://static.crates.io/crates/windows-sys/windows-sys-0.52.0.crate"
1275 +
CHECKSUM_SHA256_422="282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1276 +
SOURCE_FILENAME_422="windows-sys-0.52.0.tar.gz"
1277 +
SOURCE_URI_423="https://static.crates.io/crates/windows-sys/windows-sys-0.61.2.crate"
1278 +
CHECKSUM_SHA256_423="ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1279 +
SOURCE_FILENAME_423="windows-sys-0.61.2.tar.gz"
1280 +
SOURCE_URI_424="https://static.crates.io/crates/windows-targets/windows-targets-0.48.5.crate"
1281 +
CHECKSUM_SHA256_424="9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1282 +
SOURCE_FILENAME_424="windows-targets-0.48.5.tar.gz"
1283 +
SOURCE_URI_425="https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate"
1284 +
CHECKSUM_SHA256_425="9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1285 +
SOURCE_FILENAME_425="windows-targets-0.52.6.tar.gz"
1286 +
SOURCE_URI_426="https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.48.5.crate"
1287 +
CHECKSUM_SHA256_426="2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1288 +
SOURCE_FILENAME_426="windows_aarch64_gnullvm-0.48.5.tar.gz"
1289 +
SOURCE_URI_427="https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.52.6.crate"
1290 +
CHECKSUM_SHA256_427="32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1291 +
SOURCE_FILENAME_427="windows_aarch64_gnullvm-0.52.6.tar.gz"
1292 +
SOURCE_URI_428="https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.48.5.crate"
1293 +
CHECKSUM_SHA256_428="dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1294 +
SOURCE_FILENAME_428="windows_aarch64_msvc-0.48.5.tar.gz"
1295 +
SOURCE_URI_429="https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.52.6.crate"
1296 +
CHECKSUM_SHA256_429="09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1297 +
SOURCE_FILENAME_429="windows_aarch64_msvc-0.52.6.tar.gz"
1298 +
SOURCE_URI_430="https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.48.5.crate"
1299 +
CHECKSUM_SHA256_430="a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1300 +
SOURCE_FILENAME_430="windows_i686_gnu-0.48.5.tar.gz"
1301 +
SOURCE_URI_431="https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.52.6.crate"
1302 +
CHECKSUM_SHA256_431="8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1303 +
SOURCE_FILENAME_431="windows_i686_gnu-0.52.6.tar.gz"
1304 +
SOURCE_URI_432="https://static.crates.io/crates/windows_i686_gnullvm/windows_i686_gnullvm-0.52.6.crate"
1305 +
CHECKSUM_SHA256_432="0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1306 +
SOURCE_FILENAME_432="windows_i686_gnullvm-0.52.6.tar.gz"
1307 +
SOURCE_URI_433="https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.48.5.crate"
1308 +
CHECKSUM_SHA256_433="8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1309 +
SOURCE_FILENAME_433="windows_i686_msvc-0.48.5.tar.gz"
1310 +
SOURCE_URI_434="https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.52.6.crate"
1311 +
CHECKSUM_SHA256_434="240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1312 +
SOURCE_FILENAME_434="windows_i686_msvc-0.52.6.tar.gz"
1313 +
SOURCE_URI_435="https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.48.5.crate"
1314 +
CHECKSUM_SHA256_435="53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1315 +
SOURCE_FILENAME_435="windows_x86_64_gnu-0.48.5.tar.gz"
1316 +
SOURCE_URI_436="https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.52.6.crate"
1317 +
CHECKSUM_SHA256_436="147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1318 +
SOURCE_FILENAME_436="windows_x86_64_gnu-0.52.6.tar.gz"
1319 +
SOURCE_URI_437="https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.48.5.crate"
1320 +
CHECKSUM_SHA256_437="0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1321 +
SOURCE_FILENAME_437="windows_x86_64_gnullvm-0.48.5.tar.gz"
1322 +
SOURCE_URI_438="https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.52.6.crate"
1323 +
CHECKSUM_SHA256_438="24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1324 +
SOURCE_FILENAME_438="windows_x86_64_gnullvm-0.52.6.tar.gz"
1325 +
SOURCE_URI_439="https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.48.5.crate"
1326 +
CHECKSUM_SHA256_439="ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1327 +
SOURCE_FILENAME_439="windows_x86_64_msvc-0.48.5.tar.gz"
1328 +
SOURCE_URI_440="https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate"
1329 +
CHECKSUM_SHA256_440="589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1330 +
SOURCE_FILENAME_440="windows_x86_64_msvc-0.52.6.tar.gz"
1331 +
SOURCE_URI_441="https://static.crates.io/crates/winnow/winnow-0.7.15.crate"
1332 +
CHECKSUM_SHA256_441="df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
1333 +
SOURCE_FILENAME_441="winnow-0.7.15.tar.gz"
1334 +
SOURCE_URI_442="https://static.crates.io/crates/writeable/writeable-0.6.4.crate"
1335 +
CHECKSUM_SHA256_442="3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
1336 +
SOURCE_FILENAME_442="writeable-0.6.4.tar.gz"
1337 +
SOURCE_URI_443="https://static.crates.io/crates/x509-parser/x509-parser-0.18.1.crate"
1338 +
CHECKSUM_SHA256_443="d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
1339 +
SOURCE_FILENAME_443="x509-parser-0.18.1.tar.gz"
1340 +
SOURCE_URI_444="https://static.crates.io/crates/xattr/xattr-1.6.1.crate"
1341 +
CHECKSUM_SHA256_444="32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
1342 +
SOURCE_FILENAME_444="xattr-1.6.1.tar.gz"
1343 +
SOURCE_URI_445="https://static.crates.io/crates/yaml-rust/yaml-rust-0.4.5.crate"
1344 +
CHECKSUM_SHA256_445="56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
1345 +
SOURCE_FILENAME_445="yaml-rust-0.4.5.tar.gz"
1346 +
SOURCE_URI_446="https://static.crates.io/crates/yasna/yasna-0.6.0.crate"
1347 +
CHECKSUM_SHA256_446="b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
1348 +
SOURCE_FILENAME_446="yasna-0.6.0.tar.gz"
1349 +
SOURCE_URI_447="https://static.crates.io/crates/yoke/yoke-0.8.3.crate"
1350 +
CHECKSUM_SHA256_447="709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
1351 +
SOURCE_FILENAME_447="yoke-0.8.3.tar.gz"
1352 +
SOURCE_URI_448="https://static.crates.io/crates/yoke-derive/yoke-derive-0.8.3.crate"
1353 +
CHECKSUM_SHA256_448="33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
1354 +
SOURCE_FILENAME_448="yoke-derive-0.8.3.tar.gz"
1355 +
SOURCE_URI_449="https://static.crates.io/crates/zerocopy/zerocopy-0.8.59.crate"
1356 +
CHECKSUM_SHA256_449="6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
1357 +
SOURCE_FILENAME_449="zerocopy-0.8.59.tar.gz"
1358 +
SOURCE_URI_450="https://static.crates.io/crates/zerocopy-derive/zerocopy-derive-0.8.59.crate"
1359 +
CHECKSUM_SHA256_450="ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
1360 +
SOURCE_FILENAME_450="zerocopy-derive-0.8.59.tar.gz"
1361 +
SOURCE_URI_451="https://static.crates.io/crates/zerofrom/zerofrom-0.1.8.crate"
1362 +
CHECKSUM_SHA256_451="0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
1363 +
SOURCE_FILENAME_451="zerofrom-0.1.8.tar.gz"
1364 +
SOURCE_URI_452="https://static.crates.io/crates/zerofrom-derive/zerofrom-derive-0.1.8.crate"
1365 +
CHECKSUM_SHA256_452="f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
1366 +
SOURCE_FILENAME_452="zerofrom-derive-0.1.8.tar.gz"
1367 +
SOURCE_URI_453="https://static.crates.io/crates/zeroize/zeroize-1.9.0.crate"
1368 +
CHECKSUM_SHA256_453="e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
1369 +
SOURCE_FILENAME_453="zeroize-1.9.0.tar.gz"
1370 +
SOURCE_URI_454="https://static.crates.io/crates/zerotrie/zerotrie-0.2.5.crate"
1371 +
CHECKSUM_SHA256_454="4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
1372 +
SOURCE_FILENAME_454="zerotrie-0.2.5.tar.gz"
1373 +
SOURCE_URI_455="https://static.crates.io/crates/zerovec/zerovec-0.11.8.crate"
1374 +
CHECKSUM_SHA256_455="bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
1375 +
SOURCE_FILENAME_455="zerovec-0.11.8.tar.gz"
1376 +
SOURCE_URI_456="https://static.crates.io/crates/zerovec-derive/zerovec-derive-0.11.6.crate"
1377 +
CHECKSUM_SHA256_456="34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
1378 +
SOURCE_FILENAME_456="zerovec-derive-0.11.6.tar.gz"
1379 +
SOURCE_URI_457="https://static.crates.io/crates/zip/zip-2.4.2.crate"
1380 +
CHECKSUM_SHA256_457="fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
1381 +
SOURCE_FILENAME_457="zip-2.4.2.tar.gz"
1382 +
SOURCE_URI_458="https://static.crates.io/crates/zlib-rs/zlib-rs-0.6.8.crate"
1383 +
CHECKSUM_SHA256_458="b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
1384 +
SOURCE_FILENAME_458="zlib-rs-0.6.8.tar.gz"
1385 +
SOURCE_URI_459="https://static.crates.io/crates/zmij/zmij-1.0.23.crate"
1386 +
CHECKSUM_SHA256_459="29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
1387 +
SOURCE_FILENAME_459="zmij-1.0.23.tar.gz"
1388 +
SOURCE_URI_460="https://static.crates.io/crates/zopfli/zopfli-0.8.3.crate"
1389 +
CHECKSUM_SHA256_460="f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
1390 +
SOURCE_FILENAME_460="zopfli-0.8.3.tar.gz"
1391 +
SOURCE_URI_461="https://static.crates.io/crates/zstd/zstd-0.13.3.crate"
1392 +
CHECKSUM_SHA256_461="e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
1393 +
SOURCE_FILENAME_461="zstd-0.13.3.tar.gz"
1394 +
SOURCE_URI_462="https://static.crates.io/crates/zstd-safe/zstd-safe-7.3.0.crate"
1395 +
CHECKSUM_SHA256_462="64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882"
1396 +
SOURCE_FILENAME_462="zstd-safe-7.3.0.tar.gz"
1397 +
SOURCE_URI_463="https://static.crates.io/crates/zstd-sys/zstd-sys-2.1.0+zstd.1.5.7.crate"
1398 +
CHECKSUM_SHA256_463="0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0"
1399 +
SOURCE_FILENAME_463="zstd-sys-2.1.0+zstd.1.5.7.tar.gz"
1400 +
SOURCE_URI_464="https://static.crates.io/crates/zune-core/zune-core-0.5.3.crate"
1401 +
CHECKSUM_SHA256_464="d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
1402 +
SOURCE_FILENAME_464="zune-core-0.5.3.tar.gz"
1403 +
SOURCE_URI_465="https://static.crates.io/crates/zune-jpeg/zune-jpeg-0.5.15.crate"
1404 +
CHECKSUM_SHA256_465="27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
1405 +
SOURCE_FILENAME_465="zune-jpeg-0.5.15.tar.gz"
1406 +
1407 +
ARCHITECTURES="!all x86_64"
1408 +
1409 +
PROVIDES="
1410 +
	oh$secondaryArchSuffix = $portVersion
1411 +
	cmd:oh
1412 +
	"
1413 +
REQUIRES="
1414 +
	haiku$secondaryArchSuffix
1415 +
	lib:libzstd$secondaryArchSuffix
1416 +
	"
1417 +
1418 +
BUILD_REQUIRES="
1419 +
	haiku${secondaryArchSuffix}_devel
1420 +
	devel:libzstd$secondaryArchSuffix
1421 +
	"
1422 +
BUILD_PREREQUIRES="
1423 +
	cmd:gcc$secondaryArchSuffix
1424 +
	cmd:pkg_config$secondaryArchSuffix
1425 +
	cmd:rustc$secondaryArchSuffix
1426 +
	"
1427 +
1428 +
BUILD()
1429 +
{
1430 +
	export CARGO_HOME=$sourceDir/../cargo
1431 +
	vendor=$CARGO_HOME/haiku
1432 +
	mkdir -p "$vendor"
1433 +
	for i in $(seq 2 465); do
1434 +
		eval "srcDir=\$sourceDir$i"
1435 +
		eval "sha256sum=\$CHECKSUM_SHA256_$i"
1436 +
		set -- "$srcDir"/*
1437 +
		ln -sf "$1" "$vendor"
1438 +
		cat <<-EOF >"$vendor/${1##*/}/.cargo-checksum.json"
1439 +
		{
1440 +
		  "package": "$sha256sum",
1441 +
		  "files": {}
1442 +
		}
1443 +
		EOF
1444 +
	done
1445 +
1446 +
	cat <<-EOF >"$CARGO_HOME"/config.toml
1447 +
	[source.haiku]
1448 +
	directory = "$vendor"
1449 +
1450 +
	[source.crates-io]
1451 +
	replace-with = "haiku"
1452 +
	EOF
1453 +
1454 +
	# The bundled zstd's hidden symbols break the link on Haiku: use the system's.
1455 +
	export ZSTD_SYS_USE_PKG_CONFIG=1
1456 +
	export OXY_BUILD=v$portVersion
1457 +
	cargo build --release --frozen -p oh
1458 +
}
1459 +
1460 +
INSTALL()
1461 +
{
1462 +
	install -v -m 755 -d "$prefix"/bin "$docDir"
1463 +
	install -v -m 755 ./target/release/oh "$prefix"/bin
1464 +
	install -v -m 644 README.md LICENSE docs/oh.md "$docDir"
1465 +
}
1466 +
1467 +
TEST()
1468 +
{
1469 +
	export CARGO_HOME=$sourceDir/../cargo
1470 +
	export ZSTD_SYS_USE_PKG_CONFIG=1
1471 +
	cargo test --release --frozen -p hub-api
1472 +
}
added packaging/haikuports/dev-vcs/oxy/oxy-0.9.0.recipe +990 −0
1 +
SUMMARY="A version control system of signed, commuting changes, with Git interop"
2 +
DESCRIPTION="oxy is the command line of Oxygen, a version control system built on \
3 +
patch theory: changes commute, conflicts are recorded rather than blocking, \
4 +
every change can be signed, and a trust policy says who may move what. It \
5 +
imports and exports Git history, clones from OxygenHub and other Oxygen \
6 +
hosts, and keeps repositories on disk or in S3, GCS or Azure buckets."
7 +
HOMEPAGE="https://oxygenhub.io/andymccall/oxygen"
8 +
COPYRIGHT="2026 Andy McCall"
9 +
LICENSE="MIT
10 +
	Apache v2"
11 +
REVISION="1"
12 +
SOURCE_URI="$HOMEPAGE/releases/download/v$portVersion/oxy-$portVersion-src.tar.gz"
13 +
CHECKSUM_SHA256="b284392c53677177429b7db7f47b0b3585c766a910654a24ab47a6a75555bb8a"
14 +
SOURCE_DIR="oxygen-$portVersion"
15 +
SOURCE_URI_2="https://static.crates.io/crates/adler2/adler2-2.0.1.crate"
16 +
CHECKSUM_SHA256_2="320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
17 +
SOURCE_FILENAME_2="adler2-2.0.1.tar.gz"
18 +
SOURCE_URI_3="https://static.crates.io/crates/aho-corasick/aho-corasick-1.1.5.crate"
19 +
CHECKSUM_SHA256_3="c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
20 +
SOURCE_FILENAME_3="aho-corasick-1.1.5.tar.gz"
21 +
SOURCE_URI_4="https://static.crates.io/crates/android_system_properties/android_system_properties-0.1.6.crate"
22 +
CHECKSUM_SHA256_4="ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
23 +
SOURCE_FILENAME_4="android_system_properties-0.1.6.tar.gz"
24 +
SOURCE_URI_5="https://static.crates.io/crates/anstream/anstream-1.0.0.crate"
25 +
CHECKSUM_SHA256_5="824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
26 +
SOURCE_FILENAME_5="anstream-1.0.0.tar.gz"
27 +
SOURCE_URI_6="https://static.crates.io/crates/anstyle/anstyle-1.0.14.crate"
28 +
CHECKSUM_SHA256_6="940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
29 +
SOURCE_FILENAME_6="anstyle-1.0.14.tar.gz"
30 +
SOURCE_URI_7="https://static.crates.io/crates/anstyle-parse/anstyle-parse-1.0.0.crate"
31 +
CHECKSUM_SHA256_7="52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
32 +
SOURCE_FILENAME_7="anstyle-parse-1.0.0.tar.gz"
33 +
SOURCE_URI_8="https://static.crates.io/crates/anstyle-query/anstyle-query-1.1.5.crate"
34 +
CHECKSUM_SHA256_8="40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
35 +
SOURCE_FILENAME_8="anstyle-query-1.1.5.tar.gz"
36 +
SOURCE_URI_9="https://static.crates.io/crates/anstyle-wincon/anstyle-wincon-3.0.11.crate"
37 +
CHECKSUM_SHA256_9="291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
38 +
SOURCE_FILENAME_9="anstyle-wincon-3.0.11.tar.gz"
39 +
SOURCE_URI_10="https://static.crates.io/crates/anyhow/anyhow-1.0.104.crate"
40 +
CHECKSUM_SHA256_10="330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
41 +
SOURCE_FILENAME_10="anyhow-1.0.104.tar.gz"
42 +
SOURCE_URI_11="https://static.crates.io/crates/arc-swap/arc-swap-1.9.2.crate"
43 +
CHECKSUM_SHA256_11="c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
44 +
SOURCE_FILENAME_11="arc-swap-1.9.2.tar.gz"
45 +
SOURCE_URI_12="https://static.crates.io/crates/arrayvec/arrayvec-0.7.8.crate"
46 +
CHECKSUM_SHA256_12="d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
47 +
SOURCE_FILENAME_12="arrayvec-0.7.8.tar.gz"
48 +
SOURCE_URI_13="https://static.crates.io/crates/asn1-rs/asn1-rs-0.7.2.crate"
49 +
CHECKSUM_SHA256_13="b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
50 +
SOURCE_FILENAME_13="asn1-rs-0.7.2.tar.gz"
51 +
SOURCE_URI_14="https://static.crates.io/crates/asn1-rs-derive/asn1-rs-derive-0.6.0.crate"
52 +
CHECKSUM_SHA256_14="3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
53 +
SOURCE_FILENAME_14="asn1-rs-derive-0.6.0.tar.gz"
54 +
SOURCE_URI_15="https://static.crates.io/crates/asn1-rs-impl/asn1-rs-impl-0.2.0.crate"
55 +
CHECKSUM_SHA256_15="7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
56 +
SOURCE_FILENAME_15="asn1-rs-impl-0.2.0.tar.gz"
57 +
SOURCE_URI_16="https://static.crates.io/crates/async-trait/async-trait-0.1.92.crate"
58 +
CHECKSUM_SHA256_16="82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
59 +
SOURCE_FILENAME_16="async-trait-0.1.92.tar.gz"
60 +
SOURCE_URI_17="https://static.crates.io/crates/atomic-waker/atomic-waker-1.1.2.crate"
61 +
CHECKSUM_SHA256_17="1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
62 +
SOURCE_FILENAME_17="atomic-waker-1.1.2.tar.gz"
63 +
SOURCE_URI_18="https://static.crates.io/crates/autocfg/autocfg-1.5.1.crate"
64 +
CHECKSUM_SHA256_18="f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
65 +
SOURCE_FILENAME_18="autocfg-1.5.1.tar.gz"
66 +
SOURCE_URI_19="https://static.crates.io/crates/axum/axum-0.8.9.crate"
67 +
CHECKSUM_SHA256_19="31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
68 +
SOURCE_FILENAME_19="axum-0.8.9.tar.gz"
69 +
SOURCE_URI_20="https://static.crates.io/crates/axum-core/axum-core-0.5.6.crate"
70 +
CHECKSUM_SHA256_20="08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
71 +
SOURCE_FILENAME_20="axum-core-0.5.6.tar.gz"
72 +
SOURCE_URI_21="https://static.crates.io/crates/axum-server/axum-server-0.8.0.crate"
73 +
CHECKSUM_SHA256_21="b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc"
74 +
SOURCE_FILENAME_21="axum-server-0.8.0.tar.gz"
75 +
SOURCE_URI_22="https://static.crates.io/crates/base64/base64-0.22.1.crate"
76 +
CHECKSUM_SHA256_22="72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
77 +
SOURCE_FILENAME_22="base64-0.22.1.tar.gz"
78 +
SOURCE_URI_23="https://static.crates.io/crates/base64/base64-0.23.1.crate"
79 +
CHECKSUM_SHA256_23="ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
80 +
SOURCE_FILENAME_23="base64-0.23.1.tar.gz"
81 +
SOURCE_URI_24="https://static.crates.io/crates/base64ct/base64ct-1.8.3.crate"
82 +
CHECKSUM_SHA256_24="2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
83 +
SOURCE_FILENAME_24="base64ct-1.8.3.tar.gz"
84 +
SOURCE_URI_25="https://static.crates.io/crates/bit-set/bit-set-0.8.0.crate"
85 +
CHECKSUM_SHA256_25="08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
86 +
SOURCE_FILENAME_25="bit-set-0.8.0.tar.gz"
87 +
SOURCE_URI_26="https://static.crates.io/crates/bit-vec/bit-vec-0.8.0.crate"
88 +
CHECKSUM_SHA256_26="5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
89 +
SOURCE_FILENAME_26="bit-vec-0.8.0.tar.gz"
90 +
SOURCE_URI_27="https://static.crates.io/crates/bit-vec/bit-vec-0.9.1.crate"
91 +
CHECKSUM_SHA256_27="b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
92 +
SOURCE_FILENAME_27="bit-vec-0.9.1.tar.gz"
93 +
SOURCE_URI_28="https://static.crates.io/crates/bitflags/bitflags-2.13.2.crate"
94 +
CHECKSUM_SHA256_28="3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
95 +
SOURCE_FILENAME_28="bitflags-2.13.2.tar.gz"
96 +
SOURCE_URI_29="https://static.crates.io/crates/blake3/blake3-1.8.7.crate"
97 +
CHECKSUM_SHA256_29="6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
98 +
SOURCE_FILENAME_29="blake3-1.8.7.tar.gz"
99 +
SOURCE_URI_30="https://static.crates.io/crates/block-buffer/block-buffer-0.12.1.crate"
100 +
CHECKSUM_SHA256_30="d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
101 +
SOURCE_FILENAME_30="block-buffer-0.12.1.tar.gz"
102 +
SOURCE_URI_31="https://static.crates.io/crates/bstr/bstr-1.13.1.crate"
103 +
CHECKSUM_SHA256_31="6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
104 +
SOURCE_FILENAME_31="bstr-1.13.1.tar.gz"
105 +
SOURCE_URI_32="https://static.crates.io/crates/bumpalo/bumpalo-3.20.3.crate"
106 +
CHECKSUM_SHA256_32="72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
107 +
SOURCE_FILENAME_32="bumpalo-3.20.3.tar.gz"
108 +
SOURCE_URI_33="https://static.crates.io/crates/bytes/bytes-1.12.1.crate"
109 +
CHECKSUM_SHA256_33="fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
110 +
SOURCE_FILENAME_33="bytes-1.12.1.tar.gz"
111 +
SOURCE_URI_34="https://static.crates.io/crates/cc/cc-1.5.1.crate"
112 +
CHECKSUM_SHA256_34="f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
113 +
SOURCE_FILENAME_34="cc-1.5.1.tar.gz"
114 +
SOURCE_URI_35="https://static.crates.io/crates/cfg-if/cfg-if-1.0.5.crate"
115 +
CHECKSUM_SHA256_35="4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
116 +
SOURCE_FILENAME_35="cfg-if-1.0.5.tar.gz"
117 +
SOURCE_URI_36="https://static.crates.io/crates/chacha20/chacha20-0.10.2.crate"
118 +
CHECKSUM_SHA256_36="65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
119 +
SOURCE_FILENAME_36="chacha20-0.10.2.tar.gz"
120 +
SOURCE_URI_37="https://static.crates.io/crates/chrono/chrono-0.4.45.crate"
121 +
CHECKSUM_SHA256_37="1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
122 +
SOURCE_FILENAME_37="chrono-0.4.45.tar.gz"
123 +
SOURCE_URI_38="https://static.crates.io/crates/clap/clap-4.6.7.crate"
124 +
CHECKSUM_SHA256_38="aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
125 +
SOURCE_FILENAME_38="clap-4.6.7.tar.gz"
126 +
SOURCE_URI_39="https://static.crates.io/crates/clap_builder/clap_builder-4.6.7.crate"
127 +
CHECKSUM_SHA256_39="ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
128 +
SOURCE_FILENAME_39="clap_builder-4.6.7.tar.gz"
129 +
SOURCE_URI_40="https://static.crates.io/crates/clap_complete/clap_complete-4.6.11.crate"
130 +
CHECKSUM_SHA256_40="037e2a1a92236d0aff7e845093f64661d6df4c02c9fcc61a60e9e1d736fa392f"
131 +
SOURCE_FILENAME_40="clap_complete-4.6.11.tar.gz"
132 +
SOURCE_URI_41="https://static.crates.io/crates/clap_derive/clap_derive-4.6.7.crate"
133 +
CHECKSUM_SHA256_41="f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
134 +
SOURCE_FILENAME_41="clap_derive-4.6.7.tar.gz"
135 +
SOURCE_URI_42="https://static.crates.io/crates/clap_lex/clap_lex-1.1.1.crate"
136 +
CHECKSUM_SHA256_42="1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
137 +
SOURCE_FILENAME_42="clap_lex-1.1.1.tar.gz"
138 +
SOURCE_URI_43="https://static.crates.io/crates/clap_mangen/clap_mangen-0.3.3.crate"
139 +
CHECKSUM_SHA256_43="211d617eaa4b735c96c9e0228fcbdb5120ef623f2b8cb67ffb84c3e02dbc28a4"
140 +
SOURCE_FILENAME_43="clap_mangen-0.3.3.tar.gz"
141 +
SOURCE_URI_44="https://static.crates.io/crates/cmov/cmov-0.5.4.crate"
142 +
CHECKSUM_SHA256_44="0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
143 +
SOURCE_FILENAME_44="cmov-0.5.4.tar.gz"
144 +
SOURCE_URI_45="https://static.crates.io/crates/colorchoice/colorchoice-1.0.5.crate"
145 +
CHECKSUM_SHA256_45="1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
146 +
SOURCE_FILENAME_45="colorchoice-1.0.5.tar.gz"
147 +
SOURCE_URI_46="https://static.crates.io/crates/console/console-0.15.11.crate"
148 +
CHECKSUM_SHA256_46="054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
149 +
SOURCE_FILENAME_46="console-0.15.11.tar.gz"
150 +
SOURCE_URI_47="https://static.crates.io/crates/const-oid/const-oid-0.10.2.crate"
151 +
CHECKSUM_SHA256_47="a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
152 +
SOURCE_FILENAME_47="const-oid-0.10.2.tar.gz"
153 +
SOURCE_URI_48="https://static.crates.io/crates/constant_time_eq/constant_time_eq-0.4.2.crate"
154 +
CHECKSUM_SHA256_48="3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
155 +
SOURCE_FILENAME_48="constant_time_eq-0.4.2.tar.gz"
156 +
SOURCE_URI_49="https://static.crates.io/crates/cpufeatures/cpufeatures-0.3.1.crate"
157 +
CHECKSUM_SHA256_49="5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
158 +
SOURCE_FILENAME_49="cpufeatures-0.3.1.tar.gz"
159 +
SOURCE_URI_50="https://static.crates.io/crates/crc32fast/crc32fast-1.5.2.crate"
160 +
CHECKSUM_SHA256_50="01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
161 +
SOURCE_FILENAME_50="crc32fast-1.5.2.tar.gz"
162 +
SOURCE_URI_51="https://static.crates.io/crates/crossbeam-deque/crossbeam-deque-0.8.8.crate"
163 +
CHECKSUM_SHA256_51="622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
164 +
SOURCE_FILENAME_51="crossbeam-deque-0.8.8.tar.gz"
165 +
SOURCE_URI_52="https://static.crates.io/crates/crossbeam-epoch/crossbeam-epoch-0.9.21.crate"
166 +
CHECKSUM_SHA256_52="dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
167 +
SOURCE_FILENAME_52="crossbeam-epoch-0.9.21.tar.gz"
168 +
SOURCE_URI_53="https://static.crates.io/crates/crossbeam-utils/crossbeam-utils-0.8.23.crate"
169 +
CHECKSUM_SHA256_53="a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
170 +
SOURCE_FILENAME_53="crossbeam-utils-0.8.23.tar.gz"
171 +
SOURCE_URI_54="https://static.crates.io/crates/crypto-common/crypto-common-0.1.7.crate"
172 +
CHECKSUM_SHA256_54="78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
173 +
SOURCE_FILENAME_54="crypto-common-0.1.7.tar.gz"
174 +
SOURCE_URI_55="https://static.crates.io/crates/crypto-common/crypto-common-0.2.2.crate"
175 +
CHECKSUM_SHA256_55="ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
176 +
SOURCE_FILENAME_55="crypto-common-0.2.2.tar.gz"
177 +
SOURCE_URI_56="https://static.crates.io/crates/ctutils/ctutils-0.4.2.crate"
178 +
CHECKSUM_SHA256_56="7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
179 +
SOURCE_FILENAME_56="ctutils-0.4.2.tar.gz"
180 +
SOURCE_URI_57="https://static.crates.io/crates/curve25519-dalek/curve25519-dalek-5.0.0.crate"
181 +
CHECKSUM_SHA256_57="b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
182 +
SOURCE_FILENAME_57="curve25519-dalek-5.0.0.tar.gz"
183 +
SOURCE_URI_58="https://static.crates.io/crates/curve25519-dalek-derive/curve25519-dalek-derive-0.1.1.crate"
184 +
CHECKSUM_SHA256_58="f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
185 +
SOURCE_FILENAME_58="curve25519-dalek-derive-0.1.1.tar.gz"
186 +
SOURCE_URI_59="https://static.crates.io/crates/data-encoding/data-encoding-2.11.1.crate"
187 +
CHECKSUM_SHA256_59="4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
188 +
SOURCE_FILENAME_59="data-encoding-2.11.1.tar.gz"
189 +
SOURCE_URI_60="https://static.crates.io/crates/der/der-0.8.2.crate"
190 +
CHECKSUM_SHA256_60="a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a"
191 +
SOURCE_FILENAME_60="der-0.8.2.tar.gz"
192 +
SOURCE_URI_61="https://static.crates.io/crates/der-parser/der-parser-10.0.0.crate"
193 +
CHECKSUM_SHA256_61="07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
194 +
SOURCE_FILENAME_61="der-parser-10.0.0.tar.gz"
195 +
SOURCE_URI_62="https://static.crates.io/crates/deranged/deranged-0.5.8.crate"
196 +
CHECKSUM_SHA256_62="7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
197 +
SOURCE_FILENAME_62="deranged-0.5.8.tar.gz"
198 +
SOURCE_URI_63="https://static.crates.io/crates/digest/digest-0.10.7.crate"
199 +
CHECKSUM_SHA256_63="9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
200 +
SOURCE_FILENAME_63="digest-0.10.7.tar.gz"
201 +
SOURCE_URI_64="https://static.crates.io/crates/digest/digest-0.11.3.crate"
202 +
CHECKSUM_SHA256_64="f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
203 +
SOURCE_FILENAME_64="digest-0.11.3.tar.gz"
204 +
SOURCE_URI_65="https://static.crates.io/crates/displaydoc/displaydoc-0.2.7.crate"
205 +
CHECKSUM_SHA256_65="c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
206 +
SOURCE_FILENAME_65="displaydoc-0.2.7.tar.gz"
207 +
SOURCE_URI_66="https://static.crates.io/crates/ed25519/ed25519-3.0.0.crate"
208 +
CHECKSUM_SHA256_66="29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"
209 +
SOURCE_FILENAME_66="ed25519-3.0.0.tar.gz"
210 +
SOURCE_URI_67="https://static.crates.io/crates/ed25519-dalek/ed25519-dalek-3.0.0.crate"
211 +
CHECKSUM_SHA256_67="6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
212 +
SOURCE_FILENAME_67="ed25519-dalek-3.0.0.tar.gz"
213 +
SOURCE_URI_68="https://static.crates.io/crates/either/either-1.18.0.crate"
214 +
CHECKSUM_SHA256_68="252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
215 +
SOURCE_FILENAME_68="either-1.18.0.tar.gz"
216 +
SOURCE_URI_69="https://static.crates.io/crates/encode_unicode/encode_unicode-1.0.0.crate"
217 +
CHECKSUM_SHA256_69="34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
218 +
SOURCE_FILENAME_69="encode_unicode-1.0.0.tar.gz"
219 +
SOURCE_URI_70="https://static.crates.io/crates/equivalent/equivalent-1.0.2.crate"
220 +
CHECKSUM_SHA256_70="877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
221 +
SOURCE_FILENAME_70="equivalent-1.0.2.tar.gz"
222 +
SOURCE_URI_71="https://static.crates.io/crates/errno/errno-0.3.14.crate"
223 +
CHECKSUM_SHA256_71="39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
224 +
SOURCE_FILENAME_71="errno-0.3.14.tar.gz"
225 +
SOURCE_URI_72="https://static.crates.io/crates/fastrand/fastrand-2.5.0.crate"
226 +
CHECKSUM_SHA256_72="da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
227 +
SOURCE_FILENAME_72="fastrand-2.5.0.tar.gz"
228 +
SOURCE_URI_73="https://static.crates.io/crates/fiat-crypto/fiat-crypto-0.3.0.crate"
229 +
CHECKSUM_SHA256_73="64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
230 +
SOURCE_FILENAME_73="fiat-crypto-0.3.0.tar.gz"
231 +
SOURCE_URI_74="https://static.crates.io/crates/find-msvc-tools/find-msvc-tools-0.1.14.crate"
232 +
CHECKSUM_SHA256_74="aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
233 +
SOURCE_FILENAME_74="find-msvc-tools-0.1.14.tar.gz"
234 +
SOURCE_URI_75="https://static.crates.io/crates/flate2/flate2-1.1.10.crate"
235 +
CHECKSUM_SHA256_75="6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
236 +
SOURCE_FILENAME_75="flate2-1.1.10.tar.gz"
237 +
SOURCE_URI_76="https://static.crates.io/crates/fnv/fnv-1.0.7.crate"
238 +
CHECKSUM_SHA256_76="3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
239 +
SOURCE_FILENAME_76="fnv-1.0.7.tar.gz"
240 +
SOURCE_URI_77="https://static.crates.io/crates/form_urlencoded/form_urlencoded-1.2.2.crate"
241 +
CHECKSUM_SHA256_77="cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
242 +
SOURCE_FILENAME_77="form_urlencoded-1.2.2.tar.gz"
243 +
SOURCE_URI_78="https://static.crates.io/crates/fs-err/fs-err-3.3.1.crate"
244 +
CHECKSUM_SHA256_78="b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a"
245 +
SOURCE_FILENAME_78="fs-err-3.3.1.tar.gz"
246 +
SOURCE_URI_79="https://static.crates.io/crates/futures/futures-0.3.34.crate"
247 +
CHECKSUM_SHA256_79="9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
248 +
SOURCE_FILENAME_79="futures-0.3.34.tar.gz"
249 +
SOURCE_URI_80="https://static.crates.io/crates/futures-channel/futures-channel-0.3.34.crate"
250 +
CHECKSUM_SHA256_80="b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
251 +
SOURCE_FILENAME_80="futures-channel-0.3.34.tar.gz"
252 +
SOURCE_URI_81="https://static.crates.io/crates/futures-core/futures-core-0.3.34.crate"
253 +
CHECKSUM_SHA256_81="92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
254 +
SOURCE_FILENAME_81="futures-core-0.3.34.tar.gz"
255 +
SOURCE_URI_82="https://static.crates.io/crates/futures-executor/futures-executor-0.3.34.crate"
256 +
CHECKSUM_SHA256_82="031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
257 +
SOURCE_FILENAME_82="futures-executor-0.3.34.tar.gz"
258 +
SOURCE_URI_83="https://static.crates.io/crates/futures-io/futures-io-0.3.34.crate"
259 +
CHECKSUM_SHA256_83="53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
260 +
SOURCE_FILENAME_83="futures-io-0.3.34.tar.gz"
261 +
SOURCE_URI_84="https://static.crates.io/crates/futures-macro/futures-macro-0.3.34.crate"
262 +
CHECKSUM_SHA256_84="9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
263 +
SOURCE_FILENAME_84="futures-macro-0.3.34.tar.gz"
264 +
SOURCE_URI_85="https://static.crates.io/crates/futures-sink/futures-sink-0.3.34.crate"
265 +
CHECKSUM_SHA256_85="1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
266 +
SOURCE_FILENAME_85="futures-sink-0.3.34.tar.gz"
267 +
SOURCE_URI_86="https://static.crates.io/crates/futures-task/futures-task-0.3.34.crate"
268 +
CHECKSUM_SHA256_86="cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
269 +
SOURCE_FILENAME_86="futures-task-0.3.34.tar.gz"
270 +
SOURCE_URI_87="https://static.crates.io/crates/futures-util/futures-util-0.3.34.crate"
271 +
CHECKSUM_SHA256_87="0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
272 +
SOURCE_FILENAME_87="futures-util-0.3.34.tar.gz"
273 +
SOURCE_URI_88="https://static.crates.io/crates/generic-array/generic-array-0.14.7.crate"
274 +
CHECKSUM_SHA256_88="85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
275 +
SOURCE_FILENAME_88="generic-array-0.14.7.tar.gz"
276 +
SOURCE_URI_89="https://static.crates.io/crates/getrandom/getrandom-0.2.17.crate"
277 +
CHECKSUM_SHA256_89="ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
278 +
SOURCE_FILENAME_89="getrandom-0.2.17.tar.gz"
279 +
SOURCE_URI_90="https://static.crates.io/crates/getrandom/getrandom-0.3.4.crate"
280 +
CHECKSUM_SHA256_90="899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
281 +
SOURCE_FILENAME_90="getrandom-0.3.4.tar.gz"
282 +
SOURCE_URI_91="https://static.crates.io/crates/getrandom/getrandom-0.4.3.crate"
283 +
CHECKSUM_SHA256_91="300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
284 +
SOURCE_FILENAME_91="getrandom-0.4.3.tar.gz"
285 +
SOURCE_URI_92="https://static.crates.io/crates/globset/globset-0.4.20.crate"
286 +
CHECKSUM_SHA256_92="07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
287 +
SOURCE_FILENAME_92="globset-0.4.20.tar.gz"
288 +
SOURCE_URI_93="https://static.crates.io/crates/h2/h2-0.4.19.crate"
289 +
CHECKSUM_SHA256_93="ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
290 +
SOURCE_FILENAME_93="h2-0.4.19.tar.gz"
291 +
SOURCE_URI_94="https://static.crates.io/crates/hashbrown/hashbrown-0.17.1.crate"
292 +
CHECKSUM_SHA256_94="ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
293 +
SOURCE_FILENAME_94="hashbrown-0.17.1.tar.gz"
294 +
SOURCE_URI_95="https://static.crates.io/crates/heck/heck-0.5.0.crate"
295 +
CHECKSUM_SHA256_95="2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
296 +
SOURCE_FILENAME_95="heck-0.5.0.tar.gz"
297 +
SOURCE_URI_96="https://static.crates.io/crates/hex/hex-0.4.3.crate"
298 +
CHECKSUM_SHA256_96="7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
299 +
SOURCE_FILENAME_96="hex-0.4.3.tar.gz"
300 +
SOURCE_URI_97="https://static.crates.io/crates/http/http-1.5.0.crate"
301 +
CHECKSUM_SHA256_97="918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
302 +
SOURCE_FILENAME_97="http-1.5.0.tar.gz"
303 +
SOURCE_URI_98="https://static.crates.io/crates/http-body/http-body-1.1.0.crate"
304 +
CHECKSUM_SHA256_98="ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
305 +
SOURCE_FILENAME_98="http-body-1.1.0.tar.gz"
306 +
SOURCE_URI_99="https://static.crates.io/crates/http-body-util/http-body-util-0.1.5.crate"
307 +
CHECKSUM_SHA256_99="23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
308 +
SOURCE_FILENAME_99="http-body-util-0.1.5.tar.gz"
309 +
SOURCE_URI_100="https://static.crates.io/crates/httparse/httparse-1.10.1.crate"
310 +
CHECKSUM_SHA256_100="6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
311 +
SOURCE_FILENAME_100="httparse-1.10.1.tar.gz"
312 +
SOURCE_URI_101="https://static.crates.io/crates/httpdate/httpdate-1.0.3.crate"
313 +
CHECKSUM_SHA256_101="df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
314 +
SOURCE_FILENAME_101="httpdate-1.0.3.tar.gz"
315 +
SOURCE_URI_102="https://static.crates.io/crates/humantime/humantime-2.4.0.crate"
316 +
CHECKSUM_SHA256_102="15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
317 +
SOURCE_FILENAME_102="humantime-2.4.0.tar.gz"
318 +
SOURCE_URI_103="https://static.crates.io/crates/hybrid-array/hybrid-array-0.4.15.crate"
319 +
CHECKSUM_SHA256_103="27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
320 +
SOURCE_FILENAME_103="hybrid-array-0.4.15.tar.gz"
321 +
SOURCE_URI_104="https://static.crates.io/crates/hyper/hyper-1.11.1.crate"
322 +
CHECKSUM_SHA256_104="27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
323 +
SOURCE_FILENAME_104="hyper-1.11.1.tar.gz"
324 +
SOURCE_URI_105="https://static.crates.io/crates/hyper-util/hyper-util-0.1.21.crate"
325 +
CHECKSUM_SHA256_105="ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
326 +
SOURCE_FILENAME_105="hyper-util-0.1.21.tar.gz"
327 +
SOURCE_URI_106="https://static.crates.io/crates/iana-time-zone-haiku/iana-time-zone-haiku-0.1.2.crate"
328 +
CHECKSUM_SHA256_106="f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
329 +
SOURCE_FILENAME_106="iana-time-zone-haiku-0.1.2.tar.gz"
330 +
SOURCE_URI_107="https://static.crates.io/crates/icu_collections/icu_collections-2.3.0.crate"
331 +
CHECKSUM_SHA256_107="fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
332 +
SOURCE_FILENAME_107="icu_collections-2.3.0.tar.gz"
333 +
SOURCE_URI_108="https://static.crates.io/crates/icu_locale_core/icu_locale_core-2.3.0.crate"
334 +
CHECKSUM_SHA256_108="d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
335 +
SOURCE_FILENAME_108="icu_locale_core-2.3.0.tar.gz"
336 +
SOURCE_URI_109="https://static.crates.io/crates/icu_normalizer/icu_normalizer-2.3.0.crate"
337 +
CHECKSUM_SHA256_109="12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
338 +
SOURCE_FILENAME_109="icu_normalizer-2.3.0.tar.gz"
339 +
SOURCE_URI_110="https://static.crates.io/crates/icu_normalizer_data/icu_normalizer_data-2.3.0.crate"
340 +
CHECKSUM_SHA256_110="1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
341 +
SOURCE_FILENAME_110="icu_normalizer_data-2.3.0.tar.gz"
342 +
SOURCE_URI_111="https://static.crates.io/crates/icu_properties/icu_properties-2.3.0.crate"
343 +
CHECKSUM_SHA256_111="7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
344 +
SOURCE_FILENAME_111="icu_properties-2.3.0.tar.gz"
345 +
SOURCE_URI_112="https://static.crates.io/crates/icu_properties_data/icu_properties_data-2.3.0.crate"
346 +
CHECKSUM_SHA256_112="e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
347 +
SOURCE_FILENAME_112="icu_properties_data-2.3.0.tar.gz"
348 +
SOURCE_URI_113="https://static.crates.io/crates/icu_provider/icu_provider-2.3.1.crate"
349 +
CHECKSUM_SHA256_113="d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
350 +
SOURCE_FILENAME_113="icu_provider-2.3.1.tar.gz"
351 +
SOURCE_URI_114="https://static.crates.io/crates/idna/idna-1.1.0.crate"
352 +
CHECKSUM_SHA256_114="3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
353 +
SOURCE_FILENAME_114="idna-1.1.0.tar.gz"
354 +
SOURCE_URI_115="https://static.crates.io/crates/idna_adapter/idna_adapter-1.2.2.crate"
355 +
CHECKSUM_SHA256_115="cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
356 +
SOURCE_FILENAME_115="idna_adapter-1.2.2.tar.gz"
357 +
SOURCE_URI_116="https://static.crates.io/crates/ignore/ignore-0.4.33.crate"
358 +
CHECKSUM_SHA256_116="00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f"
359 +
SOURCE_FILENAME_116="ignore-0.4.33.tar.gz"
360 +
SOURCE_URI_117="https://static.crates.io/crates/indexmap/indexmap-2.14.2.crate"
361 +
CHECKSUM_SHA256_117="cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
362 +
SOURCE_FILENAME_117="indexmap-2.14.2.tar.gz"
363 +
SOURCE_URI_118="https://static.crates.io/crates/indicatif/indicatif-0.17.11.crate"
364 +
CHECKSUM_SHA256_118="183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
365 +
SOURCE_FILENAME_118="indicatif-0.17.11.tar.gz"
366 +
SOURCE_URI_119="https://static.crates.io/crates/is_terminal_polyfill/is_terminal_polyfill-1.70.2.crate"
367 +
CHECKSUM_SHA256_119="a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
368 +
SOURCE_FILENAME_119="is_terminal_polyfill-1.70.2.tar.gz"
369 +
SOURCE_URI_120="https://static.crates.io/crates/itertools/itertools-0.15.0.crate"
370 +
CHECKSUM_SHA256_120="8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
371 +
SOURCE_FILENAME_120="itertools-0.15.0.tar.gz"
372 +
SOURCE_URI_121="https://static.crates.io/crates/itoa/itoa-1.0.18.crate"
373 +
CHECKSUM_SHA256_121="8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
374 +
SOURCE_FILENAME_121="itoa-1.0.18.tar.gz"
375 +
SOURCE_URI_122="https://static.crates.io/crates/jobserver/jobserver-0.1.35.crate"
376 +
CHECKSUM_SHA256_122="1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
377 +
SOURCE_FILENAME_122="jobserver-0.1.35.tar.gz"
378 +
SOURCE_URI_123="https://static.crates.io/crates/js-sys/js-sys-0.3.106.crate"
379 +
CHECKSUM_SHA256_123="7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
380 +
SOURCE_FILENAME_123="js-sys-0.3.106.tar.gz"
381 +
SOURCE_URI_124="https://static.crates.io/crates/keccak/keccak-0.2.2.crate"
382 +
CHECKSUM_SHA256_124="d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598"
383 +
SOURCE_FILENAME_124="keccak-0.2.2.tar.gz"
384 +
SOURCE_URI_125="https://static.crates.io/crates/lazy_static/lazy_static-1.5.0.crate"
385 +
CHECKSUM_SHA256_125="bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
386 +
SOURCE_FILENAME_125="lazy_static-1.5.0.tar.gz"
387 +
SOURCE_URI_126="https://static.crates.io/crates/libc/libc-0.2.189.crate"
388 +
CHECKSUM_SHA256_126="3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
389 +
SOURCE_FILENAME_126="libc-0.2.189.tar.gz"
390 +
SOURCE_URI_127="https://static.crates.io/crates/libmimalloc-sys/libmimalloc-sys-0.1.49.crate"
391 +
CHECKSUM_SHA256_127="6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
392 +
SOURCE_FILENAME_127="libmimalloc-sys-0.1.49.tar.gz"
393 +
SOURCE_URI_128="https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.12.1.crate"
394 +
CHECKSUM_SHA256_128="32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
395 +
SOURCE_FILENAME_128="linux-raw-sys-0.12.1.tar.gz"
396 +
SOURCE_URI_129="https://static.crates.io/crates/litemap/litemap-0.8.3.crate"
397 +
CHECKSUM_SHA256_129="47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
398 +
SOURCE_FILENAME_129="litemap-0.8.3.tar.gz"
399 +
SOURCE_URI_130="https://static.crates.io/crates/lock_api/lock_api-0.4.14.crate"
400 +
CHECKSUM_SHA256_130="224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
401 +
SOURCE_FILENAME_130="lock_api-0.4.14.tar.gz"
402 +
SOURCE_URI_131="https://static.crates.io/crates/log/log-0.4.34.crate"
403 +
CHECKSUM_SHA256_131="f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
404 +
SOURCE_FILENAME_131="log-0.4.34.tar.gz"
405 +
SOURCE_URI_132="https://static.crates.io/crates/matchit/matchit-0.8.4.crate"
406 +
CHECKSUM_SHA256_132="47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
407 +
SOURCE_FILENAME_132="matchit-0.8.4.tar.gz"
408 +
SOURCE_URI_133="https://static.crates.io/crates/md-5/md-5-0.11.0.crate"
409 +
CHECKSUM_SHA256_133="69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
410 +
SOURCE_FILENAME_133="md-5-0.11.0.tar.gz"
411 +
SOURCE_URI_134="https://static.crates.io/crates/memchr/memchr-2.8.3.crate"
412 +
CHECKSUM_SHA256_134="cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
413 +
SOURCE_FILENAME_134="memchr-2.8.3.tar.gz"
414 +
SOURCE_URI_135="https://static.crates.io/crates/mimalloc/mimalloc-0.1.52.crate"
415 +
CHECKSUM_SHA256_135="2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
416 +
SOURCE_FILENAME_135="mimalloc-0.1.52.tar.gz"
417 +
SOURCE_URI_136="https://static.crates.io/crates/mime/mime-0.3.17.crate"
418 +
CHECKSUM_SHA256_136="6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
419 +
SOURCE_FILENAME_136="mime-0.3.17.tar.gz"
420 +
SOURCE_URI_137="https://static.crates.io/crates/minimal-lexical/minimal-lexical-0.2.1.crate"
421 +
CHECKSUM_SHA256_137="68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
422 +
SOURCE_FILENAME_137="minimal-lexical-0.2.1.tar.gz"
423 +
SOURCE_URI_138="https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.9.1.crate"
424 +
CHECKSUM_SHA256_138="b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
425 +
SOURCE_FILENAME_138="miniz_oxide-0.9.1.tar.gz"
426 +
SOURCE_URI_139="https://static.crates.io/crates/mio/mio-1.2.3.crate"
427 +
CHECKSUM_SHA256_139="4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
428 +
SOURCE_FILENAME_139="mio-1.2.3.tar.gz"
429 +
SOURCE_URI_140="https://static.crates.io/crates/ml-dsa/ml-dsa-0.1.1.crate"
430 +
CHECKSUM_SHA256_140="add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d"
431 +
SOURCE_FILENAME_140="ml-dsa-0.1.1.tar.gz"
432 +
SOURCE_URI_141="https://static.crates.io/crates/module-lattice/module-lattice-0.2.3.crate"
433 +
CHECKSUM_SHA256_141="0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe"
434 +
SOURCE_FILENAME_141="module-lattice-0.2.3.tar.gz"
435 +
SOURCE_URI_142="https://static.crates.io/crates/nom/nom-7.1.3.crate"
436 +
CHECKSUM_SHA256_142="d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
437 +
SOURCE_FILENAME_142="nom-7.1.3.tar.gz"
438 +
SOURCE_URI_143="https://static.crates.io/crates/num-bigint/num-bigint-0.4.8.crate"
439 +
CHECKSUM_SHA256_143="c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
440 +
SOURCE_FILENAME_143="num-bigint-0.4.8.tar.gz"
441 +
SOURCE_URI_144="https://static.crates.io/crates/num-conv/num-conv-0.2.2.crate"
442 +
CHECKSUM_SHA256_144="521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
443 +
SOURCE_FILENAME_144="num-conv-0.2.2.tar.gz"
444 +
SOURCE_URI_145="https://static.crates.io/crates/num-integer/num-integer-0.1.47.crate"
445 +
CHECKSUM_SHA256_145="7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
446 +
SOURCE_FILENAME_145="num-integer-0.1.47.tar.gz"
447 +
SOURCE_URI_146="https://static.crates.io/crates/num-traits/num-traits-0.2.19.crate"
448 +
CHECKSUM_SHA256_146="071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
449 +
SOURCE_FILENAME_146="num-traits-0.2.19.tar.gz"
450 +
SOURCE_URI_147="https://static.crates.io/crates/number_prefix/number_prefix-0.4.0.crate"
451 +
CHECKSUM_SHA256_147="830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
452 +
SOURCE_FILENAME_147="number_prefix-0.4.0.tar.gz"
453 +
SOURCE_URI_148="https://static.crates.io/crates/object_store/object_store-0.14.2.crate"
454 +
CHECKSUM_SHA256_148="f1796bc93603f78c5760a69f2d58badc9618d22adade0a95385bb2adbae4eb94"
455 +
SOURCE_FILENAME_148="object_store-0.14.2.tar.gz"
456 +
SOURCE_URI_149="https://static.crates.io/crates/oid-registry/oid-registry-0.8.1.crate"
457 +
CHECKSUM_SHA256_149="12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
458 +
SOURCE_FILENAME_149="oid-registry-0.8.1.tar.gz"
459 +
SOURCE_URI_150="https://static.crates.io/crates/once_cell/once_cell-1.21.4.crate"
460 +
CHECKSUM_SHA256_150="9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
461 +
SOURCE_FILENAME_150="once_cell-1.21.4.tar.gz"
462 +
SOURCE_URI_151="https://static.crates.io/crates/once_cell_polyfill/once_cell_polyfill-1.70.2.crate"
463 +
CHECKSUM_SHA256_151="384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
464 +
SOURCE_FILENAME_151="once_cell_polyfill-1.70.2.tar.gz"
465 +
SOURCE_URI_152="https://static.crates.io/crates/parking_lot/parking_lot-0.12.5.crate"
466 +
CHECKSUM_SHA256_152="93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
467 +
SOURCE_FILENAME_152="parking_lot-0.12.5.tar.gz"
468 +
SOURCE_URI_153="https://static.crates.io/crates/parking_lot_core/parking_lot_core-0.9.12.crate"
469 +
CHECKSUM_SHA256_153="2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
470 +
SOURCE_FILENAME_153="parking_lot_core-0.9.12.tar.gz"
471 +
SOURCE_URI_154="https://static.crates.io/crates/pem/pem-4.0.0.crate"
472 +
CHECKSUM_SHA256_154="d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
473 +
SOURCE_FILENAME_154="pem-4.0.0.tar.gz"
474 +
SOURCE_URI_155="https://static.crates.io/crates/percent-encoding/percent-encoding-2.3.2.crate"
475 +
CHECKSUM_SHA256_155="9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
476 +
SOURCE_FILENAME_155="percent-encoding-2.3.2.tar.gz"
477 +
SOURCE_URI_156="https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.17.crate"
478 +
CHECKSUM_SHA256_156="a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
479 +
SOURCE_FILENAME_156="pin-project-lite-0.2.17.tar.gz"
480 +
SOURCE_URI_157="https://static.crates.io/crates/pkcs8/pkcs8-0.11.0.crate"
481 +
CHECKSUM_SHA256_157="451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
482 +
SOURCE_FILENAME_157="pkcs8-0.11.0.tar.gz"
483 +
SOURCE_URI_158="https://static.crates.io/crates/pkg-config/pkg-config-0.3.34.crate"
484 +
CHECKSUM_SHA256_158="f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
485 +
SOURCE_FILENAME_158="pkg-config-0.3.34.tar.gz"
486 +
SOURCE_URI_159="https://static.crates.io/crates/portable-atomic/portable-atomic-1.15.0.crate"
487 +
CHECKSUM_SHA256_159="05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
488 +
SOURCE_FILENAME_159="portable-atomic-1.15.0.tar.gz"
489 +
SOURCE_URI_160="https://static.crates.io/crates/potential_utf/potential_utf-0.1.6.crate"
490 +
CHECKSUM_SHA256_160="d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
491 +
SOURCE_FILENAME_160="potential_utf-0.1.6.tar.gz"
492 +
SOURCE_URI_161="https://static.crates.io/crates/powerfmt/powerfmt-0.2.0.crate"
493 +
CHECKSUM_SHA256_161="439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
494 +
SOURCE_FILENAME_161="powerfmt-0.2.0.tar.gz"
495 +
SOURCE_URI_162="https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.21.crate"
496 +
CHECKSUM_SHA256_162="85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
497 +
SOURCE_FILENAME_162="ppv-lite86-0.2.21.tar.gz"
498 +
SOURCE_URI_163="https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.107.crate"
499 +
CHECKSUM_SHA256_163="985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
500 +
SOURCE_FILENAME_163="proc-macro2-1.0.107.tar.gz"
501 +
SOURCE_URI_164="https://static.crates.io/crates/proptest/proptest-1.11.0.crate"
502 +
CHECKSUM_SHA256_164="4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
503 +
SOURCE_FILENAME_164="proptest-1.11.0.tar.gz"
504 +
SOURCE_URI_165="https://static.crates.io/crates/quick-error/quick-error-1.2.3.crate"
505 +
CHECKSUM_SHA256_165="a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
506 +
SOURCE_FILENAME_165="quick-error-1.2.3.tar.gz"
507 +
SOURCE_URI_166="https://static.crates.io/crates/quick-xml/quick-xml-0.41.0.crate"
508 +
CHECKSUM_SHA256_166="e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
509 +
SOURCE_FILENAME_166="quick-xml-0.41.0.tar.gz"
510 +
SOURCE_URI_167="https://static.crates.io/crates/quote/quote-1.0.47.crate"
511 +
CHECKSUM_SHA256_167="1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
512 +
SOURCE_FILENAME_167="quote-1.0.47.tar.gz"
513 +
SOURCE_URI_168="https://static.crates.io/crates/r-efi/r-efi-5.3.0.crate"
514 +
CHECKSUM_SHA256_168="69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
515 +
SOURCE_FILENAME_168="r-efi-5.3.0.tar.gz"
516 +
SOURCE_URI_169="https://static.crates.io/crates/r-efi/r-efi-6.0.0.crate"
517 +
CHECKSUM_SHA256_169="f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
518 +
SOURCE_FILENAME_169="r-efi-6.0.0.tar.gz"
519 +
SOURCE_URI_170="https://static.crates.io/crates/rand/rand-0.10.3.crate"
520 +
CHECKSUM_SHA256_170="65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
521 +
SOURCE_FILENAME_170="rand-0.10.3.tar.gz"
522 +
SOURCE_URI_171="https://static.crates.io/crates/rand/rand-0.8.8.crate"
523 +
CHECKSUM_SHA256_171="e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
524 +
SOURCE_FILENAME_171="rand-0.8.8.tar.gz"
525 +
SOURCE_URI_172="https://static.crates.io/crates/rand/rand-0.9.5.crate"
526 +
CHECKSUM_SHA256_172="b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
527 +
SOURCE_FILENAME_172="rand-0.9.5.tar.gz"
528 +
SOURCE_URI_173="https://static.crates.io/crates/rand_chacha/rand_chacha-0.3.1.crate"
529 +
CHECKSUM_SHA256_173="e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
530 +
SOURCE_FILENAME_173="rand_chacha-0.3.1.tar.gz"
531 +
SOURCE_URI_174="https://static.crates.io/crates/rand_chacha/rand_chacha-0.9.0.crate"
532 +
CHECKSUM_SHA256_174="d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
533 +
SOURCE_FILENAME_174="rand_chacha-0.9.0.tar.gz"
534 +
SOURCE_URI_175="https://static.crates.io/crates/rand_core/rand_core-0.10.1.crate"
535 +
CHECKSUM_SHA256_175="63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
536 +
SOURCE_FILENAME_175="rand_core-0.10.1.tar.gz"
537 +
SOURCE_URI_176="https://static.crates.io/crates/rand_core/rand_core-0.6.4.crate"
538 +
CHECKSUM_SHA256_176="ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
539 +
SOURCE_FILENAME_176="rand_core-0.6.4.tar.gz"
540 +
SOURCE_URI_177="https://static.crates.io/crates/rand_core/rand_core-0.9.5.crate"
541 +
CHECKSUM_SHA256_177="76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
542 +
SOURCE_FILENAME_177="rand_core-0.9.5.tar.gz"
543 +
SOURCE_URI_178="https://static.crates.io/crates/rand_xorshift/rand_xorshift-0.4.0.crate"
544 +
CHECKSUM_SHA256_178="513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
545 +
SOURCE_FILENAME_178="rand_xorshift-0.4.0.tar.gz"
546 +
SOURCE_URI_179="https://static.crates.io/crates/rayon/rayon-1.12.0.crate"
547 +
CHECKSUM_SHA256_179="fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
548 +
SOURCE_FILENAME_179="rayon-1.12.0.tar.gz"
549 +
SOURCE_URI_180="https://static.crates.io/crates/rayon-core/rayon-core-1.13.0.crate"
550 +
CHECKSUM_SHA256_180="22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
551 +
SOURCE_FILENAME_180="rayon-core-1.13.0.tar.gz"
552 +
SOURCE_URI_181="https://static.crates.io/crates/rcgen/rcgen-0.14.10.crate"
553 +
CHECKSUM_SHA256_181="8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8"
554 +
SOURCE_FILENAME_181="rcgen-0.14.10.tar.gz"
555 +
SOURCE_URI_182="https://static.crates.io/crates/redox_syscall/redox_syscall-0.5.18.crate"
556 +
CHECKSUM_SHA256_182="ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
557 +
SOURCE_FILENAME_182="redox_syscall-0.5.18.tar.gz"
558 +
SOURCE_URI_183="https://static.crates.io/crates/regex-automata/regex-automata-0.4.18.crate"
559 +
CHECKSUM_SHA256_183="ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
560 +
SOURCE_FILENAME_183="regex-automata-0.4.18.tar.gz"
561 +
SOURCE_URI_184="https://static.crates.io/crates/regex-syntax/regex-syntax-0.8.11.crate"
562 +
CHECKSUM_SHA256_184="d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
563 +
SOURCE_FILENAME_184="regex-syntax-0.8.11.tar.gz"
564 +
SOURCE_URI_185="https://static.crates.io/crates/ring/ring-0.17.14.crate"
565 +
CHECKSUM_SHA256_185="a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
566 +
SOURCE_FILENAME_185="ring-0.17.14.tar.gz"
567 +
SOURCE_URI_186="https://static.crates.io/crates/roff/roff-1.1.1.crate"
568 +
CHECKSUM_SHA256_186="323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189"
569 +
SOURCE_FILENAME_186="roff-1.1.1.tar.gz"
570 +
SOURCE_URI_187="https://static.crates.io/crates/rustc_version/rustc_version-0.4.1.crate"
571 +
CHECKSUM_SHA256_187="cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
572 +
SOURCE_FILENAME_187="rustc_version-0.4.1.tar.gz"
573 +
SOURCE_URI_188="https://static.crates.io/crates/rusticata-macros/rusticata-macros-4.1.0.crate"
574 +
CHECKSUM_SHA256_188="faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
575 +
SOURCE_FILENAME_188="rusticata-macros-4.1.0.tar.gz"
576 +
SOURCE_URI_189="https://static.crates.io/crates/rustix/rustix-1.1.5.crate"
577 +
CHECKSUM_SHA256_189="891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
578 +
SOURCE_FILENAME_189="rustix-1.1.5.tar.gz"
579 +
SOURCE_URI_190="https://static.crates.io/crates/rustls/rustls-0.23.45.crate"
580 +
CHECKSUM_SHA256_190="0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
581 +
SOURCE_FILENAME_190="rustls-0.23.45.tar.gz"
582 +
SOURCE_URI_191="https://static.crates.io/crates/rustls-pemfile/rustls-pemfile-2.2.0.crate"
583 +
CHECKSUM_SHA256_191="dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
584 +
SOURCE_FILENAME_191="rustls-pemfile-2.2.0.tar.gz"
585 +
SOURCE_URI_192="https://static.crates.io/crates/rustls-pki-types/rustls-pki-types-1.15.1.crate"
586 +
CHECKSUM_SHA256_192="2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
587 +
SOURCE_FILENAME_192="rustls-pki-types-1.15.1.tar.gz"
588 +
SOURCE_URI_193="https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.103.15.crate"
589 +
CHECKSUM_SHA256_193="f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
590 +
SOURCE_FILENAME_193="rustls-webpki-0.103.15.tar.gz"
591 +
SOURCE_URI_194="https://static.crates.io/crates/rustversion/rustversion-1.0.23.crate"
592 +
CHECKSUM_SHA256_194="cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
593 +
SOURCE_FILENAME_194="rustversion-1.0.23.tar.gz"
594 +
SOURCE_URI_195="https://static.crates.io/crates/rusty-fork/rusty-fork-0.3.1.crate"
595 +
CHECKSUM_SHA256_195="cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
596 +
SOURCE_FILENAME_195="rusty-fork-0.3.1.tar.gz"
597 +
SOURCE_URI_196="https://static.crates.io/crates/ryu/ryu-1.0.23.crate"
598 +
CHECKSUM_SHA256_196="9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
599 +
SOURCE_FILENAME_196="ryu-1.0.23.tar.gz"
600 +
SOURCE_URI_197="https://static.crates.io/crates/same-file/same-file-1.0.6.crate"
601 +
CHECKSUM_SHA256_197="93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
602 +
SOURCE_FILENAME_197="same-file-1.0.6.tar.gz"
603 +
SOURCE_URI_198="https://static.crates.io/crates/scopeguard/scopeguard-1.2.0.crate"
604 +
CHECKSUM_SHA256_198="94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
605 +
SOURCE_FILENAME_198="scopeguard-1.2.0.tar.gz"
606 +
SOURCE_URI_199="https://static.crates.io/crates/semver/semver-1.0.28.crate"
607 +
CHECKSUM_SHA256_199="8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
608 +
SOURCE_FILENAME_199="semver-1.0.28.tar.gz"
609 +
SOURCE_URI_200="https://static.crates.io/crates/serde/serde-1.0.229.crate"
610 +
CHECKSUM_SHA256_200="4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
611 +
SOURCE_FILENAME_200="serde-1.0.229.tar.gz"
612 +
SOURCE_URI_201="https://static.crates.io/crates/serde_core/serde_core-1.0.229.crate"
613 +
CHECKSUM_SHA256_201="67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
614 +
SOURCE_FILENAME_201="serde_core-1.0.229.tar.gz"
615 +
SOURCE_URI_202="https://static.crates.io/crates/serde_derive/serde_derive-1.0.229.crate"
616 +
CHECKSUM_SHA256_202="e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
617 +
SOURCE_FILENAME_202="serde_derive-1.0.229.tar.gz"
618 +
SOURCE_URI_203="https://static.crates.io/crates/serde_json/serde_json-1.0.151.crate"
619 +
CHECKSUM_SHA256_203="c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
620 +
SOURCE_FILENAME_203="serde_json-1.0.151.tar.gz"
621 +
SOURCE_URI_204="https://static.crates.io/crates/serde_path_to_error/serde_path_to_error-0.1.20.crate"
622 +
CHECKSUM_SHA256_204="10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
623 +
SOURCE_FILENAME_204="serde_path_to_error-0.1.20.tar.gz"
624 +
SOURCE_URI_205="https://static.crates.io/crates/serde_urlencoded/serde_urlencoded-0.7.1.crate"
625 +
CHECKSUM_SHA256_205="d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
626 +
SOURCE_FILENAME_205="serde_urlencoded-0.7.1.tar.gz"
627 +
SOURCE_URI_206="https://static.crates.io/crates/sha2/sha2-0.11.0.crate"
628 +
CHECKSUM_SHA256_206="446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
629 +
SOURCE_FILENAME_206="sha2-0.11.0.tar.gz"
630 +
SOURCE_URI_207="https://static.crates.io/crates/shake/shake-0.1.0.crate"
631 +
CHECKSUM_SHA256_207="09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a"
632 +
SOURCE_FILENAME_207="shake-0.1.0.tar.gz"
633 +
SOURCE_URI_208="https://static.crates.io/crates/shlex/shlex-2.0.1.crate"
634 +
CHECKSUM_SHA256_208="f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
635 +
SOURCE_FILENAME_208="shlex-2.0.1.tar.gz"
636 +
SOURCE_URI_209="https://static.crates.io/crates/signature/signature-3.0.0.crate"
637 +
CHECKSUM_SHA256_209="28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
638 +
SOURCE_FILENAME_209="signature-3.0.0.tar.gz"
639 +
SOURCE_URI_210="https://static.crates.io/crates/simd-adler32/simd-adler32-0.3.10.crate"
640 +
CHECKSUM_SHA256_210="3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
641 +
SOURCE_FILENAME_210="simd-adler32-0.3.10.tar.gz"
642 +
SOURCE_URI_211="https://static.crates.io/crates/similar/similar-2.7.0.crate"
643 +
CHECKSUM_SHA256_211="bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
644 +
SOURCE_FILENAME_211="similar-2.7.0.tar.gz"
645 +
SOURCE_URI_212="https://static.crates.io/crates/slab/slab-0.4.12.crate"
646 +
CHECKSUM_SHA256_212="0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
647 +
SOURCE_FILENAME_212="slab-0.4.12.tar.gz"
648 +
SOURCE_URI_213="https://static.crates.io/crates/smallvec/smallvec-1.16.2.crate"
649 +
CHECKSUM_SHA256_213="f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
650 +
SOURCE_FILENAME_213="smallvec-1.16.2.tar.gz"
651 +
SOURCE_URI_214="https://static.crates.io/crates/socket2/socket2-0.6.5.crate"
652 +
CHECKSUM_SHA256_214="c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
653 +
SOURCE_FILENAME_214="socket2-0.6.5.tar.gz"
654 +
SOURCE_URI_215="https://static.crates.io/crates/spin/spin-0.10.1.crate"
655 +
CHECKSUM_SHA256_215="023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
656 +
SOURCE_FILENAME_215="spin-0.10.1.tar.gz"
657 +
SOURCE_URI_216="https://static.crates.io/crates/spki/spki-0.8.0.crate"
658 +
CHECKSUM_SHA256_216="1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
659 +
SOURCE_FILENAME_216="spki-0.8.0.tar.gz"
660 +
SOURCE_URI_217="https://static.crates.io/crates/sponge-cursor/sponge-cursor-0.1.0.crate"
661 +
CHECKSUM_SHA256_217="3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
662 +
SOURCE_FILENAME_217="sponge-cursor-0.1.0.tar.gz"
663 +
SOURCE_URI_218="https://static.crates.io/crates/stable_deref_trait/stable_deref_trait-1.2.1.crate"
664 +
CHECKSUM_SHA256_218="6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
665 +
SOURCE_FILENAME_218="stable_deref_trait-1.2.1.tar.gz"
666 +
SOURCE_URI_219="https://static.crates.io/crates/strsim/strsim-0.11.1.crate"
667 +
CHECKSUM_SHA256_219="7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
668 +
SOURCE_FILENAME_219="strsim-0.11.1.tar.gz"
669 +
SOURCE_URI_220="https://static.crates.io/crates/subtle/subtle-2.6.1.crate"
670 +
CHECKSUM_SHA256_220="13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
671 +
SOURCE_FILENAME_220="subtle-2.6.1.tar.gz"
672 +
SOURCE_URI_221="https://static.crates.io/crates/syn/syn-2.0.119.crate"
673 +
CHECKSUM_SHA256_221="872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
674 +
SOURCE_FILENAME_221="syn-2.0.119.tar.gz"
675 +
SOURCE_URI_222="https://static.crates.io/crates/syn/syn-3.0.6.crate"
676 +
CHECKSUM_SHA256_222="8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
677 +
SOURCE_FILENAME_222="syn-3.0.6.tar.gz"
678 +
SOURCE_URI_223="https://static.crates.io/crates/sync_wrapper/sync_wrapper-1.0.2.crate"
679 +
CHECKSUM_SHA256_223="0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
680 +
SOURCE_FILENAME_223="sync_wrapper-1.0.2.tar.gz"
681 +
SOURCE_URI_224="https://static.crates.io/crates/synstructure/synstructure-0.13.2.crate"
682 +
CHECKSUM_SHA256_224="728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
683 +
SOURCE_FILENAME_224="synstructure-0.13.2.tar.gz"
684 +
SOURCE_URI_225="https://static.crates.io/crates/synstructure/synstructure-0.14.0.crate"
685 +
CHECKSUM_SHA256_225="901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
686 +
SOURCE_FILENAME_225="synstructure-0.14.0.tar.gz"
687 +
SOURCE_URI_226="https://static.crates.io/crates/tempfile/tempfile-3.27.0.crate"
688 +
CHECKSUM_SHA256_226="32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
689 +
SOURCE_FILENAME_226="tempfile-3.27.0.tar.gz"
690 +
SOURCE_URI_227="https://static.crates.io/crates/thiserror/thiserror-1.0.69.crate"
691 +
CHECKSUM_SHA256_227="b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
692 +
SOURCE_FILENAME_227="thiserror-1.0.69.tar.gz"
693 +
SOURCE_URI_228="https://static.crates.io/crates/thiserror/thiserror-2.0.21.crate"
694 +
CHECKSUM_SHA256_228="09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
695 +
SOURCE_FILENAME_228="thiserror-2.0.21.tar.gz"
696 +
SOURCE_URI_229="https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.69.crate"
697 +
CHECKSUM_SHA256_229="4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
698 +
SOURCE_FILENAME_229="thiserror-impl-1.0.69.tar.gz"
699 +
SOURCE_URI_230="https://static.crates.io/crates/thiserror-impl/thiserror-impl-2.0.21.crate"
700 +
CHECKSUM_SHA256_230="fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
701 +
SOURCE_FILENAME_230="thiserror-impl-2.0.21.tar.gz"
702 +
SOURCE_URI_231="https://static.crates.io/crates/time/time-0.3.55.crate"
703 +
CHECKSUM_SHA256_231="cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
704 +
SOURCE_FILENAME_231="time-0.3.55.tar.gz"
705 +
SOURCE_URI_232="https://static.crates.io/crates/time-core/time-core-0.1.9.crate"
706 +
CHECKSUM_SHA256_232="9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
707 +
SOURCE_FILENAME_232="time-core-0.1.9.tar.gz"
708 +
SOURCE_URI_233="https://static.crates.io/crates/time-macros/time-macros-0.2.32.crate"
709 +
CHECKSUM_SHA256_233="7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
710 +
SOURCE_FILENAME_233="time-macros-0.2.32.tar.gz"
711 +
SOURCE_URI_234="https://static.crates.io/crates/tinystr/tinystr-0.8.4.crate"
712 +
CHECKSUM_SHA256_234="b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
713 +
SOURCE_FILENAME_234="tinystr-0.8.4.tar.gz"
714 +
SOURCE_URI_235="https://static.crates.io/crates/tokio/tokio-1.53.1.crate"
715 +
CHECKSUM_SHA256_235="202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
716 +
SOURCE_FILENAME_235="tokio-1.53.1.tar.gz"
717 +
SOURCE_URI_236="https://static.crates.io/crates/tokio-macros/tokio-macros-2.7.2.crate"
718 +
CHECKSUM_SHA256_236="78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
719 +
SOURCE_FILENAME_236="tokio-macros-2.7.2.tar.gz"
720 +
SOURCE_URI_237="https://static.crates.io/crates/tokio-rustls/tokio-rustls-0.26.5.crate"
721 +
CHECKSUM_SHA256_237="b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
722 +
SOURCE_FILENAME_237="tokio-rustls-0.26.5.tar.gz"
723 +
SOURCE_URI_238="https://static.crates.io/crates/tokio-util/tokio-util-0.7.19.crate"
724 +
CHECKSUM_SHA256_238="494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
725 +
SOURCE_FILENAME_238="tokio-util-0.7.19.tar.gz"
726 +
SOURCE_URI_239="https://static.crates.io/crates/tower/tower-0.5.3.crate"
727 +
CHECKSUM_SHA256_239="ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
728 +
SOURCE_FILENAME_239="tower-0.5.3.tar.gz"
729 +
SOURCE_URI_240="https://static.crates.io/crates/tower-layer/tower-layer-0.3.3.crate"
730 +
CHECKSUM_SHA256_240="121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
731 +
SOURCE_FILENAME_240="tower-layer-0.3.3.tar.gz"
732 +
SOURCE_URI_241="https://static.crates.io/crates/tower-service/tower-service-0.3.3.crate"
733 +
CHECKSUM_SHA256_241="8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
734 +
SOURCE_FILENAME_241="tower-service-0.3.3.tar.gz"
735 +
SOURCE_URI_242="https://static.crates.io/crates/tracing/tracing-0.1.44.crate"
736 +
CHECKSUM_SHA256_242="63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
737 +
SOURCE_FILENAME_242="tracing-0.1.44.tar.gz"
738 +
SOURCE_URI_243="https://static.crates.io/crates/tracing-attributes/tracing-attributes-0.1.31.crate"
739 +
CHECKSUM_SHA256_243="7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
740 +
SOURCE_FILENAME_243="tracing-attributes-0.1.31.tar.gz"
741 +
SOURCE_URI_244="https://static.crates.io/crates/tracing-core/tracing-core-0.1.36.crate"
742 +
CHECKSUM_SHA256_244="db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
743 +
SOURCE_FILENAME_244="tracing-core-0.1.36.tar.gz"
744 +
SOURCE_URI_245="https://static.crates.io/crates/typenum/typenum-1.20.1.crate"
745 +
CHECKSUM_SHA256_245="b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
746 +
SOURCE_FILENAME_245="typenum-1.20.1.tar.gz"
747 +
SOURCE_URI_246="https://static.crates.io/crates/unarray/unarray-0.1.4.crate"
748 +
CHECKSUM_SHA256_246="eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
749 +
SOURCE_FILENAME_246="unarray-0.1.4.tar.gz"
750 +
SOURCE_URI_247="https://static.crates.io/crates/unicode-ident/unicode-ident-1.0.26.crate"
751 +
CHECKSUM_SHA256_247="d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
752 +
SOURCE_FILENAME_247="unicode-ident-1.0.26.tar.gz"
753 +
SOURCE_URI_248="https://static.crates.io/crates/unicode-width/unicode-width-0.2.2.crate"
754 +
CHECKSUM_SHA256_248="b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
755 +
SOURCE_FILENAME_248="unicode-width-0.2.2.tar.gz"
756 +
SOURCE_URI_249="https://static.crates.io/crates/untrusted/untrusted-0.9.0.crate"
757 +
CHECKSUM_SHA256_249="8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
758 +
SOURCE_FILENAME_249="untrusted-0.9.0.tar.gz"
759 +
SOURCE_URI_250="https://static.crates.io/crates/ureq/ureq-2.12.1.crate"
760 +
CHECKSUM_SHA256_250="02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
761 +
SOURCE_FILENAME_250="ureq-2.12.1.tar.gz"
762 +
SOURCE_URI_251="https://static.crates.io/crates/url/url-2.5.8.crate"
763 +
CHECKSUM_SHA256_251="ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
764 +
SOURCE_FILENAME_251="url-2.5.8.tar.gz"
765 +
SOURCE_URI_252="https://static.crates.io/crates/utf8_iter/utf8_iter-1.0.4.crate"
766 +
CHECKSUM_SHA256_252="b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
767 +
SOURCE_FILENAME_252="utf8_iter-1.0.4.tar.gz"
768 +
SOURCE_URI_253="https://static.crates.io/crates/utf8parse/utf8parse-0.2.2.crate"
769 +
CHECKSUM_SHA256_253="06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
770 +
SOURCE_FILENAME_253="utf8parse-0.2.2.tar.gz"
771 +
SOURCE_URI_254="https://static.crates.io/crates/version_check/version_check-0.9.5.crate"
772 +
CHECKSUM_SHA256_254="0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
773 +
SOURCE_FILENAME_254="version_check-0.9.5.tar.gz"
774 +
SOURCE_URI_255="https://static.crates.io/crates/wait-timeout/wait-timeout-0.2.1.crate"
775 +
CHECKSUM_SHA256_255="09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
776 +
SOURCE_FILENAME_255="wait-timeout-0.2.1.tar.gz"
777 +
SOURCE_URI_256="https://static.crates.io/crates/walkdir/walkdir-2.5.0.crate"
778 +
CHECKSUM_SHA256_256="29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
779 +
SOURCE_FILENAME_256="walkdir-2.5.0.tar.gz"
780 +
SOURCE_URI_257="https://static.crates.io/crates/wasi/wasi-0.11.1+wasi-snapshot-preview1.crate"
781 +
CHECKSUM_SHA256_257="ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
782 +
SOURCE_FILENAME_257="wasi-0.11.1+wasi-snapshot-preview1.tar.gz"
783 +
SOURCE_URI_258="https://static.crates.io/crates/wasip2/wasip2-1.0.4+wasi-0.2.12.crate"
784 +
CHECKSUM_SHA256_258="b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
785 +
SOURCE_FILENAME_258="wasip2-1.0.4+wasi-0.2.12.tar.gz"
786 +
SOURCE_URI_259="https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.129.crate"
787 +
CHECKSUM_SHA256_259="9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
788 +
SOURCE_FILENAME_259="wasm-bindgen-0.2.129.tar.gz"
789 +
SOURCE_URI_260="https://static.crates.io/crates/wasm-bindgen-futures/wasm-bindgen-futures-0.4.79.crate"
790 +
CHECKSUM_SHA256_260="3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
791 +
SOURCE_FILENAME_260="wasm-bindgen-futures-0.4.79.tar.gz"
792 +
SOURCE_URI_261="https://static.crates.io/crates/wasm-bindgen-macro/wasm-bindgen-macro-0.2.129.crate"
793 +
CHECKSUM_SHA256_261="2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
794 +
SOURCE_FILENAME_261="wasm-bindgen-macro-0.2.129.tar.gz"
795 +
SOURCE_URI_262="https://static.crates.io/crates/wasm-bindgen-macro-support/wasm-bindgen-macro-support-0.2.129.crate"
796 +
CHECKSUM_SHA256_262="6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
797 +
SOURCE_FILENAME_262="wasm-bindgen-macro-support-0.2.129.tar.gz"
798 +
SOURCE_URI_263="https://static.crates.io/crates/wasm-bindgen-shared/wasm-bindgen-shared-0.2.129.crate"
799 +
CHECKSUM_SHA256_263="23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
800 +
SOURCE_FILENAME_263="wasm-bindgen-shared-0.2.129.tar.gz"
801 +
SOURCE_URI_264="https://static.crates.io/crates/web-time/web-time-1.1.0.crate"
802 +
CHECKSUM_SHA256_264="5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
803 +
SOURCE_FILENAME_264="web-time-1.1.0.tar.gz"
804 +
SOURCE_URI_265="https://static.crates.io/crates/webpki-roots/webpki-roots-0.26.11.crate"
805 +
CHECKSUM_SHA256_265="521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
806 +
SOURCE_FILENAME_265="webpki-roots-0.26.11.tar.gz"
807 +
SOURCE_URI_266="https://static.crates.io/crates/webpki-roots/webpki-roots-1.0.9.crate"
808 +
CHECKSUM_SHA256_266="7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
809 +
SOURCE_FILENAME_266="webpki-roots-1.0.9.tar.gz"
810 +
SOURCE_URI_267="https://static.crates.io/crates/winapi-util/winapi-util-0.1.11.crate"
811 +
CHECKSUM_SHA256_267="c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
812 +
SOURCE_FILENAME_267="winapi-util-0.1.11.tar.gz"
813 +
SOURCE_URI_268="https://static.crates.io/crates/windows-core/windows-core-0.62.2.crate"
814 +
CHECKSUM_SHA256_268="b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
815 +
SOURCE_FILENAME_268="windows-core-0.62.2.tar.gz"
816 +
SOURCE_URI_269="https://static.crates.io/crates/windows-implement/windows-implement-0.60.2.crate"
817 +
CHECKSUM_SHA256_269="053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
818 +
SOURCE_FILENAME_269="windows-implement-0.60.2.tar.gz"
819 +
SOURCE_URI_270="https://static.crates.io/crates/windows-interface/windows-interface-0.59.3.crate"
820 +
CHECKSUM_SHA256_270="3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
821 +
SOURCE_FILENAME_270="windows-interface-0.59.3.tar.gz"
822 +
SOURCE_URI_271="https://static.crates.io/crates/windows-link/windows-link-0.2.1.crate"
823 +
CHECKSUM_SHA256_271="f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
824 +
SOURCE_FILENAME_271="windows-link-0.2.1.tar.gz"
825 +
SOURCE_URI_272="https://static.crates.io/crates/windows-result/windows-result-0.4.1.crate"
826 +
CHECKSUM_SHA256_272="7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
827 +
SOURCE_FILENAME_272="windows-result-0.4.1.tar.gz"
828 +
SOURCE_URI_273="https://static.crates.io/crates/windows-strings/windows-strings-0.5.1.crate"
829 +
CHECKSUM_SHA256_273="7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
830 +
SOURCE_FILENAME_273="windows-strings-0.5.1.tar.gz"
831 +
SOURCE_URI_274="https://static.crates.io/crates/windows-sys/windows-sys-0.52.0.crate"
832 +
CHECKSUM_SHA256_274="282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
833 +
SOURCE_FILENAME_274="windows-sys-0.52.0.tar.gz"
834 +
SOURCE_URI_275="https://static.crates.io/crates/windows-sys/windows-sys-0.59.0.crate"
835 +
CHECKSUM_SHA256_275="1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
836 +
SOURCE_FILENAME_275="windows-sys-0.59.0.tar.gz"
837 +
SOURCE_URI_276="https://static.crates.io/crates/windows-sys/windows-sys-0.61.2.crate"
838 +
CHECKSUM_SHA256_276="ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
839 +
SOURCE_FILENAME_276="windows-sys-0.61.2.tar.gz"
840 +
SOURCE_URI_277="https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate"
841 +
CHECKSUM_SHA256_277="9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
842 +
SOURCE_FILENAME_277="windows-targets-0.52.6.tar.gz"
843 +
SOURCE_URI_278="https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.52.6.crate"
844 +
CHECKSUM_SHA256_278="32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
845 +
SOURCE_FILENAME_278="windows_aarch64_gnullvm-0.52.6.tar.gz"
846 +
SOURCE_URI_279="https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.52.6.crate"
847 +
CHECKSUM_SHA256_279="09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
848 +
SOURCE_FILENAME_279="windows_aarch64_msvc-0.52.6.tar.gz"
849 +
SOURCE_URI_280="https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.52.6.crate"
850 +
CHECKSUM_SHA256_280="8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
851 +
SOURCE_FILENAME_280="windows_i686_gnu-0.52.6.tar.gz"
852 +
SOURCE_URI_281="https://static.crates.io/crates/windows_i686_gnullvm/windows_i686_gnullvm-0.52.6.crate"
853 +
CHECKSUM_SHA256_281="0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
854 +
SOURCE_FILENAME_281="windows_i686_gnullvm-0.52.6.tar.gz"
855 +
SOURCE_URI_282="https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.52.6.crate"
856 +
CHECKSUM_SHA256_282="240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
857 +
SOURCE_FILENAME_282="windows_i686_msvc-0.52.6.tar.gz"
858 +
SOURCE_URI_283="https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.52.6.crate"
859 +
CHECKSUM_SHA256_283="147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
860 +
SOURCE_FILENAME_283="windows_x86_64_gnu-0.52.6.tar.gz"
861 +
SOURCE_URI_284="https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.52.6.crate"
862 +
CHECKSUM_SHA256_284="24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
863 +
SOURCE_FILENAME_284="windows_x86_64_gnullvm-0.52.6.tar.gz"
864 +
SOURCE_URI_285="https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate"
865 +
CHECKSUM_SHA256_285="589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
866 +
SOURCE_FILENAME_285="windows_x86_64_msvc-0.52.6.tar.gz"
867 +
SOURCE_URI_286="https://static.crates.io/crates/wit-bindgen/wit-bindgen-0.57.1.crate"
868 +
CHECKSUM_SHA256_286="1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
869 +
SOURCE_FILENAME_286="wit-bindgen-0.57.1.tar.gz"
870 +
SOURCE_URI_287="https://static.crates.io/crates/writeable/writeable-0.6.4.crate"
871 +
CHECKSUM_SHA256_287="3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
872 +
SOURCE_FILENAME_287="writeable-0.6.4.tar.gz"
873 +
SOURCE_URI_288="https://static.crates.io/crates/x509-parser/x509-parser-0.18.1.crate"
874 +
CHECKSUM_SHA256_288="d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
875 +
SOURCE_FILENAME_288="x509-parser-0.18.1.tar.gz"
876 +
SOURCE_URI_289="https://static.crates.io/crates/yasna/yasna-0.6.0.crate"
877 +
CHECKSUM_SHA256_289="b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
878 +
SOURCE_FILENAME_289="yasna-0.6.0.tar.gz"
879 +
SOURCE_URI_290="https://static.crates.io/crates/yoke/yoke-0.8.3.crate"
880 +
CHECKSUM_SHA256_290="709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
881 +
SOURCE_FILENAME_290="yoke-0.8.3.tar.gz"
882 +
SOURCE_URI_291="https://static.crates.io/crates/yoke-derive/yoke-derive-0.8.3.crate"
883 +
CHECKSUM_SHA256_291="33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
884 +
SOURCE_FILENAME_291="yoke-derive-0.8.3.tar.gz"
885 +
SOURCE_URI_292="https://static.crates.io/crates/zerocopy/zerocopy-0.8.59.crate"
886 +
CHECKSUM_SHA256_292="6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
887 +
SOURCE_FILENAME_292="zerocopy-0.8.59.tar.gz"
888 +
SOURCE_URI_293="https://static.crates.io/crates/zerocopy-derive/zerocopy-derive-0.8.59.crate"
889 +
CHECKSUM_SHA256_293="ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
890 +
SOURCE_FILENAME_293="zerocopy-derive-0.8.59.tar.gz"
891 +
SOURCE_URI_294="https://static.crates.io/crates/zerofrom/zerofrom-0.1.8.crate"
892 +
CHECKSUM_SHA256_294="0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
893 +
SOURCE_FILENAME_294="zerofrom-0.1.8.tar.gz"
894 +
SOURCE_URI_295="https://static.crates.io/crates/zerofrom-derive/zerofrom-derive-0.1.8.crate"
895 +
CHECKSUM_SHA256_295="f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
896 +
SOURCE_FILENAME_295="zerofrom-derive-0.1.8.tar.gz"
897 +
SOURCE_URI_296="https://static.crates.io/crates/zeroize/zeroize-1.9.0.crate"
898 +
CHECKSUM_SHA256_296="e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
899 +
SOURCE_FILENAME_296="zeroize-1.9.0.tar.gz"
900 +
SOURCE_URI_297="https://static.crates.io/crates/zerotrie/zerotrie-0.2.5.crate"
901 +
CHECKSUM_SHA256_297="4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
902 +
SOURCE_FILENAME_297="zerotrie-0.2.5.tar.gz"
903 +
SOURCE_URI_298="https://static.crates.io/crates/zerovec/zerovec-0.11.8.crate"
904 +
CHECKSUM_SHA256_298="bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
905 +
SOURCE_FILENAME_298="zerovec-0.11.8.tar.gz"
906 +
SOURCE_URI_299="https://static.crates.io/crates/zerovec-derive/zerovec-derive-0.11.6.crate"
907 +
CHECKSUM_SHA256_299="34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
908 +
SOURCE_FILENAME_299="zerovec-derive-0.11.6.tar.gz"
909 +
SOURCE_URI_300="https://static.crates.io/crates/zlib-rs/zlib-rs-0.6.8.crate"
910 +
CHECKSUM_SHA256_300="b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
911 +
SOURCE_FILENAME_300="zlib-rs-0.6.8.tar.gz"
912 +
SOURCE_URI_301="https://static.crates.io/crates/zmij/zmij-1.0.23.crate"
913 +
CHECKSUM_SHA256_301="29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
914 +
SOURCE_FILENAME_301="zmij-1.0.23.tar.gz"
915 +
SOURCE_URI_302="https://static.crates.io/crates/zstd/zstd-0.13.3.crate"
916 +
CHECKSUM_SHA256_302="e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
917 +
SOURCE_FILENAME_302="zstd-0.13.3.tar.gz"
918 +
SOURCE_URI_303="https://static.crates.io/crates/zstd-safe/zstd-safe-7.3.0.crate"
919 +
CHECKSUM_SHA256_303="64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882"
920 +
SOURCE_FILENAME_303="zstd-safe-7.3.0.tar.gz"
921 +
SOURCE_URI_304="https://static.crates.io/crates/zstd-sys/zstd-sys-2.1.0+zstd.1.5.7.crate"
922 +
CHECKSUM_SHA256_304="0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0"
923 +
SOURCE_FILENAME_304="zstd-sys-2.1.0+zstd.1.5.7.tar.gz"
924 +
925 +
ARCHITECTURES="!all x86_64"
926 +
927 +
PROVIDES="
928 +
	oxy$secondaryArchSuffix = $portVersion
929 +
	cmd:oxy
930 +
	"
931 +
REQUIRES="
932 +
	haiku$secondaryArchSuffix
933 +
	lib:libzstd$secondaryArchSuffix
934 +
	"
935 +
936 +
BUILD_REQUIRES="
937 +
	haiku${secondaryArchSuffix}_devel
938 +
	devel:libzstd$secondaryArchSuffix
939 +
	"
940 +
BUILD_PREREQUIRES="
941 +
	cmd:gcc$secondaryArchSuffix
942 +
	cmd:pkg_config$secondaryArchSuffix
943 +
	cmd:rustc$secondaryArchSuffix
944 +
	"
945 +
946 +
BUILD()
947 +
{
948 +
	export CARGO_HOME=$sourceDir/../cargo
949 +
	vendor=$CARGO_HOME/haiku
950 +
	mkdir -p "$vendor"
951 +
	for i in $(seq 2 304); do
952 +
		eval "srcDir=\$sourceDir$i"
953 +
		eval "sha256sum=\$CHECKSUM_SHA256_$i"
954 +
		set -- "$srcDir"/*
955 +
		ln -sf "$1" "$vendor"
956 +
		cat <<-EOF >"$vendor/${1##*/}/.cargo-checksum.json"
957 +
		{
958 +
		  "package": "$sha256sum",
959 +
		  "files": {}
960 +
		}
961 +
		EOF
962 +
	done
963 +
964 +
	cat <<-EOF >"$CARGO_HOME"/config.toml
965 +
	[source.haiku]
966 +
	directory = "$vendor"
967 +
968 +
	[source.crates-io]
969 +
	replace-with = "haiku"
970 +
	EOF
971 +
972 +
	# The bundled zstd's hidden symbols break the link on Haiku: use the system's.
973 +
	export ZSTD_SYS_USE_PKG_CONFIG=1
974 +
	export OXY_BUILD=v$portVersion
975 +
	cargo build --release --frozen -p oxy
976 +
}
977 +
978 +
INSTALL()
979 +
{
980 +
	install -v -m 755 -d "$prefix"/bin "$docDir"
981 +
	install -v -m 755 ./target/release/oxy "$prefix"/bin
982 +
	install -v -m 644 README.md LICENSE* "$docDir"
983 +
}
984 +
985 +
TEST()
986 +
{
987 +
	export CARGO_HOME=$sourceDir/../cargo
988 +
	export ZSTD_SYS_USE_PKG_CONFIG=1
989 +
	cargo test --release --frozen -p oxy-core
990 +
}
added packaging/haikuports/icons/oh-16.png +0 −0

No line changes.

added packaging/haikuports/icons/oh-32.png +0 −0

No line changes.

added packaging/haikuports/icons/oh-64.png +0 −0

No line changes.

added packaging/haikuports/icons/oh.hvif +0 −0

No line changes.

added packaging/haikuports/icons/oh.svg +17 −0
1 +
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2 +
  <defs>
3 +
    <linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11">
4 +
      <stop offset="0%" stop-color="#00F0FF"/>
5 +
      <stop offset="100%" stop-color="#0072FF"/>
6 +
    </linearGradient>
7 +
  </defs>
8 +
  <rect width="64" height="64" rx="14" fill="#070c18"/>
9 +
  <g transform="translate(32 32)">
10 +
    <path d="M -11 -6 L 11 -6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/>
11 +
    <path d="M -11 6 L 11 6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/>
12 +
    <circle cx="-11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/>
13 +
    <circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/>
14 +
    <circle cx="11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/>
15 +
    <circle cx="11" cy="0" r="3.5" fill="#FFFFFF"/>
16 +
  </g>
17 +
</svg>
added packaging/haikuports/icons/oxy-16.png +0 −0

No line changes.

added packaging/haikuports/icons/oxy-32.png +0 −0

No line changes.

added packaging/haikuports/icons/oxy-64.png +0 −0

No line changes.

added packaging/haikuports/icons/oxy.hvif +0 −0

No line changes.

added packaging/haikuports/icons/oxy.svg +17 −0
1 +
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2 +
  <defs>
3 +
    <linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11">
4 +
      <stop offset="0%" stop-color="#00F0FF"/>
5 +
      <stop offset="100%" stop-color="#0072FF"/>
6 +
    </linearGradient>
7 +
  </defs>
8 +
  <rect width="64" height="64" rx="14" fill="#C9DCF5"/><rect x="1.5" y="1.5" width="61" height="61" rx="12.5" fill="#F2F7FF"/>
9 +
  <g transform="translate(32 32)">
10 +
    <path d="M -11 -6 L 11 -6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/>
11 +
    <path d="M -11 6 L 11 6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/>
12 +
    <circle cx="-11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/>
13 +
    <circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/>
14 +
    <circle cx="11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/>
15 +
    <circle cx="11" cy="0" r="3.5" fill="#0072FF"/>
16 +
  </g>
17 +
</svg>
added packaging/haikuports/make-icons.py +163 −0
1 +
#!/usr/bin/env python3
2 +
"""HaikuDepot icons for oxy and oh (oxygen#157): the Oxygen mark as Haiku's
3 +
vector icon format (HVIF), and as SVG to render the 16, 32 and 64 px PNGs.
4 +
5 +
A family of two (Andy, 2026-10-29): Oxygen (oxy, the Oxygen VCS extension)
6 +
is the mark on a pale rounded tile with a blue right dot; OxygenHub (oh, the
7 +
OxygenHub extension) is the mark on the hub's dark tile with a white one, as
8 +
oxygenhub/assets/icons/oxygenhub.svg. Each keeps its edge on light and dark
9 +
backgrounds alike.
10 +
11 +
    make-icons.py OUTDIR    writes oxy.hvif, oh.hvif, oxy.svg, oh.svg
12 +
13 +
HVIF as Haiku's FlatIconImporter reads it: "ncif", then styles, paths and
14 +
shapes, each a count byte and its items; coordinates in a 64-unit canvas.
15 +
"""
16 +
import math
17 +
import struct
18 +
import sys
19 +
20 +
CYAN, BLUE, WHITE, TILE = (0x00, 0xF0, 0xFF), (0x00, 0x72, 0xFF), (0xFF, 0xFF, 0xFF), (0x07, 0x0C, 0x18)
21 +
LIGHT, EDGE = (0xF2, 0xF7, 0xFF), (0xC9, 0xDC, 0xF5)
22 +
K = 0.5522847498  # a circle's quarter as one cubic Bézier
23 +
24 +
25 +
def coord(v):
26 +
    if v == int(v) and -32 <= v <= 95:
27 +
        return bytes([int(v) + 32])
28 +
    n = int(round((v + 128.0) * 102.0))
29 +
    return bytes([(n >> 8) | 0x80, n & 0xFF])
30 +
31 +
32 +
def float24(v):
33 +
    if v == 0:
34 +
        return b"\0\0\0"
35 +
    bits = struct.unpack(">I", struct.pack(">f", v))[0]
36 +
    sign, exp, man = bits >> 31, ((bits >> 23) & 0xFF) - 127, bits & 0x7FFFFF
37 +
    if not -32 <= exp < 32:
38 +
        raise ValueError(f"{v} does not fit a float24")
39 +
    n = (sign << 23) | ((exp + 32) << 17) | (man >> 6)
40 +
    return n.to_bytes(3, "big")
41 +
42 +
43 +
def solid(rgb):
44 +
    return bytes([3, *rgb])  # STYLE_TYPE_SOLID_COLOR_NO_ALPHA
45 +
46 +
47 +
def linear(p1, p2, stops):
48 +
    """A linear gradient from p1 to p2: gradient space runs x from -64 to 64."""
49 +
    dx, dy = (p2[0] - p1[0]) / 128.0, (p2[1] - p1[1]) / 128.0
50 +
    mx, my = (p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2
51 +
    out = bytes([2, 0, (1 << 1) | (1 << 2), len(stops)])  # gradient, linear, transform | no alpha
52 +
    for v in (dx, dy, -dy, dx, mx, my):
53 +
        out += float24(v)
54 +
    for off, rgb in stops:
55 +
        out += bytes([off, *rgb])
56 +
    return out
57 +
58 +
59 +
def line(a, b):
60 +
    return bytes([1 << 3, 2]) + coord(a[0]) + coord(a[1]) + coord(b[0]) + coord(b[1])  # no curves, open
61 +
62 +
63 +
def curve_path(points, closed=True):
64 +
    """points: (x, y, in_x, in_y, out_x, out_y)."""
65 +
    out = bytes([(1 << 1) if closed else 0, len(points)])
66 +
    for p in points:
67 +
        for v in p:
68 +
            out += coord(v)
69 +
    return out
70 +
71 +
72 +
def circle(cx, cy, r):
73 +
    k = r * K
74 +
    pts = [(cx, cy - r, cx - k, cy - r, cx + k, cy - r), (cx + r, cy, cx + r, cy - k, cx + r, cy + k),
75 +
           (cx, cy + r, cx + k, cy + r, cx - k, cy + r), (cx - r, cy, cx - r, cy + k, cx - r, cy - k)]
76 +
    return curve_path(pts)
77 +
78 +
79 +
def rounded(x, y, w, h, r):
80 +
    k = r * (1 - K)
81 +
    x2, y2 = x + w, y + h
82 +
    pts = [
83 +
        (x + r, y, x + k, y, x + r, y), (x2 - r, y, x2 - r, y, x2 - k, y),
84 +
        (x2, y + r, x2, y + k, x2, y + r), (x2, y2 - r, x2, y2 - r, x2, y2 - k),
85 +
        (x2 - r, y2, x2 - k, y2, x2 - r, y2), (x + r, y2, x + r, y2, x + k, y2),
86 +
        (x, y2 - r, x, y2 - k, x, y2 - r), (x, y + r, x, y + r, x, y + k),
87 +
    ]
88 +
    return curve_path(pts)
89 +
90 +
91 +
def shape(style, paths, stroke=None, cap=0):
92 +
    out = bytes([10, style, len(paths), *paths])
93 +
    if stroke is None:
94 +
        return out + bytes([0])
95 +
    # has transformers: one stroke (width + 128, cap << 4 | round join, miter limit)
96 +
    return out + bytes([1 << 4, 1, 23, int(round(stroke)) + 128, (cap << 4) | 2, 4])
97 +
98 +
99 +
def mark(scale, ox, oy):
100 +
    """The mark's geometry, scaled about its centre and moved to (ox, oy)."""
101 +
    p = lambda x, y: (ox + x * scale, oy + y * scale)
102 +
    return {
103 +
        "top": (p(-11, -6), p(11, -6)), "bottom": (p(-11, 6), p(11, 6)),
104 +
        "left": (p(-11, 0), 10 * scale), "right": (p(11, 0), 10 * scale),
105 +
        "dot": 3.5 * scale, "stroke": 3 * scale, "grad": (p(-22, -11), p(22, 11)),
106 +
    }
107 +
108 +
109 +
def hvif(dark):
110 +
    m = mark(1.0, 32, 32)
111 +
    styles = [linear(*m["grad"], [(0, CYAN), (255, BLUE)]), solid(CYAN), solid(WHITE if dark else BLUE)]
112 +
    paths = [line(*m["top"]), line(*m["bottom"]), circle(*m["left"][0], m["left"][1]),
113 +
             circle(*m["right"][0], m["right"][1]), circle(*m["left"][0], m["dot"]), circle(*m["right"][0], m["dot"])]
114 +
    if dark:
115 +
        styles.append(solid(TILE))
116 +
        paths.append(rounded(0, 0, 64, 64, 14))
117 +
        tile = [shape(3, [6])]
118 +
    else:
119 +
        # A pale tile with a thin edge: the edge's colour, then the tile inset by it.
120 +
        styles += [solid(EDGE), solid(LIGHT)]
121 +
        paths += [rounded(0, 0, 64, 64, 14), rounded(1.5, 1.5, 61, 61, 12.5)]
122 +
        tile = [shape(3, [6]), shape(4, [7])]
123 +
    shapes = tile + [
124 +
        shape(0, [0], m["stroke"], cap=2), shape(0, [1], m["stroke"], cap=2),
125 +
        shape(0, [2], m["stroke"]), shape(0, [3], m["stroke"]),
126 +
        shape(1, [4]), shape(2, [5]),
127 +
    ]
128 +
    out = b"ncif"
129 +
    for section in (styles, paths, shapes):
130 +
        out += bytes([len(section)]) + b"".join(section)
131 +
    return out
132 +
133 +
134 +
def svg(dark):
135 +
    if dark:
136 +
        bg, dot = '<rect width="64" height="64" rx="14" fill="#070c18"/>', "#FFFFFF"
137 +
    else:
138 +
        bg = '<rect width="64" height="64" rx="14" fill="#C9DCF5"/><rect x="1.5" y="1.5" width="61" height="61" rx="12.5" fill="#F2F7FF"/>'
139 +
        dot = "#0072FF"
140 +
    return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
141 +
  <defs>
142 +
    <linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11">
143 +
      <stop offset="0%" stop-color="#00F0FF"/>
144 +
      <stop offset="100%" stop-color="#0072FF"/>
145 +
    </linearGradient>
146 +
  </defs>
147 +
  {bg}
148 +
  <g transform="translate(32 32)">
149 +
    <path d="M -11 -6 L 11 -6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/>
150 +
    <path d="M -11 6 L 11 6" stroke="url(#g)" stroke-width="3" stroke-linecap="round"/>
151 +
    <circle cx="-11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/>
152 +
    <circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/>
153 +
    <circle cx="11" cy="0" r="10" fill="none" stroke="url(#g)" stroke-width="3"/>
154 +
    <circle cx="11" cy="0" r="3.5" fill="{dot}"/>
155 +
  </g>
156 +
</svg>
157 +
'''
158 +
159 +
160 +
out = sys.argv[1]
161 +
for name, dark in (("oxy", False), ("oh", True)):
162 +
    open(f"{out}/{name}.hvif", "wb").write(hvif(dark))
163 +
    open(f"{out}/{name}.svg", "w").write(svg(dark))
added packaging/haikuports/make-recipe.py +140 −0
1 +
#!/usr/bin/env python3
2 +
"""Write a haikuports recipe for oxy or oh (oxygen#156, #157) from a release's
3 +
source tarball, NAME-VERSION-src.tar.gz, as the release job attaches it: the
4 +
tarball itself from the release's stable download address, and every
5 +
crates.io crate in its Cargo.lock as a source of its own with its checksum,
6 +
vendored offline in BUILD() as haikuports' Rust recipes (ripgrep) do.
7 +
8 +
    make-recipe.py oxy|oh SRC_TARBALL VERSION > NAME-VERSION.recipe
9 +
"""
10 +
import hashlib
11 +
import sys
12 +
import tarfile
13 +
import tomllib
14 +
15 +
PORTS = {
16 +
    "oxy": {
17 +
        "repo": "oxygen",
18 +
        "package": "oxy",
19 +
        "summary": "A version control system of signed, commuting changes, with Git interop",
20 +
        "description": [
21 +
            "oxy is the command line of Oxygen, a version control system built on",
22 +
            "patch theory: changes commute, conflicts are recorded rather than blocking,",
23 +
            "every change can be signed, and a trust policy says who may move what. It",
24 +
            "imports and exports Git history, clones from OxygenHub and other Oxygen",
25 +
            "hosts, and keeps repositories on disk or in S3, GCS or Azure buckets.",
26 +
        ],
27 +
        "docs": "README.md LICENSE*",
28 +
        "test": "oxy-core",
29 +
    },
30 +
    "oh": {
31 +
        "repo": "oxygenhub",
32 +
        "package": "oh",
33 +
        "summary": "The command line of OxygenHub, the forge for Oxygen repositories",
34 +
        "description": [
35 +
            "oh talks to an OxygenHub (such as https://oxygenhub.io): issues, proposals",
36 +
            "and their reviews, releases, Flow runs, their logs and reports, and checks,",
37 +
            "from the terminal. It works alongside oxy, the Oxygen version control",
38 +
            "client.",
39 +
        ],
40 +
        "docs": "README.md LICENSE docs/oh.md",
41 +
        "test": "hub-api",
42 +
    },
43 +
}
44 +
45 +
name, tarball, version = sys.argv[1], sys.argv[2], sys.argv[3]
46 +
port = PORTS[name]
47 +
prefix = f"{port['repo']}-{version}"
48 +
with tarfile.open(tarball) as t:
49 +
    lock = tomllib.load(t.extractfile(f"{prefix}/Cargo.lock"))
50 +
crates = sorted(
51 +
    (p["name"], p["version"], p["checksum"])
52 +
    for p in lock["package"]
53 +
    if p.get("source", "").startswith("registry+")
54 +
)
55 +
sha = hashlib.sha256(open(tarball, "rb").read()).hexdigest()
56 +
57 +
out = []
58 +
w = out.append
59 +
w(f'SUMMARY="{port["summary"]}"')
60 +
w('DESCRIPTION="' + " \\\n".join(port["description"]) + '"')
61 +
w(f'HOMEPAGE="https://oxygenhub.io/andymccall/{port["repo"]}"')
62 +
w('COPYRIGHT="2026 Andy McCall"')
63 +
w('LICENSE="MIT\n\tApache v2"')
64 +
w('REVISION="1"')
65 +
w(f'SOURCE_URI="$HOMEPAGE/releases/download/v$portVersion/{name}-$portVersion-src.tar.gz"')
66 +
w(f'CHECKSUM_SHA256="{sha}"')
67 +
w(f'SOURCE_DIR="{port["repo"]}-$portVersion"')
68 +
for i, (crate, ver, ck) in enumerate(crates, start=2):
69 +
    w(f'SOURCE_URI_{i}="https://static.crates.io/crates/{crate}/{crate}-{ver}.crate"')
70 +
    w(f'CHECKSUM_SHA256_{i}="{ck}"')
71 +
    w(f'SOURCE_FILENAME_{i}="{crate}-{ver}.tar.gz"')
72 +
last = len(crates) + 1
73 +
w(f'''
74 +
ARCHITECTURES="!all x86_64"
75 +
76 +
PROVIDES="
77 +
	{name}$secondaryArchSuffix = $portVersion
78 +
	cmd:{name}
79 +
	"
80 +
REQUIRES="
81 +
	haiku$secondaryArchSuffix
82 +
	lib:libzstd$secondaryArchSuffix
83 +
	"
84 +
85 +
BUILD_REQUIRES="
86 +
	haiku${{secondaryArchSuffix}}_devel
87 +
	devel:libzstd$secondaryArchSuffix
88 +
	"
89 +
BUILD_PREREQUIRES="
90 +
	cmd:gcc$secondaryArchSuffix
91 +
	cmd:pkg_config$secondaryArchSuffix
92 +
	cmd:rustc$secondaryArchSuffix
93 +
	"
94 +
95 +
BUILD()
96 +
{{
97 +
	export CARGO_HOME=$sourceDir/../cargo
98 +
	vendor=$CARGO_HOME/haiku
99 +
	mkdir -p "$vendor"
100 +
	for i in $(seq 2 {last}); do
101 +
		eval "srcDir=\\$sourceDir$i"
102 +
		eval "sha256sum=\\$CHECKSUM_SHA256_$i"
103 +
		set -- "$srcDir"/*
104 +
		ln -sf "$1" "$vendor"
105 +
		cat <<-EOF >"$vendor/${{1##*/}}/.cargo-checksum.json"
106 +
		{{
107 +
		  "package": "$sha256sum",
108 +
		  "files": {{}}
109 +
		}}
110 +
		EOF
111 +
	done
112 +
113 +
	cat <<-EOF >"$CARGO_HOME"/config.toml
114 +
	[source.haiku]
115 +
	directory = "$vendor"
116 +
117 +
	[source.crates-io]
118 +
	replace-with = "haiku"
119 +
	EOF
120 +
121 +
	# The bundled zstd's hidden symbols break the link on Haiku: use the system's.
122 +
	export ZSTD_SYS_USE_PKG_CONFIG=1
123 +
	export OXY_BUILD=v$portVersion
124 +
	cargo build --release --frozen -p {port["package"]}
125 +
}}
126 +
127 +
INSTALL()
128 +
{{
129 +
	install -v -m 755 -d "$prefix"/bin "$docDir"
130 +
	install -v -m 755 ./target/release/{name} "$prefix"/bin
131 +
	install -v -m 644 {port["docs"]} "$docDir"
132 +
}}
133 +
134 +
TEST()
135 +
{{
136 +
	export CARGO_HOME=$sourceDir/../cargo
137 +
	export ZSTD_SYS_USE_PKG_CONFIG=1
138 +
	cargo test --release --frozen -p {port["test"]}
139 +
}}''')
140 +
print("\n".join(out))
modified scripts/dist.sh +11 −0
…
136 unchanged lines
137 137
  fi
138 138
done
139 139
140 +
# The source as released (oxygen#157): the oxygen tree,
141 +
# under oxygen-VERSION/, for packagers (haikuports) to fetch from the
142 +
# release at a fixed checksum. Reproducible: sorted, fixed times and
143 +
# owners, gzip without a timestamp.
144 +
if [ -z "${ONLY:-}" ] || [[ src == $ONLY ]]; then
145 +
  echo "== src"
146 +
  tar --sort=name --mtime='1970-01-01 00:00:00Z' --owner=0 --group=0 --numeric-owner \
147 +
    --mode='u+rwX,go+rX,go-w' --exclude=./target --exclude=./lost+found --exclude=.oxy \
148 +
    --transform "s|^\.|oxygen-$VERSION|" -cf - . | gzip -9n >"$OUT/$BIN-$VERSION-src.tar.gz"
149 +
fi
150 +
140 151
rm -rf "$OUT/stage"
141 152
(cd "$OUT" && shopt -s nullglob && sha256sum -- *.tar.gz *.zip *.deb >SHA256SUMS)
142 153
# DIST_FLAT=DIR: the same files again under names without the version, for
…
12 unchanged lines
added src/cli/artifact.rs +180 −0
1 +
//! `oxy artifact`: Cylinder's artifacts (docs/cylinder.md §4.1, step C1):
2 +
//! a directory packed into a tree of the repository's own chunks, named by
3 +
//! the BLAKE3 of its canonical encoding.
4 +
5 +
use super::json;
6 +
use super::open;
7 +
use super::ui;
8 +
use anyhow::Result;
9 +
use cylinder::{Kind, Spec};
10 +
use std::path::Path;
11 +
12 +
/// `KEY=VALUE` pairs, refused when one has no `=`.
13 +
fn pairs(list: &[String], what: &str) -> Result<std::collections::BTreeMap<String, String>> {
14 +
    list.iter()
15 +
        .map(|p| {
16 +
            p.split_once('=')
17 +
                .map(|(k, v)| (k.to_string(), v.to_string()))
18 +
                .ok_or_else(|| anyhow::anyhow!("{what} {p:?}: write it KEY=VALUE"))
19 +
        })
20 +
        .collect()
21 +
}
22 +
23 +
fn summary(k: &cylinder::Kept) -> String {
24 +
    let a = &k.artifact;
25 +
    let mut parts = vec![a.kind.as_str().to_string()];
26 +
    parts.extend(a.os.clone());
27 +
    parts.extend(a.arch.clone());
28 +
    format!(
29 +
        "{} {} {}",
30 +
        ui::hash(&k.name[..12]),
31 +
        parts.join(" "),
32 +
        ui::dim(format!("{} bytes", a.total_size))
33 +
    )
34 +
}
35 +
36 +
fn as_json(k: &cylinder::Kept, files: usize) -> String {
37 +
    let a = &k.artifact;
38 +
    let text = |v: &Option<String>| v.as_deref().map(json::s).unwrap_or_else(|| "null".into());
39 +
    let map = |m: &std::collections::BTreeMap<String, String>| {
40 +
        let inner: Vec<String> = m
41 +
            .iter()
42 +
            .map(|(k, v)| format!("{}:{}", json::s(k), json::s(v)))
43 +
            .collect();
44 +
        format!("{{{}}}", inner.join(","))
45 +
    };
46 +
    json::obj([
47 +
        ("name", json::s(&k.name)),
48 +
        ("kind", json::s(a.kind.as_str())),
49 +
        ("arch", text(&a.arch)),
50 +
        ("os", text(&a.os)),
51 +
        ("tree", json::s(&a.tree.to_hex())),
52 +
        ("files", json::n(files as u64)),
53 +
        ("total_size", json::n(a.total_size)),
54 +
        ("env", map(&a.env)),
55 +
        ("labels", map(&a.labels)),
56 +
    ])
57 +
}
58 +
59 +
#[allow(clippy::too_many_arguments)]
60 +
pub fn pack(
61 +
    dir: &Path,
62 +
    kind: &str,
63 +
    arch: Option<String>,
64 +
    os: Option<String>,
65 +
    entrypoint: Vec<String>,
66 +
    env: &[String],
67 +
    labels: &[String],
68 +
    as_json_out: bool,
69 +
) -> Result<i32> {
70 +
    let repo = open()?;
71 +
    let mut spec = Spec::new(Kind::parse(kind)?);
72 +
    spec.arch = arch;
73 +
    spec.os = os;
74 +
    spec.entrypoint = (!entrypoint.is_empty()).then_some(entrypoint);
75 +
    spec.env = pairs(env, "env")?;
76 +
    spec.labels = pairs(labels, "label")?;
77 +
    let sp = ui::spinner("Packing…");
78 +
    let k = cylinder::pack(&repo, dir, &spec)?;
79 +
    sp.clear();
80 +
    let files = cylinder::tree(&repo, &k.artifact)?.entries.len();
81 +
    if as_json_out {
82 +
        println!("{}", as_json(&k, files));
83 +
    } else {
84 +
        println!(
85 +
            "{} {} as {}",
86 +
            ui::done("Packed"),
87 +
            ui::path(dir.display()),
88 +
            summary(&k)
89 +
        );
90 +
        println!("{}", ui::kv("name", &k.name));
91 +
        println!("{}", ui::kv("files", files.to_string()));
92 +
    }
93 +
    Ok(0)
94 +
}
95 +
96 +
pub fn list(as_json_out: bool) -> Result<i32> {
97 +
    let repo = open()?;
98 +
    let names = cylinder::list(&repo)?;
99 +
    let kept: Vec<cylinder::Kept> = names
100 +
        .iter()
101 +
        .map(|n| cylinder::open(&repo, n))
102 +
        .collect::<std::result::Result<_, _>>()?;
103 +
    if as_json_out {
104 +
        let items: Vec<String> = kept
105 +
            .iter()
106 +
            .map(|k| {
107 +
                let files = cylinder::tree(&repo, &k.artifact)
108 +
                    .map(|t| t.entries.len())
109 +
                    .unwrap_or(0);
110 +
                as_json(k, files)
111 +
            })
112 +
            .collect();
113 +
        println!("[{}]", items.join(","));
114 +
        return Ok(0);
115 +
    }
116 +
    if kept.is_empty() {
117 +
        println!("No artifacts yet: oxy artifact pack DIR --kind archive");
118 +
    }
119 +
    for k in &kept {
120 +
        println!("{}", summary(k));
121 +
    }
122 +
    Ok(0)
123 +
}
124 +
125 +
pub fn show(name: &str, as_json_out: bool) -> Result<i32> {
126 +
    let repo = open()?;
127 +
    let k = cylinder::open(&repo, name)?;
128 +
    let t = cylinder::tree(&repo, &k.artifact)?;
129 +
    if as_json_out {
130 +
        println!("{}", as_json(&k, t.entries.len()));
131 +
        return Ok(0);
132 +
    }
133 +
    println!("{}", summary(&k));
134 +
    println!("{}", ui::kv("name", &k.name));
135 +
    if let Some(e) = &k.artifact.entrypoint {
136 +
        println!("{}", ui::kv("entrypoint", e.join(" ")));
137 +
    }
138 +
    for (key, v) in &k.artifact.labels {
139 +
        println!("{}", ui::kv("label", format!("{key}={v}")));
140 +
    }
141 +
    for (key, v) in &k.artifact.env {
142 +
        println!("{}", ui::kv("env", format!("{key}={v}")));
143 +
    }
144 +
    for e in &t.entries {
145 +
        let mode = match e.mode {
146 +
            cylinder::MODE_EXEC => "x",
147 +
            cylinder::MODE_LINK => "l",
148 +
            _ => " ",
149 +
        };
150 +
        println!("  {mode} {:>10}  {}", e.size, e.path);
151 +
    }
152 +
    Ok(0)
153 +
}
154 +
155 +
pub fn unpack(name: &str, dest: &Path) -> Result<i32> {
156 +
    let repo = open()?;
157 +
    let k = cylinder::open(&repo, name)?;
158 +
    let n = cylinder::unpack(&repo, &k, dest)?;
159 +
    println!(
160 +
        "{} {} files of {} into {}",
161 +
        ui::done("Unpacked"),
162 +
        n,
163 +
        ui::hash(&k.name[..12]),
164 +
        ui::path(dest.display())
165 +
    );
166 +
    Ok(0)
167 +
}
168 +
169 +
pub fn verify(name: &str) -> Result<i32> {
170 +
    let repo = open()?;
171 +
    let k = cylinder::open(&repo, name)?;
172 +
    let n = cylinder::verify(&repo, &k)?;
173 +
    println!(
174 +
        "{} {}: its name, tree and {} files check out",
175 +
        ui::done("Verified"),
176 +
        ui::hash(&k.name[..12]),
177 +
        n
178 +
    );
179 +
    Ok(0)
180 +
}
modified src/cli/format.rs +25 −3
…
41 unchanged lines
42 42
43 43
/// One line describing how a change is authenticated.
44 44
pub fn signature_line(repo: &Repo, h: &ChangeHash) -> Result<String> {
45 -
    Ok(match repo.authenticate_change(h)? {
45 +
    let status = repo.authenticate_change(h)?;
46 +
    let awaiting = repo
47 +
        .store()
48 +
        .get_change(h)
49 +
        .is_ok_and(|c| awaits_seal(repo, &status, &c.author));
50 +
    Ok(match status {
51 +
        SignatureStatus::Unsigned if awaiting => {
52 +
            "awaiting seal (the next push seals it under the agent's grant)".to_string()
53 +
        }
46 54
        SignatureStatus::Unsigned => "unsigned".to_string(),
47 55
        SignatureStatus::Valid {
48 56
            key,
…
81 unchanged lines
130 138
    }
131 139
}
132 140
141 +
/// Whether an unsigned change is one the next push will seal: this
142 +
/// checkout signs keyless (an agent under a grant) and the change is its
143 +
/// configured author's (oxygen#165). Read cold, "unsigned" looks like an
144 +
/// error; it is not one until the push.
145 +
pub fn awaits_seal(repo: &Repo, status: &SignatureStatus, author: &oxy_core::hash::KeyId) -> bool {
146 +
    matches!(status, SignatureStatus::Unsigned)
147 +
        && repo.config.signer_keyless
148 +
        && repo.config.author_key == Some(*author)
149 +
}
150 +
133 151
/// A compact, styled authentication marker for log lines, from a status
134 -
/// already in hand (the caller also reads who signed, see [`via`]).
135 -
pub fn signature_mark_of(status: &SignatureStatus) -> String {
152 +
/// already in hand (the caller also reads who signed, see [`via`]);
153 +
/// `awaiting` from [`awaits_seal`].
154 +
pub fn signature_mark_of(status: &SignatureStatus, awaiting: bool) -> String {
136 155
    match status {
156 +
        SignatureStatus::Unsigned if awaiting => {
157 +
            ui::dim(format!("{} awaiting seal", ui::glyph("◌", "o")))
158 +
        }
137 159
        SignatureStatus::Unsigned => ui::dim(format!("{} unsigned", ui::glyph("·", "-"))),
138 160
        SignatureStatus::Valid {
139 161
            sealed,
…
64 unchanged lines
modified src/cli/identity.rs +85 −2
…
131 unchanged lines
132 132
        ui::author(&r.identity.name),
133 133
        r.identity.email
134 134
    );
135 -
    println!("{}", ui::kv("old key", ui::dim(r.old.to_hex())));
135 +
    // Which key it started from, and where that came from (oxygen#163).
136 +
    let from = if repo.config.is_repo_set("author.key") {
137 +
        "this repository's author.key"
138 +
    } else {
139 +
        "the user config's author.key"
140 +
    };
141 +
    println!(
142 +
        "{}",
143 +
        ui::kv(
144 +
            "old key",
145 +
            format!(
146 +
                "{} {}",
147 +
                ui::dim(r.old.to_hex()),
148 +
                ui::dim(format!("(from {from})"))
149 +
            )
150 +
        )
151 +
    );
136 152
    println!("{}", ui::kv("new key", r.new.to_hex()));
137 153
    print_delegates(repo, &r.identity.delegates);
138 154
    if let Some(e) = r.identity.expires {
…
16 unchanged lines
155 171
    local: bool,
156 172
    expires: Option<u64>,
157 173
) -> Result<i32> {
158 -
    let mut repo = open()?;
174 +
    // A first identity comes before any repository (oxygen#144): only
175 +
    // --local needs one; inside one, it is kept there too.
176 +
    let mut repo = match open() {
177 +
        Ok(r) => r,
178 +
        Err(e) if local => return Err(e),
179 +
        Err(_) => {
180 +
            let (key, id) = repo::create_user_identity(
181 +
                name.as_deref(),
182 +
                email.as_deref(),
183 +
                expires.and_then(expiry_from_days),
184 +
            )?;
185 +
            println!(
186 +
                "{} identity {} <{}>",
187 +
                ui::done("Created"),
188 +
                ui::author(&id.name),
189 +
                id.email
190 +
            );
191 +
            println!("{}", ui::kv("key", key));
192 +
            println!(
193 +
                "{}",
194 +
                ui::kv("secret", ui::path(repo::default_keys_dir().display()))
195 +
            );
196 +
            println!(
197 +
                "{}",
198 +
                ui::kv(
199 +
                    "scope",
200 +
                    format!(
201 +
                        "default author in {}",
202 +
                        ui::path(repo::default_user_config().display())
203 +
                    )
204 +
                )
205 +
            );
206 +
            println!("  {} {}", ui::note(), ui::dim("changes in every repository carry this author; run oxy config --user core.sign change to sign each commit"));
207 +
            return Ok(0);
208 +
        }
209 +
    };
159 210
    let name = name.unwrap_or_else(|| repo.config.author_name.clone());
160 211
    let email = email.unwrap_or_else(|| repo.config.author_email.clone());
161 212
    let scope = if local {
…
121 unchanged lines
283 334
            );
284 335
        }
285 336
    }
337 +
    // A key with a successor is stale, and a key with two is a fork: both
338 +
    // are how an identity splits without anyone noticing (oxygen#163).
339 +
    let next = repo.successors(&key)?;
340 +
    if let Some(n) = next.first() {
341 +
        println!(
342 +
            "  {} {}",
343 +
            ui::caution(),
344 +
            ui::warn(format!(
345 +
                "this key is not your identity's newest: {} succeeds it, {} is the newest. Rotating or delegating from it would fork the identity; point author.key at the newest",
346 +
                n.short(12),
347 +
                repo.newest(n)?.short(12)
348 +
            ))
349 +
        );
350 +
    }
351 +
    let mut cur = Some(key);
352 +
    for _ in 0..256 {
353 +
        let Some(k) = cur else { break };
354 +
        let after = repo.successors(&k)?;
355 +
        if after.len() > 1 {
356 +
            println!(
357 +
                "  {} {}",
358 +
                ui::caution(),
359 +
                ui::err(format!(
360 +
                    "the identity forks at {}: {} each succeed it. Keep one branch: revoke the others' keys (oxy identity revoke KEY) and point every home's author.key at the one you keep",
361 +
                    k.short(12),
362 +
                    after.iter().map(|a| a.short(12)).collect::<Vec<_>>().join(" and ")
363 +
                ))
364 +
            );
365 +
            break;
366 +
        }
367 +
        cur = repo.identity(&k).ok().and_then(|i| i.previous);
368 +
    }
286 369
    print_delegates(&repo, &id.delegates);
287 370
    if repo.is_revoked(&key)? {
288 371
        println!(
…
714 unchanged lines
added src/cli/mcp.rs +543 −0
1 +
//! `oxy mcp` (oxygen#159): a Model Context Protocol server over stdio for
2 +
//! the repository it starts in, so an agent works through typed tools
3 +
//! instead of parsing a shell's output.
4 +
//!
5 +
//! Each tool runs this same oxy binary with the command's `--json`, in the
6 +
//! repository, with the server's environment: so a tool returns exactly
7 +
//! what the command does, and signs as whatever `OXY_HOME` configures. In
8 +
//! an agent home every change is sealed via the agent, with no environment
9 +
//! variable to forget per call. Tools that can lose work (undo, a forced
10 +
//! push) are offered only with `--allow-destructive`.
11 +
12 +
use anyhow::{bail, Result};
13 +
use oxy_core::mcp::{self, arg_str, arg_strings, ServerInfo, Tool, ToolOutput, Tools};
14 +
use serde_json::{json, Value};
15 +
use std::path::{Path, PathBuf};
16 +
use std::process::{Command, Stdio};
17 +
18 +
/// A tool's text is cut here; its structured result is then left out.
19 +
const MAX_TEXT: usize = 256 * 1024;
20 +
21 +
type Build = fn(&Value, bool) -> Result<Vec<String>, String>;
22 +
23 +
struct Spec {
24 +
    name: &'static str,
25 +
    description: &'static str,
26 +
    schema: fn() -> Value,
27 +
    build: Build,
28 +
    json: bool,
29 +
    read_only: bool,
30 +
    destructive: bool,
31 +
}
32 +
33 +
fn obj(props: Value, required: &[&str]) -> Value {
34 +
    json!({ "type": "object", "properties": props, "required": required, "additionalProperties": false })
35 +
}
36 +
37 +
fn none() -> Value {
38 +
    obj(json!({}), &[])
39 +
}
40 +
41 +
/// An option's value as `--flag=value`, so a value starting with `-` is
42 +
/// never read as another flag.
43 +
fn opt(flag: &str, value: &str) -> String {
44 +
    format!("{flag}={value}")
45 +
}
46 +
47 +
/// Positional values after `--`, for the same reason.
48 +
fn positional(out: &mut Vec<String>, values: Vec<String>) {
49 +
    if !values.is_empty() {
50 +
        out.push("--".into());
51 +
        out.extend(values);
52 +
    }
53 +
}
54 +
55 +
fn required<'a>(args: &'a Value, key: &str) -> Result<&'a str, String> {
56 +
    arg_str(args, key)
57 +
        .filter(|s| !s.trim().is_empty())
58 +
        .ok_or_else(|| format!("{key} is required"))
59 +
}
60 +
61 +
/// Paths to act on, or every path when `all` is true; never neither, so a
62 +
/// commit or amend is always a deliberate choice of what goes in.
63 +
fn paths_or_all(args: &Value) -> Result<Vec<String>, String> {
64 +
    let paths = arg_strings(args, "paths")?;
65 +
    let all = args.get("all").and_then(Value::as_bool).unwrap_or(false);
66 +
    match (paths.is_empty(), all) {
67 +
        (false, false) => Ok(paths),
68 +
        (true, true) => Ok(vec![]),
69 +
        (false, true) => Err("give paths or all: true, not both".into()),
70 +
        (true, false) => {
71 +
            Err("give the paths to include, or all: true for every changed path".into())
72 +
        }
73 +
    }
74 +
}
75 +
76 +
fn limit(args: &Value, default: u64) -> String {
77 +
    args.get("limit")
78 +
        .and_then(Value::as_u64)
79 +
        .unwrap_or(default)
80 +
        .clamp(1, 1000)
81 +
        .to_string()
82 +
}
83 +
84 +
fn specs() -> Vec<Spec> {
85 +
    vec![
86 +
        Spec {
87 +
            name: "status",
88 +
            description: "The working copy: the bookmark, changed paths, and every conflict with its kind and the changes behind it.",
89 +
            schema: none,
90 +
            build: |_, _| Ok(vec!["status".into(), "--json".into()]),
91 +
            json: true,
92 +
            read_only: true,
93 +
            destructive: false,
94 +
        },
95 +
        Spec {
96 +
            name: "diff",
97 +
            description: "Uncommitted edits in the working copy, or one change's edits when `change` is given (a ChangeId or #hash prefix).",
98 +
            schema: || obj(json!({ "change": { "type": "string" } }), &[]),
99 +
            build: |a, _| {
100 +
                let mut v = vec!["diff".into(), "--json".into()];
101 +
                positional(&mut v, arg_str(a, "change").map(|c| vec![c.to_string()]).unwrap_or_default());
102 +
                Ok(v)
103 +
            },
104 +
            json: true,
105 +
            read_only: true,
106 +
            destructive: false,
107 +
        },
108 +
        Spec {
109 +
            name: "log",
110 +
            description: "Changes, newest first: a revset (`main`, `@`, …) or the current bookmark, optionally only those touching `path`; `limit` defaults to 20.",
111 +
            schema: || obj(json!({ "revset": { "type": "string" }, "path": { "type": "string" }, "limit": { "type": "integer", "minimum": 1 } }), &[]),
112 +
            build: |a, _| {
113 +
                let mut v = vec!["log".into(), "--json".into(), opt("--limit", &limit(a, 20))];
114 +
                if let Some(r) = arg_str(a, "revset") {
115 +
                    v.push(opt("--revset", r));
116 +
                }
117 +
                if let Some(p) = arg_str(a, "path") {
118 +
                    v.push(opt("--path", p));
119 +
                }
120 +
                Ok(v)
121 +
            },
122 +
            json: true,
123 +
            read_only: true,
124 +
            destructive: false,
125 +
        },
126 +
        Spec {
127 +
            name: "show",
128 +
            description: "One change: its message, author, signatures and edits (`stat: true` for files and line counts only). `change` defaults to @, the working copy.",
129 +
            schema: || obj(json!({ "change": { "type": "string" }, "stat": { "type": "boolean" } }), &[]),
130 +
            build: |a, _| {
131 +
                let mut v = vec!["show".into(), "--json".into()];
132 +
                if a.get("stat").and_then(Value::as_bool).unwrap_or(false) {
133 +
                    v.push("--stat".into());
134 +
                }
135 +
                positional(&mut v, vec![arg_str(a, "change").unwrap_or("@").to_string()]);
136 +
                Ok(v)
137 +
            },
138 +
            json: true,
139 +
            read_only: true,
140 +
            destructive: false,
141 +
        },
142 +
        Spec {
143 +
            name: "cat",
144 +
            description: "A file's content as a bookmark, tag or change has it (`change`), or as committed on the working copy's base.",
145 +
            schema: || obj(json!({ "path": { "type": "string" }, "change": { "type": "string" } }), &["path"]),
146 +
            build: |a, _| {
147 +
                let mut v = vec!["cat".into()];
148 +
                if let Some(c) = arg_str(a, "change") {
149 +
                    v.push(opt("--change", c));
150 +
                }
151 +
                positional(&mut v, vec![required(a, "path")?.to_string()]);
152 +
                Ok(v)
153 +
            },
154 +
            json: false,
155 +
            read_only: true,
156 +
            destructive: false,
157 +
        },
158 +
        Spec {
159 +
            name: "blame",
160 +
            description: "Which change wrote each line of a file, with its line atoms.",
161 +
            schema: || obj(json!({ "path": { "type": "string" } }), &["path"]),
162 +
            build: |a, _| {
163 +
                let mut v = vec!["blame".into(), "--json".into()];
164 +
                positional(&mut v, vec![required(a, "path")?.to_string()]);
165 +
                Ok(v)
166 +
            },
167 +
            json: true,
168 +
            read_only: true,
169 +
            destructive: false,
170 +
        },
171 +
        Spec {
172 +
            name: "bookmarks",
173 +
            description: "Bookmarks with their heads and how far each is ahead of or behind its remote; `all: true` adds remotes' bookmarks.",
174 +
            schema: || obj(json!({ "all": { "type": "boolean" } }), &[]),
175 +
            build: |a, _| {
176 +
                let mut v = vec!["bookmark".into(), "list".into(), "--json".into()];
177 +
                if a.get("all").and_then(Value::as_bool).unwrap_or(false) {
178 +
                    v.push("--all".into());
179 +
                }
180 +
                Ok(v)
181 +
            },
182 +
            json: true,
183 +
            read_only: true,
184 +
            destructive: false,
185 +
        },
186 +
        Spec {
187 +
            name: "tags",
188 +
            description: "Tags with their states, messages and signers.",
189 +
            schema: none,
190 +
            build: |_, _| Ok(vec!["tag".into(), "list".into(), "--json".into()]),
191 +
            json: true,
192 +
            read_only: true,
193 +
            destructive: false,
194 +
        },
195 +
        Spec {
196 +
            name: "conflicts",
197 +
            description: "Which conflicts of the working copy's base the current files resolve, and which remain.",
198 +
            schema: none,
199 +
            build: |_, _| Ok(vec!["resolve".into(), "--json".into()]),
200 +
            json: true,
201 +
            read_only: true,
202 +
            destructive: false,
203 +
        },
204 +
        Spec {
205 +
            name: "op_log",
206 +
            description: "The operation log, newest first (what undo would take back); `limit` defaults to 20.",
207 +
            schema: || obj(json!({ "limit": { "type": "integer", "minimum": 1 } }), &[]),
208 +
            build: |a, _| Ok(vec!["op".into(), "log".into(), "--json".into(), opt("--limit", &limit(a, 20))]),
209 +
            json: true,
210 +
            read_only: true,
211 +
            destructive: false,
212 +
        },
213 +
        Spec {
214 +
            name: "identity",
215 +
            description: "Who changes are signed as here: the author, the key, delegates and the signer (an agent home signs keyless, via the agent).",
216 +
            schema: none,
217 +
            build: |_, _| Ok(vec!["identity".into(), "show".into()]),
218 +
            json: false,
219 +
            read_only: true,
220 +
            destructive: false,
221 +
        },
222 +
        Spec {
223 +
            name: "commit",
224 +
            description: "Record a change with a message: only `paths`, or every changed path with `all: true`. Signed as this server's identity.",
225 +
            schema: || obj(json!({ "message": { "type": "string" }, "paths": { "type": "array", "items": { "type": "string" } }, "all": { "type": "boolean" } }), &["message"]),
226 +
            build: |a, _| {
227 +
                let mut v = vec!["commit".into(), opt("--message", required(a, "message")?)];
228 +
                positional(&mut v, paths_or_all(a)?);
229 +
                Ok(v)
230 +
            },
231 +
            json: false,
232 +
            read_only: false,
233 +
            destructive: false,
234 +
        },
235 +
        Spec {
236 +
            name: "amend",
237 +
            description: "Fold edits into the change the working copy sits on: only `paths`, or all with `all: true`; `message` replaces its message too.",
238 +
            schema: || obj(json!({ "message": { "type": "string" }, "paths": { "type": "array", "items": { "type": "string" } }, "all": { "type": "boolean" } }), &[]),
239 +
            build: |a, _| {
240 +
                let mut v = vec!["amend".into()];
241 +
                if let Some(m) = arg_str(a, "message") {
242 +
                    v.push(opt("--message", m));
243 +
                }
244 +
                positional(&mut v, paths_or_all(a)?);
245 +
                Ok(v)
246 +
            },
247 +
            json: false,
248 +
            read_only: false,
249 +
            destructive: false,
250 +
        },
251 +
        Spec {
252 +
            name: "describe",
253 +
            description: "Change a head's message (`change` defaults to the one the working copy sits on).",
254 +
            schema: || obj(json!({ "message": { "type": "string" }, "change": { "type": "string" } }), &["message"]),
255 +
            build: |a, _| {
256 +
                let mut v = vec!["describe".into(), "--json".into(), opt("--message", required(a, "message")?)];
257 +
                positional(&mut v, arg_str(a, "change").map(|c| vec![c.to_string()]).unwrap_or_default());
258 +
                Ok(v)
259 +
            },
260 +
            json: true,
261 +
            read_only: false,
262 +
            destructive: false,
263 +
        },
264 +
        Spec {
265 +
            name: "fetch",
266 +
            description: "Fetch from a remote (default origin) without moving local bookmarks.",
267 +
            schema: || obj(json!({ "remote": { "type": "string" } }), &[]),
268 +
            build: |a, _| remote_cmd("fetch", a),
269 +
            json: true,
270 +
            read_only: false,
271 +
            destructive: false,
272 +
        },
273 +
        Spec {
274 +
            name: "pull",
275 +
            description: "Fetch from a remote (default origin) and bring the current bookmark and working copy up to date.",
276 +
            schema: || obj(json!({ "remote": { "type": "string" } }), &[]),
277 +
            build: |a, _| remote_cmd("pull", a),
278 +
            json: true,
279 +
            read_only: false,
280 +
            destructive: false,
281 +
        },
282 +
        Spec {
283 +
            name: "push",
284 +
            description: "Push the current bookmark (or `bookmark`) to a remote (default origin). Changes are sealed at push as this server's identity. `force` (drop heads the remote has) needs the server started with --allow-destructive.",
285 +
            schema: || obj(json!({ "remote": { "type": "string" }, "bookmark": { "type": "string" }, "force": { "type": "boolean" } }), &[]),
286 +
            build: |a, destructive| {
287 +
                let mut v = vec!["push".to_string(), "--json".into()];
288 +
                if let Some(b) = arg_str(a, "bookmark") {
289 +
                    v.push(opt("--bookmark", b));
290 +
                }
291 +
                if a.get("force").and_then(Value::as_bool).unwrap_or(false) {
292 +
                    if !destructive {
293 +
                        return Err("a forced push can drop the remote's heads; start oxy mcp with --allow-destructive to allow it".into());
294 +
                    }
295 +
                    v.push("--force".into());
296 +
                }
297 +
                positional(&mut v, arg_str(a, "remote").map(|r| vec![r.to_string()]).unwrap_or_default());
298 +
                Ok(v)
299 +
            },
300 +
            json: true,
301 +
            read_only: false,
302 +
            destructive: false,
303 +
        },
304 +
        Spec {
305 +
            name: "tlog_verify",
306 +
            description: "Check the hub's transparency log: that it shows everyone the same history, with this repository's entries and each witness's cosignature.",
307 +
            schema: none,
308 +
            build: |_, _| Ok(vec!["tlog".into(), "verify".into(), "--json".into()]),
309 +
            json: true,
310 +
            read_only: true,
311 +
            destructive: false,
312 +
        },
313 +
        Spec {
314 +
            name: "undo",
315 +
            description: "Take back the last operation (see op_log). Offered only when the server was started with --allow-destructive.",
316 +
            schema: none,
317 +
            build: |_, _| Ok(vec!["undo".into()]),
318 +
            json: false,
319 +
            read_only: false,
320 +
            destructive: true,
321 +
        },
322 +
    ]
323 +
}
324 +
325 +
fn remote_cmd(command: &str, a: &Value) -> Result<Vec<String>, String> {
326 +
    let mut v = vec![command.to_string(), "--json".into()];
327 +
    positional(
328 +
        &mut v,
329 +
        arg_str(a, "remote")
330 +
            .map(|r| vec![r.to_string()])
331 +
            .unwrap_or_default(),
332 +
    );
333 +
    Ok(v)
334 +
}
335 +
336 +
struct OxyTools {
337 +
    exe: PathBuf,
338 +
    dir: PathBuf,
339 +
    allow_destructive: bool,
340 +
}
341 +
342 +
impl OxyTools {
343 +
    fn offered(&self) -> impl Iterator<Item = Spec> + '_ {
344 +
        specs()
345 +
            .into_iter()
346 +
            .filter(|s| self.allow_destructive || !s.destructive)
347 +
    }
348 +
349 +
    fn run(&self, spec: &Spec, argv: Vec<String>) -> ToolOutput {
350 +
        let out = Command::new(&self.exe)
351 +
            .arg("--plain")
352 +
            .args(&argv)
353 +
            .current_dir(&self.dir)
354 +
            .stdin(Stdio::null())
355 +
            .output();
356 +
        let out = match out {
357 +
            Ok(o) => o,
358 +
            Err(e) => return ToolOutput::error(format!("could not run oxy: {e}")),
359 +
        };
360 +
        let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
361 +
        let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
362 +
        if !out.status.success() {
363 +
            let why = if stderr.trim().is_empty() {
364 +
                stdout
365 +
            } else {
366 +
                stderr
367 +
            };
368 +
            return ToolOutput::error(format!("oxy {} failed: {}", argv.join(" "), why.trim()));
369 +
        }
370 +
        let mut result = ToolOutput::ok(stdout.clone());
371 +
        if spec.json {
372 +
            result.structured = serde_json::from_str(&stdout).ok();
373 +
        } else if !stderr.trim().is_empty() && stdout.trim().is_empty() {
374 +
            // Commands that report on stderr (commit's summary) still say what they did.
375 +
            result.text = stderr;
376 +
        }
377 +
        if result.text.len() > MAX_TEXT {
378 +
            let mut cut = MAX_TEXT;
379 +
            while !result.text.is_char_boundary(cut) {
380 +
                cut -= 1;
381 +
            }
382 +
            result.text.truncate(cut);
383 +
            result.text.push_str(
384 +
                "\n… (cut at 256 KiB: narrow the request, e.g. a smaller limit or one path)",
385 +
            );
386 +
            result.structured = None;
387 +
        }
388 +
        result
389 +
    }
390 +
}
391 +
392 +
impl Tools for OxyTools {
393 +
    fn list(&self) -> Vec<Tool> {
394 +
        self.offered()
395 +
            .map(|s| Tool {
396 +
                name: s.name.into(),
397 +
                description: s.description.into(),
398 +
                input_schema: (s.schema)(),
399 +
                read_only: s.read_only,
400 +
                destructive: s.destructive,
401 +
            })
402 +
            .collect()
403 +
    }
404 +
405 +
    fn call(&self, name: &str, args: &Value) -> Option<ToolOutput> {
406 +
        let spec = self.offered().find(|s| s.name == name)?;
407 +
        Some(match (spec.build)(args, self.allow_destructive) {
408 +
            Ok(argv) => self.run(&spec, argv),
409 +
            Err(why) => ToolOutput::error(why),
410 +
        })
411 +
    }
412 +
}
413 +
414 +
/// The first line of `oxy identity show`, said in `initialize` so the client
415 +
/// knows who its changes will be signed as.
416 +
fn signer_line(exe: &Path, dir: &Path) -> String {
417 +
    let out = Command::new(exe)
418 +
        .args(["--plain", "identity", "show"])
419 +
        .current_dir(dir)
420 +
        .stdin(Stdio::null())
421 +
        .output();
422 +
    match out {
423 +
        Ok(o) if o.status.success() => {
424 +
            let text = String::from_utf8_lossy(&o.stdout);
425 +
            let who = text.lines().next().unwrap_or("").trim_start_matches(['#', '▍', ' ']).trim().to_string();
426 +
            let signing = text.lines().find(|l| l.trim_start().starts_with("signing")).map(|l| l.trim().to_string());
427 +
            match signing {
428 +
                Some(s) => format!("{who}; {s}"),
429 +
                None => who,
430 +
            }
431 +
        }
432 +
        _ => "no identity is configured: commits will be refused until one is (oxy identity create, or an agent home with OXY_HOME)".into(),
433 +
    }
434 +
}
435 +
436 +
pub fn serve(dir: Option<PathBuf>, allow_destructive: bool) -> Result<i32> {
437 +
    let dir = match dir {
438 +
        Some(d) => d,
439 +
        None => std::env::current_dir()?,
440 +
    };
441 +
    if !dir.join(oxy_core::repo::OXY_DIR).is_dir() {
442 +
        bail!("{} is not the root of an Oxygen repository (no {}): start oxy mcp in a checkout's root, or name it", dir.display(), oxy_core::repo::OXY_DIR);
443 +
    }
444 +
    let exe = std::env::current_exe()?;
445 +
    let signer = signer_line(&exe, &dir);
446 +
    let info = ServerInfo {
447 +
        name: "oxy".into(),
448 +
        version: env!("CARGO_PKG_VERSION").into(),
449 +
        instructions: format!(
450 +
            "Oxygen version control for the repository at {}. Changes are signed as: {signer}. Prefer these tools to running oxy in a shell. \
451 +
             commit and amend take explicit paths (or all: true). {}",
452 +
            dir.display(),
453 +
            if allow_destructive { "Destructive tools (undo, forced push) are enabled: use them only when asked." } else { "Destructive tools (undo, forced push) are off." }
454 +
        ),
455 +
    };
456 +
    let tools = OxyTools {
457 +
        exe,
458 +
        dir,
459 +
        allow_destructive,
460 +
    };
461 +
    let stdin = std::io::stdin();
462 +
    mcp::serve(&info, &tools, stdin.lock(), std::io::stdout().lock())?;
463 +
    Ok(0)
464 +
}
465 +
466 +
#[cfg(test)]
467 +
mod tests {
468 +
    use super::*;
469 +
470 +
    fn build(name: &str, args: Value, destructive: bool) -> Result<Vec<String>, String> {
471 +
        let spec = specs().into_iter().find(|s| s.name == name).unwrap();
472 +
        (spec.build)(&args, destructive)
473 +
    }
474 +
475 +
    #[test]
476 +
    fn values_never_read_as_flags() {
477 +
        assert_eq!(
478 +
            build(
479 +
                "commit",
480 +
                json!({"message": "-m oops", "paths": ["-rf"]}),
481 +
                false
482 +
            )
483 +
            .unwrap(),
484 +
            ["commit", "--message=-m oops", "--", "-rf"]
485 +
        );
486 +
        assert_eq!(
487 +
            build("cat", json!({"path": "--help", "change": "main"}), false).unwrap(),
488 +
            ["cat", "--change=main", "--", "--help"]
489 +
        );
490 +
        assert_eq!(
491 +
            build("log", json!({"limit": 5000}), false).unwrap(),
492 +
            ["log", "--json", "--limit=1000"],
493 +
            "limit is capped"
494 +
        );
495 +
    }
496 +
497 +
    #[test]
498 +
    fn commits_name_what_goes_in() {
499 +
        assert!(build("commit", json!({"message": "x"}), false)
500 +
            .unwrap_err()
501 +
            .contains("all: true"));
502 +
        assert!(build(
503 +
            "commit",
504 +
            json!({"message": "x", "paths": ["a"], "all": true}),
505 +
            false
506 +
        )
507 +
        .is_err());
508 +
        assert_eq!(
509 +
            build("commit", json!({"message": "x", "all": true}), false).unwrap(),
510 +
            ["commit", "--message=x"]
511 +
        );
512 +
        assert!(build("commit", json!({"message": " ", "all": true}), false)
513 +
            .unwrap_err()
514 +
            .contains("message"));
515 +
    }
516 +
517 +
    #[test]
518 +
    fn destructive_tools_need_the_flag() {
519 +
        let off = OxyTools {
520 +
            exe: "oxy".into(),
521 +
            dir: ".".into(),
522 +
            allow_destructive: false,
523 +
        };
524 +
        let on = OxyTools {
525 +
            exe: "oxy".into(),
526 +
            dir: ".".into(),
527 +
            allow_destructive: true,
528 +
        };
529 +
        assert!(!off.list().iter().any(|t| t.name == "undo"));
530 +
        assert!(
531 +
            off.call("undo", &json!({})).is_none(),
532 +
            "an unoffered tool is unknown"
533 +
        );
534 +
        assert!(on.list().iter().any(|t| t.name == "undo" && t.destructive));
535 +
        assert!(build("push", json!({"force": true}), false)
536 +
            .unwrap_err()
537 +
            .contains("--allow-destructive"));
538 +
        assert_eq!(
539 +
            build("push", json!({"force": true, "remote": "earth"}), true).unwrap(),
540 +
            ["push", "--json", "--force", "--", "earth"]
541 +
        );
542 +
    }
543 +
}
modified src/cli/mod.rs +134 −2
1 1
//! Command-line definitions and dispatch.
2 2
3 3
mod approve;
4 +
mod artifact;
4 5
mod auth;
5 6
mod format;
6 7
mod identity;
7 8
mod json;
9 +
mod mcp;
8 10
mod proposal;
9 11
mod receipt;
10 12
mod sync;
…
196 unchanged lines
207 209
        #[arg(long)]
208 210
        json: bool,
209 211
    },
212 +
    /// Serve this repository to an AI agent over the Model Context Protocol (stdio), signing as this checkout's identity
213 +
    Mcp {
214 +
        /// The repository's root (default: the current directory)
215 +
        dir: Option<PathBuf>,
216 +
        /// Also offer tools that can lose work: undo, and a forced push
217 +
        #[arg(long)]
218 +
        allow_destructive: bool,
219 +
    },
210 220
    /// Print the version of oxy, its repository format, and protocol
211 221
    Version {
212 222
        /// Machine-readable output: one JSON document on stdout
…
198 unchanged lines
411 421
        #[command(subcommand)]
412 422
        cmd: TlogCmd,
413 423
    },
424 +
    /// Artifacts: a directory packed into a tree of this repository's chunks (Cylinder, docs/cylinder.md)
425 +
    Artifact {
426 +
        #[command(subcommand)]
427 +
        cmd: ArtifactCmd,
428 +
    },
414 429
    /// Execution receipts: signed records of what a runner ran on a state
415 430
    Receipt {
416 431
        #[command(subcommand)]
…
113 unchanged lines
530 545
        #[command(subcommand)]
531 546
        cmd: PolicyCmd,
532 547
    },
533 -
    /// Set a configuration key (author.*, core.sign, core.op-retention, core.gc-grace, core.checkpoint-interval, filter.max-blob, filter.paths, storage.cache, workspace.bookmark, remote.<name>.url|token|ca, auth.helper with --user)
548 +
    /// Set a configuration key (author.*, core.sign, core.op-retention, core.gc-grace, core.checkpoint-interval, filter.max-blob, filter.paths, storage.cache, workspace.bookmark, remote.<name>.url|token|ca, git.claim, git.colocate, auth.helper with --user)
534 549
    Config {
535 550
        /// The key, such as author.name or remote.origin.token
536 551
        key: String,
…
280 unchanged lines
817 832
    Remove {
818 833
        /// The subrepo to unregister
819 834
        path: String,
835 +
        /// Delete its directory too (#164); refused while it has uncommitted edits
836 +
        #[arg(long)]
837 +
        delete: bool,
838 +
        /// With --delete: delete it even with uncommitted edits
839 +
        #[arg(long, requires = "delete")]
840 +
        force: bool,
841 +
    },
842 +
}
843 +
844 +
#[derive(Subcommand)]
845 +
enum ArtifactCmd {
846 +
    /// Pack DIR into an artifact: each file a blob of this repository's store, so files and chunks it holds cost nothing
847 +
    Pack {
848 +
        /// The directory whose files make the artifact
849 +
        dir: std::path::PathBuf,
850 +
        /// container, runner-image, package, archive or report
851 +
        #[arg(long, default_value = "archive")]
852 +
        kind: String,
853 +
        /// x86_64, riscv64, aarch64…
854 +
        #[arg(long)]
855 +
        arch: Option<String>,
856 +
        /// linux, darwin, none…
857 +
        #[arg(long)]
858 +
        os: Option<String>,
859 +
        /// The default command, one word per flag
860 +
        #[arg(long = "entrypoint", value_name = "WORD")]
861 +
        entrypoint: Vec<String>,
862 +
        /// KEY=VALUE for the environment (repeatable)
863 +
        #[arg(long = "env", value_name = "KEY=VALUE")]
864 +
        env: Vec<String>,
865 +
        /// KEY=VALUE metadata (repeatable)
866 +
        #[arg(long = "label", value_name = "KEY=VALUE")]
867 +
        labels: Vec<String>,
868 +
        /// Machine-readable output: one JSON document on stdout
869 +
        #[arg(long)]
870 +
        json: bool,
871 +
    },
872 +
    /// The artifacts kept here
873 +
    List {
874 +
        /// Machine-readable output: one JSON document on stdout
875 +
        #[arg(long)]
876 +
        json: bool,
877 +
    },
878 +
    /// One artifact and its files
879 +
    Show {
880 +
        /// Its name, or a unique prefix of at least six characters
881 +
        name: String,
882 +
        /// Machine-readable output: one JSON document on stdout
883 +
        #[arg(long)]
884 +
        json: bool,
885 +
    },
886 +
    /// Write an artifact's files into DEST (missing or empty)
887 +
    Unpack {
888 +
        /// Its name, or a unique prefix of at least six characters
889 +
        name: String,
890 +
        /// Where to write its files
891 +
        dest: std::path::PathBuf,
820 892
    },
893 +
    /// Check an artifact's name, tree and every file against the store
894 +
    Verify {
895 +
        /// Its name, or a unique prefix of at least six characters
896 +
        name: String,
897 +
    },
821 898
}
822 899
823 900
#[derive(Subcommand)]
…
222 unchanged lines
1046 1123
1047 1124
#[derive(Subcommand)]
1048 1125
enum GitCmd {
1126 +
    /// Keep a .git beside .oxy, up to date after every oxy command, so editors' Git features work here (one way: oxy to Git)
1127 +
    Colocate {
1128 +
        /// Stop updating it (the .git is left as it is)
1129 +
        #[arg(long)]
1130 +
        off: bool,
1131 +
    },
1049 1132
    /// Export the current bookmark to a Git repository (created bare if absent)
1050 1133
    Export {
1051 1134
        /// Git repository to write to (created bare if absent)
…
141 unchanged lines
1193 1276
    },
1194 1277
}
1195 1278
1279 +
/// After a command, bring the side-by-side `.git` up to date when this
1280 +
/// checkout keeps one (`git.colocate`). It never fails the command: a
1281 +
/// problem is a warning, and the next command tries again.
1282 +
pub fn after_command() {
1283 +
    if AT_OP.get().and_then(|o| o.as_deref()).is_some() {
1284 +
        return;
1285 +
    }
1286 +
    let Ok(dir) = std::env::current_dir() else {
1287 +
        return;
1288 +
    };
1289 +
    let Ok(repo) = Repo::open(&dir) else { return };
1290 +
    if !repo.config.git_colocate || repo.is_bare() {
1291 +
        return;
1292 +
    }
1293 +
    if let Err(e) = repo.git_colocate_sync() {
1294 +
        eprintln!(
1295 +
            "{} the .git beside this checkout was not updated: {e}",
1296 +
            ui::warn("!")
1297 +
        );
1298 +
    }
1299 +
}
1300 +
1196 1301
/// Open the repository containing the current directory.
1197 1302
fn open() -> Result<Repo> {
1198 1303
    let repo = Repo::open(&std::env::current_dir()?).context("opening repository")?;
…
72 unchanged lines
1271 1376
        }
1272 1377
        Cmd::Cat { path, change } => workspace::cat(&path, change.as_deref()),
1273 1378
        Cmd::Root { json } => workspace::root(json),
1379 +
        Cmd::Mcp {
1380 +
            dir,
1381 +
            allow_destructive,
1382 +
        } => mcp::serve(dir, allow_destructive),
1274 1383
        Cmd::Version { json } => workspace::version(json),
1275 1384
        Cmd::Discard { paths, hunks } => workspace::discard(&paths, &hunks),
1276 1385
        Cmd::Hunks { json } => workspace::hunks(json),
…
147 unchanged lines
1424 1533
            } => sync::submodule_add(&url, &path, bookmark.as_deref()),
1425 1534
            SubmoduleCmd::Update { init, paths } => sync::submodule_update(init, &paths),
1426 1535
            SubmoduleCmd::Status { json } => sync::submodule_status(json),
1427 -
            SubmoduleCmd::Remove { path } => sync::submodule_remove(&path),
1536 +
            SubmoduleCmd::Remove {
1537 +
                path,
1538 +
                delete,
1539 +
                force,
1540 +
            } => sync::submodule_remove(&path, delete, force),
1428 1541
        },
1429 1542
        Cmd::Witness {
1430 1543
            cmd:
…
14 unchanged lines
1445 1558
                json,
1446 1559
            } => tlog::verify(hub.as_deref(), key.as_deref(), all, json),
1447 1560
        },
1561 +
        Cmd::Artifact { cmd } => match cmd {
1562 +
            ArtifactCmd::Pack {
1563 +
                dir,
1564 +
                kind,
1565 +
                arch,
1566 +
                os,
1567 +
                entrypoint,
1568 +
                env,
1569 +
                labels,
1570 +
                json,
1571 +
            } => artifact::pack(&dir, &kind, arch, os, entrypoint, &env, &labels, json),
1572 +
            ArtifactCmd::List { json } => artifact::list(json),
1573 +
            ArtifactCmd::Show { name, json } => artifact::show(&name, json),
1574 +
            ArtifactCmd::Unpack { name, dest } => artifact::unpack(&name, &dest),
1575 +
            ArtifactCmd::Verify { name } => artifact::verify(&name),
1576 +
        },
1448 1577
        Cmd::Receipt { cmd } => match cmd {
1449 1578
            ReceiptCmd::List { rev, json } => receipt::list(rev.as_deref(), json),
1450 1579
            ReceiptCmd::Show {
…
86 unchanged lines
1537 1666
            },
1538 1667
        },
1539 1668
        Cmd::Git {
1669 +
            cmd: GitCmd::Colocate { off },
1670 +
        } => sync::git_colocate(off),
1671 +
        Cmd::Git {
1540 1672
            cmd: GitCmd::Export { git_dir, branch },
1541 1673
        } => sync::git_export(&git_dir, branch.as_deref()),
1542 1674
        Cmd::Git {
…
120 unchanged lines
modified src/cli/sync.rs +110 −7
…
371 unchanged lines
372 372
    let repo = open()?;
373 373
    let remote = remote_name(&repo, remote);
374 374
    let sp = ui::spinner(&format!("Fetching from {remote}…"));
375 -
    let report = repo.fetch(&remote)?;
375 +
    let report = noted(&repo, &remote, repo.fetch(&remote))?;
376 376
    sp.clear();
377 377
    if as_json {
378 378
        println!(
…
160 unchanged lines
539 539
    let repo = open()?;
540 540
    let remote = remote_name(&repo, remote);
541 541
    let sp = ui::spinner(&format!("Pulling from {remote}…"));
542 -
    let r = repo.pull(&remote)?;
542 +
    let r = noted(&repo, &remote, repo.pull(&remote))?;
543 543
    if as_json {
544 544
        sp.clear();
545 545
        let conflicted = repo
…
143 unchanged lines
689 689
    let sp = ui::spinner(&format!("Pushing to {remote}…"));
690 690
    let bookmark_name = bookmark.map_or_else(|| repo.config.bookmark.clone(), String::from);
691 691
    // A long push goes in parts: the spinner says which.
692 -
    let results = repo.push_bookmark_with(&remote, &bookmark_name, force, &mut |m| {
693 -
        sp.set(&format!("Pushing to {remote}… {m}"))
694 -
    })?;
692 +
    let results = noted(
693 +
        &repo,
694 +
        &remote,
695 +
        repo.push_bookmark_with(&remote, &bookmark_name, force, &mut |m| {
696 +
            sp.set(&format!("Pushing to {remote}… {m}"))
697 +
        }),
698 +
    )?;
695 699
    sp.clear();
696 700
    if as_json {
697 701
        // The state each accepted bookmark now holds on the remote (its local
…
78 unchanged lines
776 780
    Ok(0)
777 781
}
778 782
783 +
/// `oxy git colocate [--off]`: keep a `.git` beside `.oxy` (spec §23).
784 +
pub fn git_colocate(off: bool) -> Result<i32> {
785 +
    let mut repo = open()?;
786 +
    if repo.is_bare() {
787 +
        anyhow::bail!("a bare repository has no working copy for a .git beside it");
788 +
    }
789 +
    repo.config
790 +
        .set("git.colocate", if off { "false" } else { "true" })?;
791 +
    repo.save_config()?;
792 +
    if off {
793 +
        println!(
794 +
            "{} the .git beside this checkout is no longer updated {}",
795 +
            ui::done("Stopped:"),
796 +
            ui::dim("(it is left as it is: delete .git yourself if you want it gone)")
797 +
        );
798 +
        return Ok(0);
799 +
    }
800 +
    let sp = ui::spinner("Exporting to .git…");
801 +
    repo.git_colocate_sync()?;
802 +
    sp.clear();
803 +
    println!(
804 +
        "{} .git beside .oxy holds {} {}",
805 +
        ui::done("Colocated:"),
806 +
        ui::bookmark(&repo.config.bookmark),
807 +
        ui::dim("(updated after every oxy command; Git's own commits are not read back: commit with oxy)")
808 +
    );
809 +
    Ok(0)
810 +
}
811 +
779 812
pub fn git_export(git_dir: &std::path::Path, branch: Option<&str>) -> Result<i32> {
780 813
    let repo = open()?;
781 814
    let sp = ui::spinner("Exporting to Git…");
…
421 unchanged lines
1203 1236
    Ok(0)
1204 1237
}
1205 1238
1206 -
pub fn submodule_remove(path: &str) -> Result<i32> {
1239 +
pub fn submodule_remove(path: &str, delete: bool, force: bool) -> Result<i32> {
1207 1240
    let repo = open()?;
1241 +
    let dir = repo.root().join(path.trim_end_matches('/'));
1242 +
    // Look before unregistering, so a refused delete changes nothing.
1243 +
    if delete && dir.exists() && !force {
1244 +
        if let Ok(sub) = oxy_core::repo::Repo::open(&dir) {
1245 +
            let snap = sub.snapshot("oxy submodule remove --delete")?;
1246 +
            if !snap.statuses.is_empty() {
1247 +
                anyhow::bail!(
1248 +
                    "{path} has {} with uncommitted edits; commit them in the subrepo (and push them if you want them kept), or pass --force to delete them",
1249 +
                    plural(snap.statuses.len(), "file")
1250 +
                );
1251 +
            }
1252 +
        }
1253 +
    }
1208 1254
    repo.submodule_remove(path)?;
1255 +
    if !delete {
1256 +
        println!(
1257 +
            "{} subrepo {} {}",
1258 +
            ui::done("Removed"),
1259 +
            ui::path(path),
1260 +
            ui::dim("(directory left in place; commit to record the removal)")
1261 +
        );
1262 +
        println!(
1263 +
            "  {} {}",
1264 +
            ui::note(),
1265 +
            ui::dim(format!(
1266 +
                "to delete the directory too: oxy submodule remove {path} --delete"
1267 +
            ))
1268 +
        );
1269 +
        return Ok(0);
1270 +
    }
1271 +
    if dir.exists() {
1272 +
        delete_tree(&dir)?;
1273 +
    }
1209 1274
    println!(
1210 1275
        "{} subrepo {} {}",
1211 1276
        ui::done("Removed"),
1212 1277
        ui::path(path),
1213 -
        ui::dim("(directory left in place; commit to record the removal)")
1278 +
        ui::dim("and its directory (commit to record the removal)")
1214 1279
    );
1215 1280
    Ok(0)
1216 1281
}
1217 1282
1283 +
/// Delete a directory tree that holds a repository: its object files are
1284 +
/// read-only, so each is made writable first (#164).
1285 +
fn delete_tree(dir: &std::path::Path) -> Result<()> {
1286 +
    fn writable(p: &std::path::Path) -> std::io::Result<()> {
1287 +
        let meta = std::fs::symlink_metadata(p)?;
1288 +
        if meta.file_type().is_symlink() {
1289 +
            return Ok(());
1290 +
        }
1291 +
        let mut perms = meta.permissions();
1292 +
        if perms.readonly() {
1293 +
            #[allow(clippy::permissions_set_readonly_false)]
1294 +
            perms.set_readonly(false);
1295 +
            std::fs::set_permissions(p, perms)?;
1296 +
        }
1297 +
        if meta.is_dir() {
1298 +
            for e in std::fs::read_dir(p)? {
1299 +
                writable(&e?.path())?;
1300 +
            }
1301 +
        }
1302 +
        Ok(())
1303 +
    }
1304 +
    writable(dir).map_err(|e| anyhow::anyhow!("{}: {e}", dir.display()))?;
1305 +
    std::fs::remove_dir_all(dir).map_err(|e| anyhow::anyhow!("{}: {e}", dir.display()))?;
1306 +
    Ok(())
1307 +
}
1308 +
1218 1309
pub fn deepen(remote: &str, depth: u32) -> Result<i32> {
1219 1310
    let repo = open()?;
1220 1311
    if !repo.is_shallow() {
…
17 unchanged lines
1238 1329
    ));
1239 1330
    Ok(0)
1240 1331
}
1332 +
1333 +
/// `r`, with a note when the remote's own token was refused while another
1334 +
/// sign-in for its hub would do (oxygen#155).
1335 +
fn noted<T>(repo: &oxy_core::repo::Repo, remote: &str, r: oxy_core::repo::Result<T>) -> Result<T> {
1336 +
    r.map_err(|e| {
1337 +
        let m = e.to_string();
1338 +
        match repo.stale_token_note(remote, &m) {
1339 +
            Some(note) => anyhow::anyhow!("{m}\n  note: {note}"),
1340 +
            None => e.into(),
1341 +
        }
1342 +
    })
1343 +
}
modified src/cli/workspace.rs +110 −4
1 1
//! Commands that act on the local repository and working copy.
2 2
3 3
use super::format::{
4 -
    author_name, deps_line, deps_of, plural, short_id, signature_line, signature_mark_of,
5 -
    subject_of, via, when,
4 +
    author_name, awaits_seal, deps_line, deps_of, plural, short_id, signature_line,
5 +
    signature_mark_of, subject_of, via, when,
6 6
};
7 7
use super::json;
8 8
use super::open;
…
235 unchanged lines
244 244
        );
245 245
        print_behind(&repo);
246 246
        print_statuses(&statuses);
247 +
        print_pin_moves(&pin_moves(&repo, None));
247 248
        print_untracked_nested(&repo, &repo.untracked_nested());
248 249
        print_skipped_large(&repo);
249 250
        return Ok(0);
…
25 unchanged lines
275 276
    );
276 277
    print_behind(&repo);
277 278
    print_statuses(&snap.statuses);
279 +
    print_pin_moves(&pin_moves(&repo, None));
278 280
    print_untracked_nested(&repo, &snap.untracked_nested);
279 281
    print_skipped_large(&repo);
280 282
    if !snap.base_snapshot.is_conflicted() {
…
140 unchanged lines
421 423
    } else {
422 424
        oxy_core::repo::Selection::Paths(paths.to_vec())
423 425
    };
426 +
    // Pins this commit records as moved, said after it (oxygen#162): a pin
427 +
    // moved by work inside a subrepo is a pin bump, easy to make unasked.
428 +
    let moves = match &selection {
429 +
        oxy_core::repo::Selection::All => pin_moves(&repo, None),
430 +
        oxy_core::repo::Selection::Paths(p) => pin_moves(&repo, Some(p)),
431 +
        oxy_core::repo::Selection::Hunks(_) => Vec::new(),
432 +
    };
424 433
    let sp = ui::spinner("Committing…");
425 434
    let (h, partial) = repo.commit_selection_partial(message, &deps, &selection)?;
426 435
    // The metadata only: a first commit's change holds the whole tree.
…
5 unchanged lines
432 441
        ui::hash(format!("#{}", h.short(12))),
433 442
        ui::subject(subject_of(message))
434 443
    ));
444 +
    for m in &moves {
445 +
        let line = match (&m.to, m.dirty) {
446 +
            (Some(to), _) => format!(
447 +
                "recorded subrepo {}'s pin moving {} {} {}: a pin bump. Meant only your own files? `oxy commit PATHS` leaves pins out",
448 +
                m.path,
449 +
                m.from,
450 +
                ui::glyph("→", "->"),
451 +
                to
452 +
            ),
453 +
            (None, true) => format!(
454 +
                "subrepo {} has uncommitted edits of its own: the pin names its last committed state; the edits stay in it",
455 +
                m.path
456 +
            ),
457 +
            (None, false) => continue,
458 +
        };
459 +
        println!("  {} {}", ui::caution(), ui::warn(line));
460 +
    }
435 461
    print_untracked_nested(&repo, &repo.untracked_nested());
436 462
    print_skipped_large(&repo);
437 463
    // Only a commit that took part of the working copy leaves anything.
…
14 unchanged lines
452 478
    Ok(0)
453 479
}
454 480
481 +
/// A subrepo whose pin the next commit would move (`to`), or which has
482 +
/// uncommitted edits of its own (oxygen#162).
483 +
struct PinMove {
484 +
    path: String,
485 +
    from: String,
486 +
    to: Option<String>,
487 +
    dirty: bool,
488 +
}
489 +
490 +
/// The subrepos (among `paths`, when given) whose checkout is not at the
491 +
/// pin the base records, or holds uncommitted edits. Only a checkout with
492 +
/// subrepos (an `.oxymodules`) pays for the look.
493 +
fn pin_moves(repo: &oxy_core::repo::Repo, paths: Option<&[String]>) -> Vec<PinMove> {
494 +
    if !repo.root().join(".oxymodules").exists() {
495 +
        return Vec::new();
496 +
    }
497 +
    let Ok(subs) = repo.subrepos() else {
498 +
        return Vec::new();
499 +
    };
500 +
    let chosen = |p: &str| {
501 +
        paths.is_none_or(|ps| {
502 +
            ps.iter().any(|x| {
503 +
                let x = x.trim_end_matches('/');
504 +
                p == x || p.starts_with(&format!("{x}/")) || x.is_empty() || x == "."
505 +
            })
506 +
        })
507 +
    };
508 +
    subs.into_iter()
509 +
        .filter(|s| s.committed && chosen(&s.path))
510 +
        .filter_map(|s| {
511 +
            let to = s
512 +
                .actual
513 +
                .as_ref()
514 +
                .filter(|a| !a.same_state(&s.pin))
515 +
                .map(|a| a.summary());
516 +
            (to.is_some() || s.dirty).then(|| PinMove {
517 +
                from: s.pin.summary(),
518 +
                to,
519 +
                dirty: s.dirty,
520 +
                path: s.path,
521 +
            })
522 +
        })
523 +
        .collect()
524 +
}
525 +
526 +
/// `oxy status`'s word on subrepos: a moved pin is a change the next commit
527 +
/// records; edits inside a subrepo are not.
528 +
fn print_pin_moves(moves: &[PinMove]) {
529 +
    if moves.is_empty() {
530 +
        return;
531 +
    }
532 +
    println!("{}", ui::section("Subrepo pins"));
533 +
    for m in moves {
534 +
        if let Some(to) = &m.to {
535 +
            println!(
536 +
                "  {} {}  {} {} {}  {}",
537 +
                ui::warn(ui::glyph("↕", "<>")),
538 +
                ui::path(&m.path),
539 +
                ui::dim(&m.from),
540 +
                ui::dim(ui::glyph("→", "->")),
541 +
                to,
542 +
                ui::dim("pin moved: a bare `oxy commit` records it")
543 +
            );
544 +
        }
545 +
        if m.dirty {
546 +
            println!(
547 +
                "  {} {}  {}",
548 +
                ui::warn(ui::glyph("✎", "~")),
549 +
                ui::path(&m.path),
550 +
                ui::dim("uncommitted edits inside it: commit them there; the parent records only its pin")
551 +
            );
552 +
        }
553 +
    }
554 +
}
555 +
455 556
pub fn blame(path: &str, json: bool) -> Result<i32> {
456 557
    let repo = open()?;
457 558
    let lines = repo.blame(path)?;
…
368 unchanged lines
826 927
            if let Some(agent) = via(&repo, &status, &c.author) {
827 928
                head.push(ui::dim(format!("via {}", agent.name)));
828 929
            }
829 -
            head.push(signature_mark_of(&status));
930 +
            head.push(signature_mark_of(
931 +
                &status,
932 +
                awaits_seal(&repo, &status, &c.author),
933 +
            ));
830 934
        }
831 935
        println!("{}{}", node_line(row, &bullet), head.join(" "));
832 936
        let subject = if e.is_working_copy && c.message.is_empty() {
…
1018 unchanged lines
1851 1955
        repo.config.snapshot_retention_days = 0;
1852 1956
    }
1853 1957
    let sp = ui::spinner("Packing and pruning…");
1854 -
    let r = repo.gc_with(now, &mut |phase| sp.set(&format!("{phase}…")))?;
1958 +
    // Cylinder's artifacts are named from outside the history: keep them.
1959 +
    let artifacts = cylinder::roots(&repo)?;
1960 +
    let r = repo.gc_keeping(now, &artifacts, &mut |phase| sp.set(&format!("{phase}…")))?;
1855 1961
    sp.clear();
1856 1962
    if r.packed.objects > 0 {
1857 1963
        println!(
…
1315 unchanged lines
modified src/main.rs +3 −0
…
34 unchanged lines
35 35
    }
36 36
    let command = oxy_core::timing::phase("command");
37 37
    let result = cli::run();
38 +
    if result.is_ok() {
39 +
        cli::after_command();
40 +
    }
38 41
    drop(command);
39 42
    oxy_core::timing::memory_summary();
40 43
    match result {
…
8 unchanged lines
added tests/cli_colocate.rs +110 −0
1 +
//! `oxy git colocate` (oxygenhub#355, part two): a `.git` beside `.oxy`,
2 +
//! kept up to date after every oxy command, so Git tools read an Oxygen
3 +
//! checkout; oxy never takes `.git` as content.
4 +
5 +
use std::path::Path;
6 +
use std::process::Command;
7 +
8 +
fn oxy(dir: &Path, home: &Path, args: &[&str]) -> (bool, String) {
9 +
    let out = Command::new(env!("CARGO_BIN_EXE_oxy"))
10 +
        .arg("--plain")
11 +
        .args(args)
12 +
        .current_dir(dir)
13 +
        .env("OXY_HOME", home)
14 +
        .env("NO_COLOR", "1")
15 +
        .output()
16 +
        .unwrap();
17 +
    (
18 +
        out.status.success(),
19 +
        format!(
20 +
            "{}{}",
21 +
            String::from_utf8_lossy(&out.stdout),
22 +
            String::from_utf8_lossy(&out.stderr)
23 +
        ),
24 +
    )
25 +
}
26 +
27 +
fn git(dir: &Path, args: &[&str]) -> String {
28 +
    let out = Command::new("git")
29 +
        .args(args)
30 +
        .current_dir(dir)
31 +
        .env("GIT_CONFIG_NOSYSTEM", "1")
32 +
        .env("HOME", dir)
33 +
        .output()
34 +
        .unwrap();
35 +
    assert!(
36 +
        out.status.success(),
37 +
        "git {args:?}: {}",
38 +
        String::from_utf8_lossy(&out.stderr)
39 +
    );
40 +
    String::from_utf8_lossy(&out.stdout).trim().to_string()
41 +
}
42 +
43 +
#[test]
44 +
fn a_colocated_git_follows_the_bookmark() {
45 +
    if Command::new("git").arg("--version").output().is_err() {
46 +
        eprintln!("git not installed; skipping");
47 +
        return;
48 +
    }
49 +
    let tmp = tempfile::tempdir().unwrap();
50 +
    let home = tmp.path().join("home");
51 +
    std::fs::create_dir_all(&home).unwrap();
52 +
    let r = tmp.path().join("r");
53 +
    assert!(oxy(tmp.path(), &home, &["init", r.to_str().unwrap()]).0);
54 +
    assert!(
55 +
        oxy(
56 +
            &r,
57 +
            &home,
58 +
            &[
59 +
                "identity",
60 +
                "create",
61 +
                "--name",
62 +
                "Ada",
63 +
                "--email",
64 +
                "[email protected]"
65 +
            ]
66 +
        )
67 +
        .0
68 +
    );
69 +
    std::fs::write(r.join("a.txt"), "one\n").unwrap();
70 +
    assert!(oxy(&r, &home, &["commit", "-m", "first"]).0);
71 +
72 +
    let (ok, out) = oxy(&r, &home, &["git", "colocate"]);
73 +
    assert!(ok, "{out}");
74 +
    assert!(r.join(".git/HEAD").exists());
75 +
    assert_eq!(git(&r, &["log", "--format=%s"]), "first");
76 +
    assert_eq!(git(&r, &["symbolic-ref", "HEAD"]), "refs/heads/main");
77 +
    assert_eq!(
78 +
        git(&r, &["status", "--porcelain"]),
79 +
        "",
80 +
        "Git sees no difference, and not .oxy either"
81 +
    );
82 +
    let (_, status) = oxy(&r, &home, &["status"]);
83 +
    assert!(
84 +
        !status.contains(".git"),
85 +
        "oxy never takes .git as content: {status}"
86 +
    );
87 +
88 +
    // An oxy commit shows in Git without asking.
89 +
    std::fs::write(r.join("a.txt"), "one\ntwo\n").unwrap();
90 +
    assert_eq!(
91 +
        git(&r, &["status", "--porcelain"]),
92 +
        "M a.txt",
93 +
        "uncommitted work shows as modified"
94 +
    );
95 +
    assert!(oxy(&r, &home, &["commit", "-m", "second"]).0);
96 +
    assert_eq!(git(&r, &["log", "--format=%s"]), "second\nfirst");
97 +
    assert_eq!(git(&r, &["status", "--porcelain"]), "");
98 +
    assert_eq!(git(&r, &["show", "HEAD:a.txt"]), "one\ntwo");
99 +
100 +
    // Off: no more updates, the .git stays.
101 +
    assert!(oxy(&r, &home, &["git", "colocate", "--off"]).0);
102 +
    std::fs::write(r.join("b.txt"), "b\n").unwrap();
103 +
    assert!(oxy(&r, &home, &["commit", "-m", "third"]).0);
104 +
    assert_eq!(
105 +
        git(&r, &["log", "--format=%s"]),
106 +
        "second\nfirst",
107 +
        "no longer followed"
108 +
    );
109 +
    assert!(r.join(".git/HEAD").exists());
110 +
}
modified tests/cli_json.rs +118 −0
…
261 unchanged lines
262 262
263 263
/// `oxy commit -F`, `oxy describe` naming and tags as changes (feedback
264 264
/// 4 and 6).
265 +
/// Cylinder C1 (oxygen#108): a directory packs into an artifact of this
266 +
/// repository's chunks, lists and shows as JSON, survives gc, and unpacks.
267 +
#[test]
268 +
fn artifacts_pack_show_survive_gc_and_unpack() {
269 +
    let tmp = tempfile::tempdir().unwrap();
270 +
    let home = tmp.path().join("home");
271 +
    let dir = tmp.path().join("r");
272 +
    std::fs::create_dir_all(&dir).unwrap();
273 +
    assert_eq!(oxy(&dir, &home, &["init"]).0, 0);
274 +
    write(tmp.path(), "out/bin/tool", "#!/bin/sh\necho hi\n");
275 +
    write(tmp.path(), "out/README", "a tool\n");
276 +
    let out = tmp.path().join("out");
277 +
    let pack = [
278 +
        "artifact",
279 +
        "pack",
280 +
        out.to_str().unwrap(),
281 +
        "--kind",
282 +
        "package",
283 +
        "--arch",
284 +
        "x86_64",
285 +
        "--label",
286 +
        "version=0.1.0",
287 +
        "--json",
288 +
    ];
289 +
    let a = json(&dir, &home, &pack);
290 +
    let name = a["name"].as_str().unwrap().to_string();
291 +
    assert_eq!(
292 +
        (
293 +
            a["kind"].as_str(),
294 +
            a["files"].as_u64(),
295 +
            a["labels"]["version"].as_str()
296 +
        ),
297 +
        (Some("package"), Some(2), Some("0.1.0"))
298 +
    );
299 +
    let list = json(&dir, &home, &["artifact", "list", "--json"]);
300 +
    assert_eq!(list[0]["name"], name.as_str());
301 +
    let (code, _, err) = oxy(
302 +
        &dir,
303 +
        &home,
304 +
        &[
305 +
            "artifact",
306 +
            "pack",
307 +
            out.to_str().unwrap(),
308 +
            "--kind",
309 +
            "tarball",
310 +
        ],
311 +
    );
312 +
    assert_ne!(code, 0);
313 +
    assert!(err.contains("runner-image"), "{err}");
314 +
    assert_eq!(oxy(&dir, &home, &["gc", "--now"]).0, 0);
315 +
    let (code, verified, err) = oxy(&dir, &home, &["artifact", "verify", &name[..8]]);
316 +
    assert_eq!(code, 0, "{err}");
317 +
    assert!(String::from_utf8_lossy(&verified).contains("2 files check out"));
318 +
    let back = tmp.path().join("back");
319 +
    assert_eq!(
320 +
        oxy(
321 +
            &dir,
322 +
            &home,
323 +
            &["artifact", "unpack", &name, back.to_str().unwrap()]
324 +
        )
325 +
        .0,
326 +
        0
327 +
    );
328 +
    assert_eq!(
329 +
        std::fs::read_to_string(back.join("bin/tool")).unwrap(),
330 +
        "#!/bin/sh\necho hi\n"
331 +
    );
332 +
}
333 +
334 +
/// oxygen#144: a first identity needs no repository; the next repository
335 +
/// signs as it.
336 +
#[test]
337 +
fn a_first_identity_needs_no_repository() {
338 +
    let tmp = tempfile::tempdir().unwrap();
339 +
    let home = tmp.path().join("home");
340 +
    let nowhere = tmp.path().join("nowhere");
341 +
    std::fs::create_dir_all(&nowhere).unwrap();
342 +
    let local = ["identity", "create", "--local"];
343 +
    let (code, _, err) = oxy(&nowhere, &home, &local);
344 +
    assert_ne!(code, 0, "--local needs a repository");
345 +
    assert!(err.contains("no oxy repository"), "{err}");
346 +
    let ident = [
347 +
        "identity",
348 +
        "create",
349 +
        "--name",
350 +
        "Ada",
351 +
        "--email",
352 +
        "[email protected]",
353 +
    ];
354 +
    let (code, out, err) = oxy(&nowhere, &home, &ident);
355 +
    assert_eq!(code, 0, "{err}");
356 +
    let out = String::from_utf8_lossy(&out);
357 +
    assert!(
358 +
        out.contains("Created identity Ada <[email protected]>"),
359 +
        "{out}"
360 +
    );
361 +
    let user = oxy_core::repo::read_config_file(&home.join("config.toml")).unwrap();
362 +
    let key = user["author.key"].clone();
363 +
    assert_eq!(user["author.name"], "Ada");
364 +
    assert!(home.join("keys").join(format!("{key}.secret")).exists());
365 +
    assert!(!nowhere.join(".oxy").exists(), "no repository made");
366 +
367 +
    // A repository made afterwards signs its changes with it.
368 +
    let dir = tmp.path().join("r");
369 +
    std::fs::create_dir_all(&dir).unwrap();
370 +
    assert_eq!(oxy(&dir, &home, &["init"]).0, 0);
371 +
    assert_eq!(oxy(&dir, &home, &["config", "core.sign", "change"]).0, 0);
372 +
    write(&dir, "a.txt", "a\n");
373 +
    let (code, _, err) = oxy(&dir, &home, &["commit", "-m", "First"]);
374 +
    assert_eq!(code, 0, "{err}");
375 +
    let log = json(&dir, &home, &["log", "--json", "-n", "2"]);
376 +
    let first = &log["changes"][1];
377 +
    assert_eq!(first["message"], "First");
378 +
    assert_eq!(first["signature"]["status"], "signed", "{first}");
379 +
    assert_eq!(first["signature"]["key"], key.as_str());
380 +
    assert_eq!(first["signature"]["name"], "Ada");
381 +
}
382 +
265 383
#[test]
266 384
fn commit_from_a_file_and_describe_for_builds() {
267 385
    let tmp = tempfile::tempdir().unwrap();
…
500 unchanged lines
added tests/cli_mcp.rs +231 −0
1 +
//! `oxy mcp` (oxygen#159) end to end: an MCP client talks to the server over
2 +
//! stdio, lists its tools, reads the repository, commits by path as the
3 +
//! configured identity, and is refused what it may not do.
4 +
5 +
use serde_json::{json, Value};
6 +
use std::io::{BufRead, BufReader, Write};
7 +
use std::path::Path;
8 +
use std::process::{Command, Stdio};
9 +
10 +
fn oxy(dir: &Path, home: &Path, args: &[&str]) -> i32 {
11 +
    Command::new(env!("CARGO_BIN_EXE_oxy"))
12 +
        .arg("--plain")
13 +
        .args(args)
14 +
        .current_dir(dir)
15 +
        .env("OXY_HOME", home)
16 +
        .env("NO_COLOR", "1")
17 +
        .status()
18 +
        .unwrap()
19 +
        .code()
20 +
        .unwrap_or(-1)
21 +
}
22 +
23 +
/// Send each request in turn and collect the replies, one per request (the
24 +
/// notification gets none).
25 +
fn session(dir: &Path, home: &Path, extra: &[&str], requests: &[Value]) -> Vec<Value> {
26 +
    let mut child = Command::new(env!("CARGO_BIN_EXE_oxy"))
27 +
        .arg("mcp")
28 +
        .args(extra)
29 +
        .current_dir(dir)
30 +
        .env("OXY_HOME", home)
31 +
        .env("NO_COLOR", "1")
32 +
        .stdin(Stdio::piped())
33 +
        .stdout(Stdio::piped())
34 +
        .stderr(Stdio::null())
35 +
        .spawn()
36 +
        .unwrap();
37 +
    let mut stdin = child.stdin.take().unwrap();
38 +
    let mut stdout = BufReader::new(child.stdout.take().unwrap());
39 +
    let mut replies = vec![];
40 +
    for r in requests {
41 +
        writeln!(stdin, "{r}").unwrap();
42 +
        stdin.flush().unwrap();
43 +
        if r.get("id").is_some() {
44 +
            let mut line = String::new();
45 +
            stdout.read_line(&mut line).unwrap();
46 +
            replies.push(
47 +
                serde_json::from_str(&line).unwrap_or_else(|e| panic!("not JSON ({e}): {line}")),
48 +
            );
49 +
        }
50 +
    }
51 +
    drop(stdin);
52 +
    assert!(
53 +
        child.wait().unwrap().success(),
54 +
        "the server ends cleanly when its input does"
55 +
    );
56 +
    replies
57 +
}
58 +
59 +
fn call(id: u64, name: &str, args: Value) -> Value {
60 +
    json!({"jsonrpc": "2.0", "id": id, "method": "tools/call", "params": {"name": name, "arguments": args}})
61 +
}
62 +
63 +
#[test]
64 +
fn an_agent_reads_and_commits_through_mcp() {
65 +
    let tmp = tempfile::tempdir().unwrap();
66 +
    let home = tmp.path().join("home");
67 +
    std::fs::create_dir_all(&home).unwrap();
68 +
    let repo = tmp.path().join("repo");
69 +
    assert_eq!(oxy(tmp.path(), &home, &["init", repo.to_str().unwrap()]), 0);
70 +
    assert_eq!(
71 +
        oxy(
72 +
            &repo,
73 +
            &home,
74 +
            &[
75 +
                "identity",
76 +
                "create",
77 +
                "--name",
78 +
                "Ada",
79 +
                "--email",
80 +
                "[email protected]"
81 +
            ]
82 +
        ),
83 +
        0
84 +
    );
85 +
    assert_eq!(
86 +
        oxy(&repo, &home, &["config", "--user", "core.sign", "change"]),
87 +
        0
88 +
    );
89 +
    std::fs::write(repo.join("a.txt"), "one\n").unwrap();
90 +
    std::fs::write(repo.join("b.txt"), "two\n").unwrap();
91 +
92 +
    let r = session(
93 +
        &repo,
94 +
        &home,
95 +
        &[],
96 +
        &[
97 +
            json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "0"}}}),
98 +
            json!({"jsonrpc": "2.0", "method": "notifications/initialized"}),
99 +
            json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
100 +
            call(3, "status", json!({})),
101 +
            call(4, "commit", json!({"message": "everything"})),
102 +
            call(5, "commit", json!({"message": "Add a", "paths": ["a.txt"]})),
103 +
            call(6, "log", json!({"limit": 5})),
104 +
            call(7, "status", json!({})),
105 +
            call(8, "undo", json!({})),
106 +
            call(9, "push", json!({"force": true})),
107 +
            call(10, "cat", json!({"path": "a.txt", "change": "@"})),
108 +
        ],
109 +
    );
110 +
111 +
    let init = &r[0]["result"];
112 +
    assert_eq!(init["serverInfo"]["name"], "oxy");
113 +
    let said = init["instructions"].as_str().unwrap();
114 +
    assert!(
115 +
        said.contains("Ada"),
116 +
        "initialize says who changes are signed as: {said}"
117 +
    );
118 +
    assert!(
119 +
        said.contains("Destructive tools (undo, forced push) are off"),
120 +
        "{said}"
121 +
    );
122 +
123 +
    let names: Vec<&str> = r[1]["result"]["tools"]
124 +
        .as_array()
125 +
        .unwrap()
126 +
        .iter()
127 +
        .map(|t| t["name"].as_str().unwrap())
128 +
        .collect();
129 +
    for t in [
130 +
        "status",
131 +
        "diff",
132 +
        "log",
133 +
        "show",
134 +
        "cat",
135 +
        "blame",
136 +
        "bookmarks",
137 +
        "commit",
138 +
        "amend",
139 +
        "push",
140 +
        "pull",
141 +
        "tlog_verify",
142 +
    ] {
143 +
        assert!(names.contains(&t), "{t} is offered: {names:?}");
144 +
    }
145 +
    assert!(!names.contains(&"undo"), "undo needs --allow-destructive");
146 +
147 +
    let before = &r[2]["result"];
148 +
    assert_eq!(before["isError"], false, "{before}");
149 +
    assert!(
150 +
        before["structuredContent"].is_object(),
151 +
        "status comes back as JSON: {before}"
152 +
    );
153 +
    assert!(before["content"][0]["text"]
154 +
        .as_str()
155 +
        .unwrap()
156 +
        .contains("b.txt"));
157 +
158 +
    assert_eq!(
159 +
        r[3]["result"]["isError"], true,
160 +
        "a commit names its paths or all: {}",
161 +
        r[3]
162 +
    );
163 +
    assert!(r[3]["result"]["content"][0]["text"]
164 +
        .as_str()
165 +
        .unwrap()
166 +
        .contains("all: true"));
167 +
168 +
    assert_eq!(r[4]["result"]["isError"], false, "commit by path: {}", r[4]);
169 +
170 +
    let log = r[5]["result"]["content"][0]["text"].as_str().unwrap();
171 +
    assert!(log.contains("Add a"), "{log}");
172 +
    assert!(
173 +
        r[5]["result"]["structuredContent"].is_object(),
174 +
        "a JSON array comes wrapped: {}",
175 +
        r[5]
176 +
    );
177 +
178 +
    let after = r[6]["result"]["content"][0]["text"].as_str().unwrap();
179 +
    assert!(
180 +
        after.contains("b.txt") && !after.contains("a.txt"),
181 +
        "only a.txt went in: {after}"
182 +
    );
183 +
184 +
    assert!(
185 +
        r[7]["error"]["message"]
186 +
            .as_str()
187 +
            .unwrap()
188 +
            .contains("no tool named undo"),
189 +
        "{}",
190 +
        r[7]
191 +
    );
192 +
    assert_eq!(r[8]["result"]["isError"], true);
193 +
    assert!(r[8]["result"]["content"][0]["text"]
194 +
        .as_str()
195 +
        .unwrap()
196 +
        .contains("--allow-destructive"));
197 +
    assert_eq!(r[9]["result"]["content"][0]["text"], "one\n");
198 +
}
199 +
200 +
#[test]
201 +
fn destructive_tools_come_with_the_flag_and_a_bad_directory_is_refused() {
202 +
    let tmp = tempfile::tempdir().unwrap();
203 +
    let home = tmp.path().join("home");
204 +
    std::fs::create_dir_all(&home).unwrap();
205 +
    let repo = tmp.path().join("repo");
206 +
    assert_eq!(oxy(tmp.path(), &home, &["init", repo.to_str().unwrap()]), 0);
207 +
    let r = session(
208 +
        &repo,
209 +
        &home,
210 +
        &["--allow-destructive"],
211 +
        &[json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})],
212 +
    );
213 +
    let undo = r[0]["result"]["tools"]
214 +
        .as_array()
215 +
        .unwrap()
216 +
        .iter()
217 +
        .find(|t| t["name"] == "undo")
218 +
        .cloned();
219 +
    assert_eq!(
220 +
        undo.expect("undo is offered")["annotations"]["destructiveHint"],
221 +
        true
222 +
    );
223 +
224 +
    let out = Command::new(env!("CARGO_BIN_EXE_oxy"))
225 +
        .args(["mcp", tmp.path().to_str().unwrap()])
226 +
        .env("OXY_HOME", &home)
227 +
        .output()
228 +
        .unwrap();
229 +
    assert!(!out.status.success());
230 +
    assert!(String::from_utf8_lossy(&out.stderr).contains("not the root of an Oxygen repository"));
231 +
}
added tests/cli_subrepo.rs +94 −0
1 +
//! oxygen#162: a subrepo pin moved by work inside the subrepo is a change
2 +
//! the parent's next commit records; `oxy status` and `oxy commit` say so,
3 +
//! and say when a subrepo holds uncommitted edits of its own.
4 +
5 +
use std::path::Path;
6 +
use std::process::Command;
7 +
8 +
fn oxy(dir: &Path, home: &Path, args: &[&str]) -> (i32, String) {
9 +
    let out = Command::new(env!("CARGO_BIN_EXE_oxy"))
10 +
        .arg("--plain")
11 +
        .args(args)
12 +
        .current_dir(dir)
13 +
        .env("OXY_HOME", home)
14 +
        .env("NO_COLOR", "1")
15 +
        .output()
16 +
        .unwrap();
17 +
    let text = format!(
18 +
        "{}{}",
19 +
        String::from_utf8_lossy(&out.stdout),
20 +
        String::from_utf8_lossy(&out.stderr)
21 +
    );
22 +
    (out.status.code().unwrap_or(-1), text)
23 +
}
24 +
25 +
fn ok(dir: &Path, home: &Path, args: &[&str]) -> String {
26 +
    let (code, out) = oxy(dir, home, args);
27 +
    assert_eq!(code, 0, "oxy {args:?}: {out}");
28 +
    out
29 +
}
30 +
31 +
#[test]
32 +
fn moved_pins_and_edits_inside_subrepos_are_named() {
33 +
    let tmp = tempfile::tempdir().unwrap();
34 +
    let t = tmp.path();
35 +
    let home = t.join("home");
36 +
    std::fs::create_dir_all(&home).unwrap();
37 +
    ok(
38 +
        t,
39 +
        &home,
40 +
        &[
41 +
            "identity",
42 +
            "create",
43 +
            "--name",
44 +
            "Ada",
45 +
            "--email",
46 +
            "[email protected]",
47 +
        ],
48 +
    );
49 +
    // The subrepo's own repository, with one change.
50 +
    let lib = t.join("lib");
51 +
    ok(t, &home, &["init", lib.to_str().unwrap()]);
52 +
    std::fs::write(lib.join("lib.txt"), "one\n").unwrap();
53 +
    ok(&lib, &home, &["commit", "-m", "lib one"]);
54 +
    // The parent pins it.
55 +
    let app = t.join("app");
56 +
    ok(t, &home, &["init", app.to_str().unwrap()]);
57 +
    std::fs::write(app.join("app.txt"), "app\n").unwrap();
58 +
    ok(
59 +
        &app,
60 +
        &home,
61 +
        &["submodule", "add", lib.to_str().unwrap(), "deps/lib"],
62 +
    );
63 +
    ok(&app, &home, &["commit", "-m", "app with lib"]);
64 +
    let quiet = ok(&app, &home, &["status"]);
65 +
    assert!(!quiet.contains("Subrepo pins"), "{quiet}");
66 +
67 +
    // Edits inside the subrepo, not committed there: named, not a pin move.
68 +
    let inner = app.join("deps/lib");
69 +
    std::fs::write(inner.join("lib.txt"), "two\n").unwrap();
70 +
    let st = ok(&app, &home, &["status"]);
71 +
    assert!(
72 +
        st.contains("Subrepo pins") && st.contains("uncommitted edits inside it"),
73 +
        "{st}"
74 +
    );
75 +
    assert!(!st.contains("pin moved"), "{st}");
76 +
77 +
    // Committed inside: the pin has moved, and a bare commit says it records it.
78 +
    ok(&inner, &home, &["commit", "-m", "lib two"]);
79 +
    let st = ok(&app, &home, &["status"]);
80 +
    assert!(st.contains("deps/lib") && st.contains("pin moved"), "{st}");
81 +
    std::fs::write(app.join("app.txt"), "app 2\n").unwrap();
82 +
    let by_path = ok(&app, &home, &["commit", "-m", "app only", "app.txt"]);
83 +
    assert!(
84 +
        !by_path.contains("recorded subrepo"),
85 +
        "a commit by path leaves the pin out: {by_path}"
86 +
    );
87 +
    let bare = ok(&app, &home, &["commit", "-m", "everything"]);
88 +
    assert!(
89 +
        bare.contains("recorded subrepo deps/lib's pin moving") && bare.contains("a pin bump"),
90 +
        "{bare}"
91 +
    );
92 +
    let after = ok(&app, &home, &["status"]);
93 +
    assert!(!after.contains("Subrepo pins"), "recorded: {after}");
94 +
}