~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A private repository's records are named in full, and public-form references are moved

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:space/migrate
.changeset/space-record-uris-full.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: minor
---
# A private repository's pulls, issues, comments and checks are named by their URI in its space
knotless names a record in a private repository's space as atproto's permissioned data proposal does, `at://<owner>/space/dev.knotless.space/<name>/<author>/<collection>/<rkey>`, wherever it gives one: what `knotless pull`, `issue`, `comment` and `checks` print, what `show` and `list` name, and what new records written into the space refer to. A URI in the public form, `at://<author>/<collection>/<rkey>`, is still taken for one in the repository being asked about, and records written before are read as they were. A knotless older than the one that reads both forms does not find a record named in full: upgrade with `knotless upgrade`.
.changeset/space-short-form-moved.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: patch
---
# Records in a private repository that name another in the public form are written again in full
The first time knotless writes into a private repository's space for you, it rewrites the records you wrote there whose `subject`, `pull`, `reply`, `check` or pull head names another in the public form, `at://<author>/<collection>/<rkey>`, to the full form, through your own login, once. What you typed in a body is left as you wrote it.
crates/knotless/src/checks.rs+6−2
@@ -4,6 +4,7 @@
//! private repository. A runner's checks of one repository share a key
//! prefix, so each takes one scan, and a pull's checks a narrower one.
use crate::aturi::{self, AtUri};
use crate::env::Ctx;
use crate::identity;
use crate::keys::{check_prefix, checks_of, queue_name};
@@ -177,6 +178,7 @@ pub async fn by(
let mut repos = HashMap::new();
for read in futures::future::join_all(reads).await {
let (runner, theirs, found) = read?;
let space = theirs.space_ref();
repos.insert(runner.clone(), theirs);
for stored in found {
let Ok(record) = serde_json::from_value::<CheckRecord>(stored.value) else {
@@ -186,13 +188,15 @@ pub async fn by(
|| filter.commit.is_some_and(|c| !record.commit.starts_with(c))
|| filter
.pull
.is_some_and(|p| Some(p) != record.pull.as_deref())
.is_some_and(|p| !record.pull.as_deref().is_some_and(|r| aturi::same(p, r)))
|| filter.workflow.is_some_and(|w| w != record.workflow)
{
continue;
}
checks.push(Check {
uri: format!("at://{runner}/{}/{}", nsid::CHECK, stored.rkey),
uri: AtUri::public(&runner, nsid::CHECK, &stored.rkey)
.resolved(space.as_ref())
.to_string(),
runner: runner.clone(),
record,
log_text: None,
crates/knotless/src/forge.rs+43−8
@@ -746,11 +746,23 @@ impl Target {
}
}
/// The repository as forge records name it, under the name it has now.
/// The repository as forge records name it, under the name it has now:
/// a private one's record by its URI in the space.
pub fn reference(&self) -> RepoRef {
self.snap
let mut reference = self
.snap
.reference(self.remote.did())
.expect("an opened repository has a record")
.expect("an opened repository has a record");
reference.uri = self.named(&reference.uri);
reference
}
/// `uri`, a record of this repository's, typed or given in either
/// form, as its records are named here: in full, in the space, for a
/// private repository's. One in some other space is as it was, and
/// names nothing here.
pub fn named(&self, uri: &str) -> String {
crate::aturi::resolve(uri, self.remote.space_ref().as_ref())
}
fn is_about(&self, value: &Value) -> bool {
@@ -939,7 +951,7 @@ impl Target {
for r in member.list_all::<Value>(collection).await? {
if self.is_about(&r.value) {
entries.push(Entry {
uri: format!("at://{did}/{collection}/{}", r.rkey),
uri: member.uri_of(collection, &r.rkey),
author: did.clone(),
value: r.value,
});
@@ -949,7 +961,7 @@ impl Target {
// A reaction names what it is about, not the repository.
for r in member.list_all::<Value>(nsid::REACTION).await? {
reactions.push(Entry {
uri: format!("at://{did}/{}/{}", nsid::REACTION, r.rkey),
uri: member.uri_of(nsid::REACTION, &r.rkey),
author: did.clone(),
value: r.value,
});
@@ -976,6 +988,7 @@ impl Target {
.as_ref()
.map(|s| Space::owner(ctx, &s.uri, &me)),
};
crate::short_form::move_once(ctx, &mine, &me.auth()).await;
Ok((mine, me))
}
@@ -987,7 +1000,7 @@ impl Target {
mine.apply(vec![remote::create(collection, &rkey, value)], &me.auth())
.await?;
self.shown(ctx).await;
Ok(format!("at://{}/{collection}/{rkey}", me.did))
Ok(mine.uri_of(collection, &rkey))
}
/// Fails when the repository is archived, and so takes nothing more
@@ -1066,7 +1079,7 @@ impl Target {
mine.apply(vec![write(nsid::LABEL, &rkey, value.clone())], &me.auth())
.await?;
Ok(Entry {
uri: format!("at://{}/{}/{rkey}", me.did, nsid::LABEL),
uri: mine.uri_of(nsid::LABEL, &rkey),
author: me.did.clone(),
value,
})
@@ -1117,6 +1130,8 @@ pub async fn hide(ctx: &Ctx, target: &RepoUrl, subject: &str, hidden: bool) -> R
bail!("{subject} is not a pull, an issue or a comment");
}
let t = Target::open(ctx, target).await?;
let subject = t.named(subject);
let subject = subject.as_str();
let (mine, me) = t.mine(ctx).await?;
if me.did != t.remote.did() {
bail!("only {}'s owner can hide what is said about it", t.url);
@@ -1566,6 +1581,8 @@ pub async fn comment(
}
let t = Target::open(ctx, target).await?;
t.writable()?;
let subject = t.named(subject);
let subject = subject.as_str();
let said = |subject: &str, body: &str, images: &[ImageAttachment]| CommentRecord {
repo: t.reference(),
subject: subject.to_string(),
@@ -1617,7 +1634,7 @@ pub async fn comment(
}
};
let (mine, me) = t.mine(ctx).await?;
let uri = |rkey: &str| format!("at://{}/{}/{rkey}", me.did, nsid::COMMENT);
let uri = |rkey: &str| mine.uri_of(nsid::COMMENT, rkey);
let (mut writes, mut wrote) = (Vec::new(), Vec::new());
let mut review = None;
if lines.is_empty() || verdict.is_some() || !body.trim().is_empty() {
@@ -1662,6 +1679,8 @@ pub async fn comment(
/// thread's author, the pull's, the owner or a collaborator.
pub async fn resolve(ctx: &Ctx, target: &RepoUrl, thread: &str, resolved: bool) -> Result<String> {
let t = Target::open(ctx, target).await?;
let thread = t.named(thread);
let thread = thread.as_str();
let value = match parse_uri(thread)?.1 {
nsid::COMMENT => t.record(ctx, thread).await?,
_ => None,
@@ -1722,6 +1741,8 @@ pub fn resolved(
async fn own(ctx: &Ctx, target: &RepoUrl, subject: &str) -> Result<(Remote, Session, Value)> {
let (author, _, _) = parse_uri(subject)?;
let t = Target::open(ctx, target).await?;
let subject = t.named(subject);
let subject = subject.as_str();
t.writable()?;
let (mine, me) = t.mine(ctx).await?;
if author != me.did {
@@ -1866,6 +1887,8 @@ pub async fn label(
bail!("name a label to put on or take off");
}
let t = Target::open(ctx, target).await?;
let subject = t.named(subject);
let subject = subject.as_str();
let mut defined = Vec::new();
for name in names {
let Some(label) = t.record.label(name) else {
@@ -1921,6 +1944,8 @@ pub async fn set_state(ctx: &Ctx, target: &RepoUrl, subject: &str, state: &str)
bail!("a pull or issue is open or closed; merging takes `merge`");
}
let t = Target::open(ctx, target).await?;
let subject = t.named(subject);
let subject = subject.as_str();
let record = StateRecord {
repo: t.reference(),
subject: subject.to_string(),
@@ -2007,6 +2032,8 @@ pub async fn merge(
gate: Gate,
) -> Result<Merged> {
let t = Target::open(ctx, target).await?;
let pull = t.named(pull);
let pull = pull.as_str();
// The pull as its author's repo has it now, not as the index last saw
// it, and the newest reviews: the index may not have them yet.
let me = session::me(ctx).await?;
@@ -2372,6 +2399,8 @@ pub struct Comparison {
/// to the merge, as its state records them: its branch may be gone.
pub async fn compare(ctx: &Ctx, target: &RepoUrl, pull: &str) -> Result<Comparison> {
let t = Target::open(ctx, target).await?;
let pull = t.named(pull);
let pull = pull.as_str();
let (item, _) = t.item(ctx, pull).await?;
if let Some((git, from, merged)) = merged_sides(ctx, &t, &item).await? {
let range = format!("{from}..{merged}");
@@ -2422,6 +2451,8 @@ async fn merged_sides(ctx: &Ctx, t: &Target, item: &Item) -> Result<Option<(Git,
/// merged pull's is what its merge brought in, as `compare` has it.
pub async fn pull_diff(ctx: &Ctx, target: &RepoUrl, pull: &str) -> Result<String> {
let t = Target::open(ctx, target).await?;
let pull = t.named(pull);
let pull = pull.as_str();
let (item, _) = t.item(ctx, pull).await?;
if let Some((git, from, merged)) = merged_sides(ctx, &t, &item).await? {
return crate::read::diff(&git, &from, &merged).await;
@@ -2574,6 +2605,8 @@ pub async fn retarget(
head: Option<&str>,
) -> Result<()> {
let t = Target::open(ctx, target).await?;
let pull = t.named(pull);
let pull = pull.as_str();
t.writable()?;
let (item, value) = t.item(ctx, pull).await?;
if item.kind != "pull" {
@@ -2889,6 +2922,8 @@ async fn taken_down(ctx: &Ctx, subject: &str, author: &str) -> bool {
/// who wrote any of it.
pub async fn show(ctx: &Ctx, target: &RepoUrl, subject: &str) -> Result<Thread> {
let t = Target::open(ctx, target).await?;
let subject = t.named(subject);
let subject = subject.as_str();
let mut entries = t.entries(ctx).await?;
// One the appview's index lacks is kept out, or too new for it yet:
// the newest are read from their authors' repos, unless the appview's
crates/knotless/src/landing.rs+2−0
@@ -128,6 +128,8 @@ pub fn may(t: Facts, item: &Item, who: &str) -> May {
/// nothing.
pub async fn mergeable(ctx: &Ctx, target: &RepoUrl, pull: &str, who: &str) -> Result<Mergeable> {
let t = Target::open(ctx, target).await?;
let pull = t.named(pull);
let pull = pull.as_str();
let entries = t.reviewed(ctx, pull, who).await?;
let items = forge::assemble(&t.record, t.remote.did(), &entries);
let item = forge::thread_in(&t.record, t.remote.did(), &entries, &items, pull)?.item;
crates/knotless/src/lib.rs+1−0
@@ -57,6 +57,7 @@ pub mod reviews;
pub mod sealed;
pub mod secrets;
pub mod session;
pub mod short_form;
pub mod social;
pub mod space;
pub mod space_sync;
crates/knotless/src/manage.rs+3−3
@@ -754,7 +754,7 @@ pub async fn rerun(
let record = records::RerunRecord {
repo: RepoRef::new(remote.did(), &repo.rkey, &repo.value.key),
ref_name: refname,
pull: pull.map(str::to_string),
pull: pull.map(|p| crate::aturi::resolve(p, remote.space_ref().as_ref())),
workflow: workflow.map(str::to_string),
env,
created_at: records::now(),
@@ -762,7 +762,7 @@ pub async fn rerun(
let rkey = format!("{}.{}", repo.value.key, keys::tid());
let write = remote::create(nsid::RERUN, &rkey, records::typed(nsid::RERUN, &record));
remote.apply(vec![write], &auth.auth()).await?;
Ok(format!("at://{}/{}/{rkey}", remote.did(), nsid::RERUN))
Ok(remote.uri_of(nsid::RERUN, &rkey))
}
/// The ref `name` names in `snap`: itself, else the branch or the tag of
@@ -1238,7 +1238,7 @@ async fn owned(remote: &Remote, key: &str) -> Result<Vec<serde_json::Value>> {
for (collection, field) in fields.chain([(nsid::STAR, "subject")]) {
for r in remote.list_all::<serde_json::Value>(collection).await? {
if about(&r.value, field) {
gone.insert(format!("at://{did}/{collection}/{}", r.rkey));
gone.insert(remote.uri_of(collection, &r.rkey));
writes.push(remote::delete(collection, &r.rkey));
}
}
crates/knotless/src/queue.rs+7−1
@@ -123,6 +123,8 @@ async fn open_pull(ctx: &Ctx, t: &Target, pull: &str) -> Result<(Vec<Entry>, Ite
/// One already queued keeps its place. The owner and collaborators may.
pub async fn enqueue(ctx: &Ctx, url: &RepoUrl, pull: &str) -> Result<Place> {
let t = Target::open(ctx, url).await?;
let pull = t.named(pull);
let pull = pull.as_str();
let me = maintainer(ctx, &t).await?;
if t.record.runners.is_empty() {
bail!(
@@ -230,6 +232,8 @@ fn short(sha: &str) -> &str {
/// Takes a pull out of the merge queue. The owner and collaborators may.
pub async fn leave(ctx: &Ctx, url: &RepoUrl, pull: &str) -> Result<()> {
let t = Target::open(ctx, url).await?;
let pull = t.named(pull);
let pull = pull.as_str();
maintainer(ctx, &t).await?;
let (_, item) = open_pull(ctx, &t, pull).await?;
if item.queue.as_ref().is_none_or(|q| q.state != QUEUED) {
@@ -595,7 +599,9 @@ pub async fn list(ctx: &Ctx, url: &RepoUrl) -> Result<Vec<Waiting>> {
/// it is queued or was ejected and is open.
pub async fn standing(ctx: &Ctx, url: &RepoUrl, pull: &str) -> Result<Option<Waiting>> {
let all = list(ctx, url).await?;
Ok(all.into_iter().find(|w| w.item.uri == pull))
Ok(all
.into_iter()
.find(|w| crate::aturi::same(&w.item.uri, pull)))
}
/// A pull squashed onto the one before it in a batch: the head it brought,
crates/knotless/src/remote.rs+45−4
@@ -1,6 +1,7 @@
//! One repository's records on its owner's PDS: in their public repo, or,
//! for a private repository, in a space.
use crate::aturi::{AtUri, SpaceRef};
use crate::env::Ctx;
use crate::identity::{self, Identity};
use crate::keys;
@@ -244,6 +245,28 @@ impl Remote {
&self.identity.did
}
/// The space a private repository's records are in.
pub fn space_ref(&self) -> Option<SpaceRef> {
let space = self.space.as_ref()?;
SpaceRef::parse(&space.uri).ok()
}
/// The URI of this repo's record `rkey` in `collection`: in full, in
/// the space, for a private repository's.
pub fn uri_of(&self, collection: &str, rkey: &str) -> String {
let at = AtUri::public(self.did(), collection, rkey);
at.resolved(self.space_ref().as_ref()).to_string()
}
/// `raw`, read from here: in a space, with the references it holds in
/// the public form given the space.
fn parse<T: DeserializeOwned>(&self, mut raw: RawRecord) -> Result<Stored<T>> {
if let Some(space) = self.space_ref() {
crate::short_form::resolve_refs(&mut raw.value, &space);
}
parse(raw)
}
/// The space this one points to, for a space named for a later name of
/// a private repository.
pub async fn home(&self) -> Result<Option<String>> {
@@ -315,7 +338,7 @@ impl Remote {
.read::<RawRecord>("getRecord", &[("collection", collection), ("rkey", rkey)])
.await;
match res {
Ok(raw) => parse(raw).map(Some),
Ok(raw) => self.parse(raw).map(Some),
Err(err) if XrpcError::of(&err).is_some_and(|e| e.error == "RecordNotFound") => {
Ok(None)
}
@@ -441,7 +464,7 @@ impl Remote {
return Ok(page);
}
page.next = Some(rkey.to_string());
page.items.push(parse(raw)?);
page.items.push(self.parse(raw)?);
}
if !full {
page.next = None;
@@ -672,11 +695,25 @@ impl Remote {
let limit = limit.to_string();
let params = [("collection", collection), ("limit", limit.as_str())];
let out: ListOut = self.read("listRecords", &params).await?;
out.records.into_iter().map(parse).collect()
out.records.into_iter().map(|r| self.parse(r)).collect()
}
/// Every record in `collection`, from the public repo or the space.
pub async fn list_all<T: DeserializeOwned>(&self, collection: &str) -> Result<Vec<Stored<T>>> {
self.list_with(collection, true).await
}
/// [`Remote::list_all`], each record as it was written, its references
/// in the public form left so.
pub async fn list_as_written(&self, collection: &str) -> Result<Vec<Stored<Value>>> {
self.list_with(collection, false).await
}
async fn list_with<T: DeserializeOwned>(
&self,
collection: &str,
resolve: bool,
) -> Result<Vec<Stored<T>>> {
let mut all = Vec::new();
let mut cursor: Option<String> = None;
let limit = PAGE.to_string();
@@ -688,7 +725,11 @@ impl Remote {
let out: ListOut = self.read("listRecords", &params).await?;
let full = out.records.len() == PAGE;
for raw in out.records {
all.push(parse(raw)?);
all.push(if resolve {
self.parse(raw)?
} else {
parse(raw)?
});
}
match out.cursor {
Some(next) if full => cursor = Some(next),
crates/knotless/src/revert.rs+2−0
@@ -41,6 +41,8 @@ pub async fn revert(
reason: Option<&str>,
) -> Result<Reverted> {
let t = Target::open(ctx, target).await?;
let pull = t.named(pull);
let pull = pull.as_str();
let me = session::me(ctx).await?;
if me.did != t.remote.did() && !t.record.is_collaborator(&me.did) {
bail!("only the owner of {target} and its collaborators revert a pull");
crates/knotless/src/short_form.rsnew+252−0
crates/knotless/src/social.rs+2−0
crates/knotless/tests/library.rs+96−0
crates/knotless-run/src/check.rs+14−2
crates/knotless-run/src/lib.rs+1−1
crates/knotless-run/src/queue.rs+5−1
crates/knotless-serve/src/index/short_form.rsnew+329−0
crates/knotless-serve/src/web/actor.rs+18−15
crates/knotless-serve/src/web/reactions.rs+2−1
crates/knotless-serve/src/web/repos.rs+1−1
crates/knotless-serve/src/web/threads.rs+2−1
crates/knotless-serve/src/index.rs+127−19
crates/knotless-serve/src/web.rs+21−6
crates/knotless-serve/tests/gateway.rs+8−4
docs/design/access.md+36−3
harness/answers.mjs+17−1
web/src/lib/format.ts+16−5