chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A private repository's pages no longer wait for its space to sync
crates/knotless-serve/src/web/settings.rs+93−65
| @@ -98,13 +98,6 @@ pub async fn get_repo_settings( | ||
Query(p): Query<SettingsParams>, | ||
) -> Result<Response> { | ||
let repo = repo::found(&app, &p.repo).await?; | ||
let url = url_of(&repo); | ||
// A private one is read as the member asking, whom finding it proved | ||
// one; a public one as this server. | ||
let reader = match crate::private::viewer().filter(|_| repo.in_space.is_some()) { | ||
Some(viewer) => crate::login::ctx_for(&app, &viewer), | ||
None => app.ctx.clone(), | ||
}; | ||
// As the index has it, which a write from the pages updates at once, | ||
// else as the mirror last synced. | ||
let record = repo.record(&app).unwrap_or(repo.synced.clone()); | ||
| @@ -118,6 +111,23 @@ pub async fn get_repo_settings( | ||
} | ||
} | ||
let gates = futures::future::join_all(gated.into_iter().map(|did| actor(&app, did))).await; | ||
// Who reads a private one but does not push, as its record says, which | ||
// sharing it writes and the appview is shown at once: its owner's word, | ||
// shown to its owner alone, as only they may list its space's members. | ||
let mut readers = None; | ||
if repo.in_space.is_some() { | ||
let owner = crate::private::viewer().is_some_and(|v| v == repo.did); | ||
let writers: Vec<&str> = record.writers().collect(); | ||
let named = record.readers.iter().map(|r| r.did.as_str()); | ||
let mut read: Vec<&str> = Vec::new(); | ||
for did in named.chain(record.runners.iter().map(|r| r.did.as_str())) { | ||
if owner && !writers.contains(&did) && !read.contains(&did) { | ||
read.push(did); | ||
} | ||
} | ||
readers = | ||
Some(futures::future::join_all(read.into_iter().map(|did| actor(&app, did))).await); | ||
} | ||
let mut triagers = Vec::new(); | ||
for t in record.triagers { | ||
triagers.push(TriagerView::new(&app, t).await); | ||
| @@ -126,13 +136,7 @@ pub async fn get_repo_settings( | ||
Some(to) => Some(basic(&app, to).await), | ||
None => None, | ||
}; | ||
let mut readers = None; | ||
if repo.in_space.is_some() { | ||
// Only its owner may list them; anyone else is shown none. | ||
let members = manage::members(&reader, &url).await.unwrap_or_default(); | ||
let read = members.iter().filter(|m| m.access == "read"); | ||
readers = Some(futures::future::join_all(read.map(|m| actor(&app, &m.did))).await); | ||
} | ||
| ||
let owners = crate::login::ctx_for(&app, &repo.did); | ||
let delegated = knotless::gateway::delegated(&owners, &repo.did); | ||
Ok(answer(Settings { | ||
| @@ -153,86 +157,105 @@ pub async fn get_repo_settings( | ||
})) | ||
} | ||
| ||
/// The most repositories whose storage [`Counts`] keeps: once full, it | ||
/// starts again, as counting one again costs only that page a wait. | ||
const COUNTS_KEPT: usize = 4096; | ||
/// The most repositories [`Kept`] keeps for: once full, it starts again, | ||
/// as reading one again costs only that page a wait. | ||
const KEPT_MOST: usize = 4096; | ||
| ||
/// What reading a repository's PDS found, by two names for it (a public | ||
/// one's owner and key; a private one's space, and none), with how | ||
/// far it had moved then and when it was read: good until it moves on, | ||
/// or, unless `watching` says something tells of it moving, for `ttl`. | ||
pub(crate) struct Kept<T>(Mutex<HashMap<(String, String), Found<T>>>); | ||
| ||
/// Each public repository's storage as last counted, by its owner's DID | ||
/// and key, with the events of that repository seen then | ||
/// ([`App::stamp`]) and when it was counted: good until another such | ||
/// event, or, with no firehose watched, for as long as a mirror is. | ||
#[derive(Default)] | ||
pub(crate) struct Counts(Mutex<HashMap<(String, String), Counted>>); | ||
/// What was read, with how far its repository had moved, and when. | ||
type Found<T> = (u64, Instant, T); | ||
| ||
/// A repository's storage, with its events seen and the time it was counted. | ||
type Counted = (u64, Instant, Storage); | ||
/// Each repository's storage as last counted ([`get_repo_storage`]). | ||
pub(crate) type Counts = Kept<Storage>; | ||
| ||
impl Counts { | ||
/// What `did`'s repository `key` was counted at, if nothing of it has | ||
/// been seen since, `seen` being its events now, and it is not older | ||
/// than `ttl` when `watching` is unset. | ||
impl<T> Default for Kept<T> { | ||
fn default() -> Self { | ||
Kept(Mutex::default()) | ||
} | ||
} | ||
| ||
impl<T: Clone> Kept<T> { | ||
/// What was read of `first` and `second`, if it has not moved since, | ||
/// `seen` being how far it has now, and it is not older than `ttl` | ||
/// when `watching` is unset. | ||
fn get( | ||
&self, | ||
did: &str, | ||
key: &str, | ||
first: &str, | ||
second: &str, | ||
seen: u64, | ||
watching: bool, | ||
ttl: Duration, | ||
) -> Option<Storage> { | ||
) -> Option<T> { | ||
let kept = self.0.lock().unwrap(); | ||
let (then, at, storage) = kept.get(&(did.to_string(), key.to_string()))?; | ||
let (then, at, found) = kept.get(&(first.to_string(), second.to_string()))?; | ||
let fresh = *then == seen && (watching || at.elapsed() < ttl); | ||
fresh.then(|| storage.clone()) | ||
fresh.then(|| found.clone()) | ||
} | ||
| ||
fn put(&self, did: &str, key: &str, seen: u64, storage: Storage, most: usize) { | ||
fn put(&self, first: &str, second: &str, seen: u64, found: T, most: usize) { | ||
let mut kept = self.0.lock().unwrap(); | ||
if kept.len() >= most { | ||
kept.clear(); | ||
} | ||
kept.insert((did.into(), key.into()), (seen, Instant::now(), storage)); | ||
kept.insert((first.into(), second.into()), (seen, Instant::now(), found)); | ||
} | ||
| ||
/// Has `did`'s repository `key` counted again, after a write here | ||
/// that the firehose has yet to tell of. | ||
pub(crate) fn forget(&self, did: &str, key: &str) { | ||
self.0 | ||
.lock() | ||
.unwrap() | ||
.remove(&(did.to_string(), key.to_string())); | ||
/// Has all that was read under `first` read again, after a write here | ||
/// that nothing has told of yet. | ||
pub(crate) fn forget(&self, first: &str) { | ||
self.0.lock().unwrap().retain(|(f, _), _| f != first); | ||
} | ||
} | ||
| ||
/// What a repository's storage is kept under: a public one's owner and | ||
/// key, or a private one's space. | ||
fn counted_as(repo: &Repo) -> (String, String) { | ||
match &repo.in_space { | ||
Some(space) => (space.clone(), String::new()), | ||
None => (repo.did.clone(), repo.key.clone()), | ||
} | ||
} | ||
| ||
/// `dev.knotless.getRepoStorage`: counted from its pack records, which a | ||
/// sync no longer reads, so apart from its settings, and kept. | ||
/// sync no longer reads, so apart from its settings, and kept until the | ||
/// repository moves: a public one's as the firehose tells of it, a | ||
/// private one's as a sync of its space finds it. | ||
pub async fn get_repo_storage( | ||
State(app): State<Arc<App>>, | ||
Query(p): Query<SettingsParams>, | ||
) -> Result<Response> { | ||
let repo = repo::found(&app, &p.repo).await?; | ||
let url = url_of(&repo); | ||
// A private one is read as the member asking, and counted each time: | ||
// no firehose tells of its writes. | ||
// A private one is read as the member asking, whom finding it proved | ||
// one; a public one as this server. | ||
let private = crate::private::viewer().filter(|_| repo.in_space.is_some()); | ||
if let Some(viewer) = private { | ||
let reader = crate::login::ctx_for(&app, &viewer); | ||
let counted = knotless::storage::storage(&reader, &url).await; | ||
return Ok(answer(counted.map_err(AppError::upstream)?)); | ||
} | ||
// Its events taken before counting, so one seen meanwhile has it | ||
// counted again next time. | ||
let seen = app.stamp(&repo.did, Some(&repo.key)).1; | ||
let watching = app.watching.load(Ordering::Relaxed); | ||
let kept = app | ||
.counts | ||
.get(&repo.did, &repo.key, seen, watching, app.ttl); | ||
let reader = match &private { | ||
Some(viewer) => crate::login::ctx_for(&app, viewer), | ||
None => app.ctx.clone(), | ||
}; | ||
// How far it has moved, taken before counting, so a move meanwhile | ||
// has it counted again next time. | ||
let (first, second) = counted_as(&repo); | ||
let (seen, watching) = match &repo.in_space { | ||
Some(space) => (app.moves_of(space), app.space_followed(space)), | ||
None => ( | ||
app.stamp(&repo.did, Some(&repo.key)).1, | ||
app.watching.load(Ordering::Relaxed), | ||
), | ||
}; | ||
let kept = app.counts.get(&first, &second, seen, watching, app.ttl); | ||
if let Some(storage) = kept { | ||
return Ok(answer(storage)); | ||
} | ||
let counted = knotless::storage::storage(&app.ctx, &url).await; | ||
let counted = knotless::storage::storage(&reader, &url).await; | ||
let storage = counted.map_err(AppError::upstream)?; | ||
app.counts | ||
.put(&repo.did, &repo.key, seen, storage.clone(), COUNTS_KEPT); | ||
.put(&first, &second, seen, storage.clone(), KEPT_MOST); | ||
Ok(answer(storage)) | ||
} | ||
| ||
| @@ -394,7 +417,7 @@ pub async fn compact_repo( | ||
let (repo, ctx) = owned(&app, viewer, &headers, &input.repo, "compact it").await?; | ||
let compacted = knotless::compact::compact_repo(&ctx, &url_of(&repo)).await; | ||
let c = compacted.map_err(refused)?; | ||
app.counts.forget(&repo.did, &repo.key); | ||
app.counts.forget(&counted_as(&repo).0); | ||
wrote(&app, &repo.did, &[]).await?; | ||
Ok(answer(Compacted { | ||
packs: c.before, | ||
| @@ -532,7 +555,7 @@ mod tests { | ||
use super::*; | ||
| ||
#[test] | ||
fn counts_keep_until_the_repository_moves() { | ||
fn kept_until_the_repository_moves() { | ||
let counts = Counts::default(); | ||
let one = Storage { | ||
packs: 1, | ||
| @@ -542,15 +565,20 @@ mod tests { | ||
assert_eq!(counts.get("d", "k", 0, true, day), None); | ||
counts.put("d", "k", 3, one.clone(), 2); | ||
assert_eq!(counts.get("d", "k", 3, true, day), Some(one.clone())); | ||
// Another event of it, or none watched for longer than a mirror | ||
// is trusted, and it is counted again. | ||
// Moved on, or nothing watched for longer than `ttl`, and it is | ||
// read again. | ||
assert_eq!(counts.get("d", "k", 4, true, day), None); | ||
assert_eq!(counts.get("d", "k", 3, false, Duration::ZERO), None); | ||
assert_eq!(counts.get("d", "k", 3, false, day), Some(one.clone())); | ||
// Another repository's is its own. | ||
assert_eq!(counts.get("d", "j", 3, true, day), None); | ||
counts.forget("d", "k"); | ||
// Forgotten, all under its first name go. | ||
counts.put("d", "j", 3, one.clone(), 4); | ||
counts.put("e", "k", 3, one.clone(), 4); | ||
counts.forget("d"); | ||
assert_eq!(counts.get("d", "k", 3, true, day), None); | ||
assert_eq!(counts.get("d", "j", 3, true, day), None); | ||
assert_eq!(counts.get("e", "k", 3, true, day), Some(one.clone())); | ||
// Full, it starts again. | ||
counts.put("d", "a", 0, one.clone(), 2); | ||
counts.put("d", "b", 0, one.clone(), 2); | ||
crates/knotless-serve/src/lib.rs+10−8
| @@ -87,15 +87,16 @@ pub struct App { | ||
account_events: Mutex<HashMap<String, u64>>, | ||
/// And those that change one repository, by its owner's DID and key. | ||
repo_events: Mutex<HashMap<(String, String), u64>>, | ||
/// Each public repository's storage as last counted, kept until it | ||
/// moves (`dev.knotless.getRepoStorage`). | ||
/// Each repository's storage as last counted, kept until it moves | ||
/// (`dev.knotless.getRepoStorage`). | ||
pub(crate) counts: web::Counts, | ||
/// The last commit, by its rev, that wrote to each DID's repository | ||
/// collections ([`first_of`]). | ||
revs: Mutex<HashMap<String, String>>, | ||
pub watching: AtomicBool, | ||
/// Syncs of each space that found something written there, by URI. | ||
moves: Mutex<HashMap<String, u64>>, | ||
/// Syncs of each space that found something written there, by URI: | ||
/// shared with a sync run aside from the read that asked for it. | ||
moves: Arc<Mutex<HashMap<String, u64>>>, | ||
/// Set while the spaces kept are synced ([`spaces::follow`]): a | ||
/// private repository's mirror is then fresh until its space moves. | ||
pub following: AtomicBool, | ||
| @@ -128,10 +129,11 @@ pub struct App { | ||
/// Wakes [`spaces::follow`] for what is due. | ||
told: tokio::sync::Notify, | ||
/// When each space was last synced in full, as anyone here synced it: | ||
/// a read syncs it itself once that is [`spaces::STALE`] ago, as when | ||
/// a read syncs it itself, aside unless it never was, once that is | ||
/// [`spaces::STALE`] ago, as when | ||
/// the session of the member it is synced as no longer reads it. Its | ||
/// entries taken out, every space counts as not synced lately. | ||
pub last_synced: Mutex<HashMap<String, Instant>>, | ||
pub last_synced: Arc<Mutex<HashMap<String, Instant>>>, | ||
/// When this server last asked each space's authority to tell it of | ||
/// writes there. | ||
registered: tokio::sync::Mutex<HashMap<String, Instant>>, | ||
| @@ -246,7 +248,7 @@ impl App { | ||
counts: web::Counts::default(), | ||
revs: Mutex::default(), | ||
watching: AtomicBool::new(false), | ||
moves: Mutex::default(), | ||
moves: Default::default(), | ||
following: AtomicBool::new(false), | ||
feeds: Mutex::default(), | ||
stale: None, | ||
| @@ -259,7 +261,7 @@ impl App { | ||
with_spaces: Mutex::default(), | ||
due: Mutex::default(), | ||
told: tokio::sync::Notify::new(), | ||
last_synced: Mutex::default(), | ||
last_synced: Default::default(), | ||
registered: tokio::sync::Mutex::default(), | ||
code: None, | ||
usage: None, | ||
crates/knotless-serve/src/private.rs+26−1
| @@ -210,7 +210,7 @@ impl App { | ||
let events = self.stamp(did, entry_key.as_deref()); | ||
// A space no one here has a session for is synced only as it is | ||
// read, with what proved its reader a member. | ||
let followed = self.following.load(Ordering::Relaxed) && self.syncer_of(&space).is_some(); | ||
let followed = self.space_followed(&space); | ||
let fresh = match (&entry.git, entry.synced) { | ||
// Nothing is written to a space but its sync finds, within | ||
// seconds, or the pages touch. | ||
| @@ -321,6 +321,13 @@ impl App { | ||
}; | ||
} | ||
| ||
/// Whether the space `uri` is synced as its authority tells of writes | ||
/// there, so that it is current until a sync of it finds one | ||
/// ([`App::moves_of`]). | ||
pub(crate) fn space_followed(&self, uri: &str) -> bool { | ||
self.following.load(Ordering::Relaxed) && self.syncer_of(uri).is_some() | ||
} | ||
| ||
/// The member whose session here syncs the space `uri`, if any. | ||
fn syncer_of(&self, uri: &str) -> Option<String> { | ||
let index = self.spaces.as_ref()?; | ||
| @@ -347,6 +354,24 @@ impl App { | ||
return Ok(()); | ||
} | ||
let ctx = crate::login::syncer_ctx(self, syncer); | ||
// Synced before, it is read as it was while it syncs aside, as a | ||
// sync reads the whole space and can take seconds: only one never | ||
// synced has nothing to read until its sync is done. Taken as | ||
// synced now, so reads meanwhile start no other. | ||
if synced { | ||
self.synced_now(space); | ||
let (moves, last) = (self.moves.clone(), self.last_synced.clone()); | ||
let (index, space) = (index.clone(), space.to_string()); | ||
tokio::spawn(async move { | ||
let read = crate::spaces::sync(&ctx, &index, &space).await; | ||
last.lock().unwrap().insert(space.clone(), Instant::now()); | ||
if matches!(read, Ok(true)) { | ||
*moves.lock().unwrap().entry(space.clone()).or_default() += 1; | ||
} | ||
crate::telemetry::space_synced(&space, read.map(drop)); | ||
}); | ||
return Ok(()); | ||
} | ||
let read = crate::spaces::sync(&ctx, index, space).await; | ||
self.read_now(space, read, synced) | ||
} | ||
crates/knotless-serve/tests/gateway.rs+44−3
| @@ -3562,6 +3562,37 @@ fn private_repositories_synced_by_readers_once_their_syncer_lapses() { | ||
let syncers = server.app.syncers(); | ||
assert!(syncers.iter().any(|(_, d)| *d == did), "{syncers:?}"); | ||
| ||
// Once it was not synced lately, the owner's read is answered from the | ||
// index as it was, and the space syncs aside, taking in what was | ||
// written since. | ||
run(&member, &["issue", &url, "Written while synced"]); | ||
server.app.last_synced.lock().unwrap().clear(); | ||
let owners = || { | ||
let res = client | ||
.get(format!( | ||
"{}/xrpc/dev.knotless.listItems?repo={at}&kind=issue", | ||
server.base | ||
)) | ||
.header("cookie", &cookie) | ||
.send() | ||
.unwrap(); | ||
assert_eq!(res.status().as_u16(), 200); | ||
res.json::<Value>().unwrap()["items"].to_string() | ||
}; | ||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); | ||
while !owners().contains("Written while synced") { | ||
assert!(std::time::Instant::now() < deadline, "never synced aside"); | ||
std::thread::sleep(std::time::Duration::from_millis(100)); | ||
} | ||
assert!( | ||
server | ||
.app | ||
.last_synced | ||
.lock() | ||
.unwrap() | ||
.contains_key(&knotless::space::uri(&did, "ledger")) | ||
); | ||
| ||
// Their session here lapses, and they stay its syncer: each sync as | ||
// them fails, though not as one refused, which would let it go. | ||
let pages = knotless_serve::login::ctx_for(&server.app, &did); | ||
| @@ -3599,7 +3630,10 @@ fn private_repositories_synced_by_readers_once_their_syncer_lapses() { | ||
| ||
// Once it was not synced lately, a member's read syncs it as them. | ||
stale(); | ||
assert_eq!(listed(), ["Written after the lapse"]); | ||
assert_eq!( | ||
listed(), | ||
["Written after the lapse", "Written while synced"] | ||
); | ||
| ||
// A member whose repo cannot be read fails the sync: one who writes, | ||
// then is taken down where they live. The read is answered from the | ||
| @@ -3620,7 +3654,10 @@ fn private_repositories_synced_by_readers_once_their_syncer_lapses() { | ||
}; | ||
takedown(true); | ||
stale(); | ||
assert_eq!(listed(), ["Written after the lapse"]); | ||
assert_eq!( | ||
listed(), | ||
["Written after the lapse", "Written while synced"] | ||
); | ||
assert!(server.app.last_synced.lock().unwrap().contains_key(&space)); | ||
| ||
// Restored, it is taken in at the next read that finds it stale. | ||
| @@ -3628,7 +3665,11 @@ fn private_repositories_synced_by_readers_once_their_syncer_lapses() { | ||
stale(); | ||
assert_eq!( | ||
listed(), | ||
["Written after the lapse", "Written before the takedown"] | ||
[ | ||
"Written after the lapse", | ||
"Written before the takedown", | ||
"Written while synced" | ||
] | ||
); | ||
} | ||
| ||
crates/knotless-serve/tests/pages.rs+35−0
| @@ -869,6 +869,32 @@ fn private_repositories_settings_and_forks() { | ||
assert_eq!((status, &storage["packs"]), (200, &json!(1)), "{storage}"); | ||
assert_eq!(browse("GET", &counted, None, None).0, 404); | ||
assert_eq!(settings(None).0, 404); | ||
// Who reads it but does not push, to its owner, as its record says: | ||
// the member it was shared with, for reading, and one shared so from | ||
// the pages, at once. A member is shown none. | ||
let read = |set: &Value| -> Vec<String> { | ||
let readers = set["readers"].as_array().unwrap().iter(); | ||
readers | ||
.map(|r| r["did"].as_str().unwrap().to_string()) | ||
.collect() | ||
}; | ||
assert_eq!( | ||
read(&set), | ||
std::slice::from_ref(&member.account.did), | ||
"{set}" | ||
); | ||
let reader = person(account()); | ||
let sharing = json!({"repo": at, "with": reader.account.handle, "write": false}); | ||
let (status, shared) = server.call("shareRepo", me, sharing); | ||
assert_eq!(status, 200, "{shared}"); | ||
let mut both = read(&settings(me).1); | ||
both.sort(); | ||
let mut want = [member.account.did.clone(), reader.account.did.clone()]; | ||
want.sort(); | ||
assert_eq!(both, want); | ||
let theirs = log_in(&server, &member.account); | ||
let (status, set) = settings(Some(theirs.as_str())); | ||
assert_eq!((status, &set["readers"]), (200, &json!([])), "{set}"); | ||
// Changed from the pages, they say so at once: read from this server's | ||
// index of its space, which the change is put in as it is made. | ||
let described = json!({"repo": at, "description": "Kept quiet"}); | ||
| @@ -1097,6 +1123,15 @@ fn an_account_deletes_its_private_repositories() { | ||
{"name": "secret", "private": true, "collaborators": 1, "members": 1}, | ||
]); | ||
assert_eq!(held["repos"], repos); | ||
// Its runner reads it, so its settings name it among those who do; the | ||
// member pushes, so is not. | ||
let at = format!("at://{}/dev.knotless.repo/secret", owner.account.did); | ||
let asked = format!("{}?repo={at}", server.xrpc("getRepoSettings")); | ||
let (status, _, set) = browse("GET", &asked, Some(&cookie), None); | ||
assert_eq!(status, 200, "{set}"); | ||
let readers = set["readers"].as_array().unwrap(); | ||
let dids: Vec<&Value> = readers.iter().map(|r| &r["did"]).collect(); | ||
assert_eq!(dids, [&json!(runner.account.did)], "{set}"); | ||
| ||
let did = owner.account.did.clone(); | ||
let (answers, _) = server.delete_data(&cookie, &did, 1); | ||
docs/design/access.md+5−2
| @@ -294,8 +294,11 @@ repository on the pages is a repository in that index. | ||
index, so syncing goes on across restarts. | ||
- A space not synced within a minute, two of the sweeps that sync every | ||
space every 30 seconds, or not since the server started, is synced by | ||
whoever reads it next, with their own session or delegation token, | ||
before the page is answered. Its syncer's sync can fail without its | ||
whoever reads it next, with their own session or delegation token. A | ||
read with a session is answered from the index as it was while the | ||
sync runs aside, as a sync reads the whole space and can take seconds; | ||
one with a delegation token waits for it. Only a space never synced | ||
makes every reader wait. Its syncer's sync can fail without its | ||
being refused, as when their session here lapses or its grant leaves | ||
the space out; until they logged in again, every write since would | ||
otherwise be missing from it, for every reader. A reader's sync that | ||
docs/design/serve.md+5−2
| @@ -299,8 +299,11 @@ Details: | ||
is kept in memory by repository, up to 4,096 of them, until the firehose | ||
tells of a write to that repository's packs, claims or refs, or the pages | ||
compact it; with no firehose watched, for as long as a mirror is trusted. | ||
A private repository's is counted each time, as no firehose tells of its | ||
writes. | ||
A private repository's is kept by its space until a sync of the space | ||
finds a write there, as its mirror is ([access](access.md)). Who reads a | ||
private repository, which its owner's settings list, is read from its | ||
record, whose `readers` and `runners` sharing it writes, rather than | ||
asked of its space: the client shows the appview each such write. | ||
- A repository has three Atom feeds, for feed readers: `commits.atom`, the | ||
newest 30 commits on its default branch; `tags.atom`, its newest 30 tags | ||
that name a commit; and `releases.atom`, its newest 30 releases, with | ||
docs/reference/api.md+1−1
| @@ -600,7 +600,7 @@ name it has now. | ||
| `dev.knotless.getEmails` | `{count}`, how many addresses the asker's `dev.knotless.emails` claims: only hashes are kept, so they cannot be listed. 401 with no asker | | ||
| `dev.knotless.listBlocks` | the asker's blocks, the latest first, from its own repo: each `uri`, `subject` (an actor), `repo` when it is from one repository only, and `createdAt`. Those naming a private repository live in its space and are not listed. 401 with no asker | | ||
| `dev.knotless.getRepoSettings?repo=` | what the owner sets: description, links, default branch, protected branches, labels, collaborators, triagers, gateway, an offer to move it (`transfer`), the gateway the owner delegated to from these pages (`delegated`), for a private one, to its owner alone, the members who read but do not push (`readers`), and the accounts its rules name as [gates](../design/collaboration.md#gates), each once (`gates`) | | ||
| `dev.knotless.getRepoStorage?repo=` | what it takes up on its owner's PDS, as `knotless storage` counts it: kept per repository, and counted again only once a write to its packs, claims or refs is seen, or the pages compact it | | ||
| `dev.knotless.getRepoStorage?repo=` | what it takes up on its owner's PDS, as `knotless storage` counts it: kept per repository, and counted again only once a write to its packs, claims or refs is seen (for a private one, once a sync of its space finds a write), or the pages compact it | | ||
| `dev.knotless.getOrg?actor=` | an organization's members, as the index has its `dev.knotless.org` and `dev.knotless.orgMember` records and its members' `dev.knotless.orgAcceptance` ones: `{org, organization, inside, gateway?, members}`, `gateway` the one its public `dev.knotless.org` record names, each member `{actor, role, standing, invitedAt, expiresAt?, joinedAt?}`, the first invited first, `standing` one of `invited`, `lapsed`, `public` and `hidden`. To anyone, its members in public; to the organization and its members, logged in or with a service token (`inside`), its invitations and hidden members too, from its space, which this server keeps an index of; to an account it invited, its own entry. `organization: false`, with no members, for an account that is none; `ProfileNotFound` for an unknown account | | ||
| `dev.knotless.getActorStars?actor=` | the public repositories an account starred, newest first, each with its `language`, `starCount` and `forkCount`: 404 with no index, `ProfileNotFound` for an unknown account | | ||
| `dev.knotless.getMergeStatus?repo=&pull=` | whether an open pull can merge now, as `knotless merge --check` says ([below](#landing-and-checks)): 401 with no asker, 400 for a pull not open, 404 for none | | ||
harness/quarantine.json+1−0
| @@ -1,6 +1,7 @@ | ||
[ | ||
{"net": "spaces", "binary": "gateway (test)", "test": "private_repositories_on_the_pages", "until": "2026-11-04"}, | ||
{"net": "spaces", "binary": "gateway (test)", "test": "private_repositories_indexed_here", "until": "2026-11-04"}, | ||
{"net": "spaces", "binary": "mcp (test)", "test": "agents_fork_review_and_merge", "until": "2026-11-06"}, | ||
{"net": "spaces", "binary": "web (test)", "test": "searching_code", "until": "2026-11-10"}, | ||
{"net": "stock", "binary": "web (test)", "test": "searching_code", "until": "2026-11-10"} | ||
] | ||
lexicons/dev/knotless/getRepoStorage.json+1−1
| @@ -5,7 +5,7 @@ | ||
"defs": { | ||
"main": { | ||
"type": "query", | ||
"description": "What a repository's packs take up on its owner's PDS, as `knotless storage` counts them: counted again only once a write to the repository's packs, claims or refs is seen, or compaction from these pages folds them. Apart from dev.knotless.getRepoSettings, as counting reads every pack record from the PDS.", | ||
"description": "What a repository's packs take up on its owner's PDS, as `knotless storage` counts them: counted again only once a write to the repository's packs, claims or refs is seen (for a private one, once a sync of its space finds a write there), or compaction from these pages folds them. Apart from dev.knotless.getRepoSettings, as counting reads every pack record from the PDS.", | ||
"parameters": { | ||
"type": "params", | ||
"required": ["repo"], | ||