chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
createCommit holds linear branches, and backfill reads redirects
crates/knotless-run/src/lib.rs+14−1
| @@ -356,6 +356,9 @@ fn count(flag: &str, what: &str, value: String) -> Result<usize, String> { | ||
n.ok_or(format!("{flag} takes a number of {what}, at least 1")) | ||
} | ||
| ||
/// The most pulls a batch takes: a check record's `batch` holds no more. | ||
const MAX_BATCH: usize = 64; | ||
| ||
/// `value`, read as CPUs, as docker's `--cpus` takes them: more than none. | ||
fn cpus(value: String) -> Result<String, String> { | ||
let n: f64 = value.parse().unwrap_or(0.0); | ||
| @@ -400,7 +403,12 @@ pub fn parse_args(args: &[String]) -> Result<Options, String> { | ||
"--trust" => opts.trust.push(value()?), | ||
"--discover" => opts.discover = true, | ||
"--queue" => opts.queue = true, | ||
"--batch" => opts.batch = count(flag, "pulls", value()?)?, | ||
"--batch" => { | ||
opts.batch = count(flag, "pulls", value()?)?; | ||
if opts.batch > MAX_BATCH { | ||
return Err(format!("--batch is {MAX_BATCH} pulls at most")); | ||
} | ||
} | ||
"--depth" => { | ||
opts.depth = count(flag, "batches", value()?)?; | ||
if opts.depth > 2 { | ||
| @@ -4659,6 +4667,11 @@ mod tests { | ||
assert_eq!((opts.queue, opts.batch, opts.depth), (true, 3, 2)); | ||
let opts = args("a/b").unwrap(); | ||
assert_eq!((opts.queue, opts.batch, opts.depth), (false, 8, 2)); | ||
assert_eq!(args("--batch 64 a/b").unwrap().batch, 64); | ||
assert_eq!( | ||
args("--batch 65 a/b").unwrap_err(), | ||
"--batch is 64 pulls at most" | ||
); | ||
assert_eq!(args("--depth 1 a/b").unwrap().depth, 1); | ||
assert_eq!( | ||
args("--depth 0 a/b").unwrap_err(), | ||
crates/knotless-serve/src/gateway.rs+7−0
| @@ -809,6 +809,13 @@ async fn attempt( | ||
dst: refname.clone(), | ||
force: true, | ||
}; | ||
// What a push of this tip would be refused, a commit is: a new | ||
// branch under a linear rule takes no merge commits from its base. | ||
let owner = remote.did(); | ||
let held = gateway::refusal(&mirror.git, &snap, &update, owner, caller).await; | ||
if let Some(why) = held.map_err(AppError::internal)? { | ||
return Err(forbidden(format!("{refname}: {why}"))); | ||
} | ||
let report = push::push_by( | ||
&app.ctx, | ||
&remote, | ||
crates/knotless-serve/src/lib.rs+6−4
| @@ -9,10 +9,11 @@ | ||
//! - a JSON and raw API at `/api/<handle or did>/<name>/...` for agents that | ||
//! want a file, a tree, a log or a diff without a clone. | ||
//! | ||
//! It holds no state of its own: every mirror is a cache of the owner's | ||
//! records, revalidated against them (one round trip) when older than the | ||
//! TTL, or kept warm by the firehose or Jetstream. Every object it serves is named by its | ||
//! hash, so a client can check it. | ||
//! Everything it holds is a cache of the owners' records, which stay the | ||
//! truth: each mirror is revalidated against them (one round trip) when | ||
//! older than the TTL, or kept warm by the firehose or Jetstream, and the | ||
//! index (a SQLite file) is built from them and can be rebuilt by a backfill. | ||
//! Every object it serves is named by its hash, so a client can check it. | ||
| ||
pub mod accounts; | ||
mod api; | ||
| @@ -821,6 +822,7 @@ fn indexed_collections() -> impl Iterator<Item = &'static str> { | ||
knotless::records::nsid::REPO, | ||
knotless::records::nsid::REF, | ||
knotless::records::nsid::CLAIM, | ||
knotless::records::nsid::REDIRECT, | ||
knotless::records::nsid::CHECK, | ||
knotless::records::nsid::MINUTES, | ||
knotless::sealed::KEY_NSID, | ||
crates/knotless-serve/tests/gateway.rs+10−3
| @@ -290,6 +290,16 @@ fn protected_branches_hold_collaborators() { | ||
owner.cli(&["protect", &url, "release"], b""); | ||
let err = refused(&collab, &clone, "main:refs/heads/release").unwrap(); | ||
assert!(err.contains("no merge commits"), "{err}"); | ||
// Nor does a commit, made on a base with a merge in its history. | ||
let merged = collab.git(&clone, &["rev-parse", "HEAD"]); | ||
let note = collab.path("note"); | ||
std::fs::write(¬e, "n\n").unwrap(); | ||
let path = note.to_string_lossy(); | ||
let on_merge = [ | ||
"commit", &url, "-b", "release", "-m", "n", "--base", &merged, "--put", "n", &path, | ||
]; | ||
let (done, _, err) = collab.knotless(&on_merge, b""); | ||
assert!(!done && err.contains("no merge commits"), "{err}"); | ||
collab.git(&clone, &["reset", "-q", "--hard", &base]); | ||
let err = refused(&collab, &clone, "+HEAD~1:main").unwrap(); | ||
assert!(err.contains("only moves forward"), "{err}"); | ||
| @@ -333,9 +343,6 @@ fn protected_branches_hold_collaborators() { | ||
); | ||
let err = refused(&collab, &clone, "main").unwrap(); | ||
assert!(err.contains("only through the merge queue"), "{err}"); | ||
let note = collab.path("note"); | ||
std::fs::write(¬e, "n\n").unwrap(); | ||
let path = note.to_string_lossy(); | ||
let commit = ["commit", &url, "-b", "main", "-m", "n", "--put", "n", &path]; | ||
let (done, _, err) = collab.knotless(&commit, b""); | ||
assert!( | ||
docs/design/gateway.md+7−4
| @@ -259,10 +259,13 @@ not, whose commit's message ends with a pull's AT-URI, as every merge's | ||
does (`merge gate: <why>` on an unprotected branch); a commit that says no | ||
such thing passes, as the collaborator could push it anyway. | ||
| ||
Commits through `createCommit` build on the tip with one parent, so they | ||
pass the rest; into a `queue` branch they are refused from a collaborator | ||
before they are batched, 403 `<branch> takes pulls only through the merge | ||
queue: commit to another branch and open a pull`, and into a branch that | ||
Commits through `createCommit` build on the tip with one parent, so on an | ||
existing branch they pass the rest; a new branch under a linear rule is | ||
refused as a push of it would be, 403 `protected branch: no merge commits; | ||
squash or rebase`, when its `base` has merge commits in its history. Into a | ||
`queue` branch they are refused from a collaborator before they are | ||
batched, 403 `<branch> takes pulls only through the merge queue: commit to | ||
another branch and open a pull`, and into a branch that | ||
asks for reviews, 403 `<branch> takes changes only through reviewed pulls: | ||
commit to another branch and open a pull`. Pulls merge into a linear branch | ||
[squashed](collaboration.md#merging). | ||
docs/reference/cli.md+3−2
| @@ -471,7 +471,7 @@ add` make it one, and invite it, which `--discover` reads. | ||
| `--pulls` | off | also run open pulls, at their head merged with their base, without secrets; takes `docker` | | ||
| `--pulls-only <handle>/<name>` | none | watch that repository for its open pulls alone, never its pushes or its merge queue, which another runner runs; as there, a queued pull does not run on its own, and the rest run again as their head moves, not their base; repeatable; takes `--pulls` | | ||
| `--queue` | off | land the repositories' [merge queues](../design/merge-queue.md) where its account may push, or where they name it and no runner that may ([Who lands a queue](../design/merge-queue.md#who-lands-a-queue)); takes `docker` | | ||
| `--batch <n>` | 8 | with `--queue`, the most pulls in a batch | | ||
| `--batch <n>` | 8 | with `--queue`, the most pulls in a batch, 64 at most (a check record's `batch` holds no more) | | ||
| `--depth <n>` | 2 | with `--queue`, 2 checks the next batch on top of the one being checked; 1 turns that off; nothing else | | ||
| `--notice <secs>` | 30 | with `--queue`, how often the owner's and collaborators' queue records are read | | ||
| `--review-wait <secs>` | 1800 | with `--queue`, how long a queued pull waits for a review of its head from its branch's [gates](../design/collaboration.md#gates) before it is ejected | | ||
| @@ -482,7 +482,8 @@ and `--pids` take at least 1, as in `--batch takes | ||
a number of pulls, at least 1` and `--notice takes a number of seconds, at | ||
least 1`. `--depth 0` is refused with `--depth takes a number of batches, | ||
at least 1`, and more than 2 with `--depth is 1 or 2: one batch at most | ||
runs ahead`. `--pulls` and | ||
runs ahead`. `--batch` above 64 is refused with `--batch is 64 pulls at | ||
most`. `--pulls` and | ||
`--queue` refuse `--executor shell` (`--pulls and --queue run code anyone | ||
can open a pull with: they take --executor docker`), and so do `--cpus`, | ||
`--memory`, `--pids`, `--network`, `--runtime`, `--trusted-runtime` and | ||