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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
175 lines · 5.7 KB Raw History
//! 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<App>, 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<App>,
    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<App>, 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<RepoUrl> {
    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<String>,
    default_branch: Option<String>,
    #[serde(default)]
    private: bool,
}

/// `dev.knotless.createRepo`.
pub async fn create_repo(
    State(app): State<Arc<App>>,
    Extension(viewer): Extension<Viewer>,
    headers: HeaderMap,
    axum::Json(input): axum::Json<CreateInput>,
) -> Result<Response> {
    let did = logged_in(&app, viewer, &headers, "create a repository")?;
    let url = named(&did, input.name.trim())?;
    let given = |s: Option<String>| 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<String>,
}

/// `dev.knotless.forkRepo`.
pub async fn fork_repo(
    State(app): State<Arc<App>>,
    Extension(viewer): Extension<Viewer>,
    headers: HeaderMap,
    axum::Json(input): axum::Json<ForkInput>,
) -> Result<Response> {
    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)))
}