chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The releases page shows how Cargo adds a crate
crates/knotless-serve/src/web/releases.rs+3−0
| @@ -40,6 +40,8 @@ pub struct ReleaseView { | ||
image: bool, | ||
#[serde(skip_serializing_if = "std::ops::Not::not")] | ||
yanked: bool, | ||
#[serde(rename = "crate", skip_serializing_if = "std::ops::Not::not")] | ||
is_crate: bool, | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
commit: Option<String>, | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
| @@ -210,6 +212,7 @@ pub(super) async fn list_releases( | ||
.as_deref() | ||
.map(|n| markdown(n, &base, &base, "", None)), | ||
image: image(&r), | ||
is_crate: r.cargo.is_some(), | ||
uri, | ||
package: r.package, | ||
version: r.version, | ||
crates/knotless-serve/tests/web.rs+5−0
| @@ -3534,6 +3534,11 @@ fn a_cargo_registry() { | ||
); | ||
let everything = site.json(&format!("{here}/api/v1/crates")); | ||
assert_eq!(everything["meta"]["total"], 3); | ||
// The releases page knows each for a crate. | ||
let repo = format!("at://{did}/dev.knotless.repo/crates"); | ||
let listed = site.json(&xrpc("listReleases", &[("repo", &repo)])); | ||
let listed = listed["releases"].as_array().unwrap(); | ||
assert!(listed.iter().all(|r| r["crate"] == true), "{listed:?}"); | ||
| ||
// A file whose bytes are not what its record says is not sent, and a | ||
// body too large is refused as it is read. | ||
docs/design/packages.md+3−1
| @@ -335,7 +335,9 @@ A Releases tab, with how many releases there are, opens the releases | ||
page: every version of every package, newest first, each with its | ||
notes, the commit it was built from and its files, each with its size | ||
and SHA-256. An image shows the `docker pull` command for it rather than | ||
its blobs. | ||
its blobs, and a crate the lines naming its repository's | ||
[registry](#crates) in `.cargo/config.toml` and the `cargo add` that | ||
adds it, rather than its `.crate`. | ||
| ||
Each file's name links to download it, from | ||
`/<owner>/<repo>/releases/download/<package>/<version>/<file>`. | ||
docs/reference/api.md+1−1
| @@ -588,7 +588,7 @@ name it has now. | ||
| `dev.knotless.searchOwners?q=` | the owners of repositories whose handle holds `q` (above) | | ||
| `dev.knotless.getActorRepos?actor=` | an account's public repositories, each with the `language` most of it is written in: from the index while a feed is followed, else as its PDS lists them; to the account itself, asking, its private ones too, each `private`, and to a member asking those it was proven a member of; asked as the account itself, apart, as `shared`, the private repositories of others it was proven a member of | | ||
| `dev.knotless.getRepo?repo=` | a repository: its owner, description, `links` (only http and https ones), `topics`, default branch, counts, languages, `collaborators` (the first twenty its record names, besides its owner) and `collaboratorCount`, `private` if it is, `archivedAt` if its owner archived it, and `site` if it has a website this server serves: its `url`, `deployedAt`, the `commit` its deployer named, and its `check` only when that check is from a runner the repository names; with an asker, `viewer`: its `star`, a `transfer` offered to it, the `labels` it may put on the repository's pulls and issues, and `admin: true` when it may change the repository's settings, as its owner or an admin who joined the organization that owns it | | ||
| `dev.knotless.listReleases?repo=&package=&limit=&cursor=` | its releases ([packages.md](../design/packages.md#on-the-pages)), newest first, 30 a page by default, of one `package` if given: each with `latest` when it is its package's latest, `yanked` when its owner withdrew it, `image` when its files are an OCI layout's, its notes as Markdown and as `html`, and its files' names, sizes and SHA-256; and every package it publishes, by name, with its latest version, how many releases it has and whether it is an image. None for a private repository or without an index | | ||
| `dev.knotless.listReleases?repo=&package=&limit=&cursor=` | its releases ([packages.md](../design/packages.md#on-the-pages)), newest first, 30 a page by default, of one `package` if given: each with `latest` when it is its package's latest, `yanked` when its owner withdrew it, `image` when its files are an OCI layout's, `crate` when it is a crate of the repository's Cargo registry, its notes as Markdown and as `html`, and its files' names, sizes and SHA-256; and every package it publishes, by name, with its latest version, how many releases it has and whether it is an image. None for a private repository or without an index | | ||
| `dev.knotless.getTree?repo=&ref=&path=` | a folder, its README and the newest checks of its last commit | | ||
| `dev.knotless.listPaths?repo=&ref=&limit=` | every folder and file at a revision, the shallowest first, at most 50,000, and `truncated` when the deepest were left out: for Go to file | | ||
| `dev.knotless.getLastCommits?repo=&ref=&path=` | the newest commit to change each entry of a folder, and at the root, how many commits the ref's history holds | | ||
lexicons/dev/knotless/defs.json+1−0
| @@ -344,6 +344,7 @@ | ||
"latest": { "type": "boolean", "description": "True for its package's latest version: the highest with no pre-release part that is not yanked." }, | ||
"image": { "type": "boolean", "description": "True when its files are a container image's OCI layout: an `index.json` beside blobs named `sha256-<hex>`, pulled by `docker pull` rather than downloaded." }, | ||
"yanked": { "type": "boolean", "description": "True when its owner withdrew it, by a dev.knotless.releaseYank record: its files can still be read, but it is no one's latest." }, | ||
"crate": { "type": "boolean", "description": "True when it is a crate, added with Cargo from its repository's registry, at `/cargo/<owner's DID>/<repository's key>/`, rather than downloaded." }, | ||
"commit": { "type": "string", "description": "The hex object ID of the commit its files were built from, if its record names one." }, | ||
"check": { "type": "string", "format": "at-uri", "description": "The dev.knotless.check of the run that built it, if its record names one." }, | ||
"notes": { "type": "string", "description": "What changed since the version before, in Markdown, as its record says." }, | ||
web/src/lib/organisms/ReleaseCard.svelte+14−3
| @@ -2,7 +2,7 @@ | ||
import type { ReleaseView } from '#lib/lexicons/dev/knotless/defs.ts'; | ||
import { page } from '$app/state'; | ||
import { ago, plural, repoHref, size } from '#lib/format.ts'; | ||
import { downloadHref, pullCommand, releaseAnchor } from '#lib/releases.ts'; | ||
import { cargoUse, downloadHref, pullCommand, releaseAnchor } from '#lib/releases.ts'; | ||
import Sha from '#lib/atoms/Sha.svelte'; | ||
import Time from '#lib/atoms/Time.svelte'; | ||
import Command from '#lib/molecules/Command.svelte'; | ||
| @@ -14,12 +14,15 @@ | ||
* commit, its notes, and its files, each with its size and SHA-256 and | ||
* linked to download; | ||
* or, for an image, the | ||
* command that pulls it rather than its blobs. A shared step's links to | ||
* the step's page. | ||
* command that pulls it rather than its blobs, and for a crate, how | ||
* Cargo adds it. A shared step's links to the step's page. | ||
*/ | ||
let { owner, name, release: r }: { owner: string; name: string; release: ReleaseView } = $props(); | ||
const step = $derived(stepOf(r.package)); | ||
const bytes = $derived(r.files.reduce((n, f) => n + f.size, 0)); | ||
const cargo = $derived( | ||
r.crate ? cargoUse(page.url.origin, r.uri, name, r.package, r.version) : undefined | ||
); | ||
</script> | ||
| ||
<article class="box release" id={releaseAnchor(r)}> | ||
| @@ -43,6 +46,14 @@ | ||
An image: {plural(r.files.length - 1, 'blob', 'blobs')} and its index, {size(bytes)} in all. | ||
</p> | ||
</div> | ||
{:else if cargo} | ||
<div class="release-pull"> | ||
<Command text={cargo.config} /> | ||
<Command text={cargo.add} /> | ||
<p class="mute"> | ||
A crate. Add the registry to <code>.cargo/config.toml</code>, then add the crate with Cargo. | ||
</p> | ||
</div> | ||
{:else} | ||
<ul class="list release-files" aria-label="Files"> | ||
{#each r.files as f (f.name)} | ||
web/src/lib/releases.ts+24−1
| @@ -1,5 +1,5 @@ | ||
// What the pages say of a repository's releases: which is a pre-release, | ||
// how an image is pulled, and where each is. | ||
// how an image is pulled, how a crate is added, and where each is. | ||
| ||
import { repoHref, urlPath } from '#lib/format.ts'; | ||
| ||
| @@ -22,6 +22,29 @@ export function pullCommand( | ||
return `docker pull ${host}/${name}:${version}`; | ||
} | ||
| ||
/** | ||
* What a crate's release, `uri`, says to Cargo, served from `origin`: the | ||
* lines of `.cargo/config.toml` naming its repository's registry, as | ||
* `registry`, and the command that adds the crate from it. The registry is | ||
* at the owner's DID and the repository's key, which open its record's | ||
* URI and key, as names can change and Cargo keeps it in `Cargo.lock`. | ||
*/ | ||
export function cargoUse( | ||
origin: string, | ||
uri: string, | ||
registry: string, | ||
pkg: string, | ||
version: string | ||
): { config: string; add: string } { | ||
const [did, , rkey] = uri.replace(/^at:\/\//, '').split('/'); | ||
const key = rkey.split('.')[0]; | ||
const name = registry.replace(/[^A-Za-z0-9_-]/g, '-').replace(/^[^A-Za-z_]/, 'r$&'); | ||
return { | ||
config: `[registries.${name}]\nindex = "sparse+${origin}/cargo/${did}/${key}/"`, | ||
add: `cargo add ${pkg}@${version} --registry ${name}` | ||
}; | ||
} | ||
| ||
/** The id a release has on the releases page, for a link to land on it. */ | ||
export const releaseAnchor = (r: { package: string; version: string }) => | ||
`${r.package}@${r.version}`; | ||