//! Making repositories from the pages: `dev.knotless.createRepo` and //! `forkRepo`, as the account logged in to them, through the same code as //! `knotless repo create` and `knotless repo fork`; and what every write from the //! pages to a repository shares. use super::stars::logged_in; use super::*; use crate::viewer::Viewer; use axum::Extension; use knotless::manage::{self, NewRepo}; /// A write the crate refused, in its words: what was asked could not be /// done, and what to do about it on the pages. pub(super) fn refused(e: anyhow::Error) -> Shown { Shown(StatusCode::BAD_REQUEST, crate::shown(&e)) } /// What a write from the pages did to `did`'s records, taken in as an /// event from its PDS would be, but at once: each `(action, collection, /// rkey)`, a record created, updated or deleted, indexed as its PDS now /// has it, so the page after shows it; and the account's mirrors /// refreshed. pub(super) async fn wrote(app: &Arc, did: &str, ops: &[(&str, &str, &str)]) -> Result<()> { app.touch(did); for index in app.index.iter() { for (action, collection, rkey) in ops { let op = crate::firehose::Op { action: action.to_string(), collection: collection.to_string(), rkey: rkey.to_string(), record: None, }; let indexed = crate::index_op(&app.ctx, index, did, &op, None).await; indexed.map_err(AppError::internal)?; } } Ok(()) } /// [`wrote`], of `repo`'s own records: none of them, for a private one, /// which no public index holds, and its PDS shows no one. pub(super) async fn wrote_of( app: &Arc, repo: &Repo, ops: &[(&str, &str, &str)], ) -> Result<()> { let public = if repo.in_space.is_some() { &[][..] } else { ops }; wrote(app, &repo.did, public).await } /// [`wrote_of`], for a change to `repo`'s record: for a private one, into /// this server's index of its space, as the viewer reads it there now. pub(super) async fn rewrote(app: &Arc, repo: &Repo) -> Result<()> { wrote_of(app, repo, &[("update", nsid::REPO, repo.name.as_str())]).await?; let uri = repo.uri(); let private = repo .in_space .is_some() .then_some(super::threads::reindex(app, repo, &uri)); futures::future::OptionFuture::from(private) .await .transpose()?; Ok(()) } /// `dev.knotless.defs#repoMade`. #[derive(Serialize)] pub struct RepoMade { uri: String, name: String, } impl RepoMade { pub(super) fn new(did: &str, name: &str) -> RepoMade { RepoMade { uri: forge::repo_uri(did, name), name: name.to_string(), } } } /// `did`'s repository `name`, as a URL, or why that is no name. fn named(did: &str, name: &str) -> Result { if !knotless::keys::valid_name(name) { let why = format!( "{name:?} is no repository's name: up to 100 letters, digits, `.`, `-`, `_` and `~`" ); return Err(repo::invalid(why)); } RepoUrl::parse(&format!("knotless://{did}/{name}")).map_err(refused) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateInput { name: String, description: Option, default_branch: Option, #[serde(default)] private: bool, } /// `dev.knotless.createRepo`. pub async fn create_repo( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { let did = logged_in(&app, viewer, &headers, "create a repository")?; let url = named(&did, input.name.trim())?; let given = |s: Option| s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); let new = NewRepo { private: input.private, description: given(input.description), default_branch: given(input.default_branch), ..Default::default() }; let ctx = crate::login::ctx_for(&app, &did); manage::create(&ctx, &url, &new).await.map_err(refused)?; // A private one is in a space of its own, which no public index holds, // and in this server's own as it answers. let made = [("create", nsid::REPO, url.name.as_str())]; let public = if new.private { &[][..] } else { &made[..] }; wrote(&app, &did, public).await?; if new.private { let space = knotless::space::uri(&did, &url.name); app.keep(&space, &did).await?; } Ok(answer(RepoMade::new(&did, &url.name))) } #[derive(Deserialize)] pub struct ForkInput { repo: String, name: Option, } /// `dev.knotless.forkRepo`. pub async fn fork_repo( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { let did = logged_in(&app, viewer, &headers, "fork")?; let repo = repo::found(&app, &input.repo).await?; let name = input .name .as_deref() .map(str::trim) .filter(|n| !n.is_empty()); let dest = named(&did, name.unwrap_or(&repo.name))?; let source = RepoUrl { authority: repo.did.clone(), name: repo.name.clone(), }; let ctx = crate::login::ctx_for(&app, &did); let forked = forge::fork(&ctx, &source, Some(&dest.name)).await; let url = forked.map_err(refused)?; // A private repository's fork is private, its record in a space of its // own, which no public index holds. let made = [("create", nsid::REPO, url.name.as_str())]; let public = if repo.in_space.is_some() { &[][..] } else { &made[..] }; wrote(&app, &did, public).await?; Ok(answer(RepoMade::new(&did, &url.name))) }