//! What a repository's settings change besides its name and place: its //! branches (`dev.knotless.createBranch`, `deleteBranch`, `renameBranch`), //! which branches are protected (`protectBranch`, `unprotectBranch`), its //! labels (`defineLabel`, `removeLabel`), who may push to it or label it //! (`shareRepo`, `unshareRepo`, `setTriager`, `removeTriager`), and whose //! pulls its runners check as code no one vouches for (`addAgent`, //! `removeAgent`), each through the same code as the command it is named //! for. //! //! Branches are made and deleted as `knotless branch` does them, by the //! owner directly and by a collaborator through the gateway, which refuses //! anyone else; the rest is the owner's alone, checked here. use super::repos::{refused, rewrote, wrote}; use super::settings::owned; use super::stars::logged_in; use super::*; use crate::viewer::Viewer; use axum::Extension; use knotless::manage; use knotless::records::{Label, Reviews, Rule}; /// The repository `repo` names, as a URL by its owner's DID. fn url_of(repo: &Repo) -> RepoUrl { RepoUrl { authority: repo.did.clone(), name: repo.name.clone(), } } #[derive(Deserialize)] pub struct BranchInput { repo: String, name: String, from: Option, to: Option, } /// `dev.knotless.createBranch`'s, `deleteBranch`'s and `renameBranch`'s /// answer. #[derive(Serialize)] pub struct Branched { name: String, #[serde(skip_serializing_if = "Option::is_none")] sha: Option, } /// The repository `uri` names, for a branch to be changed in by the /// account logged in, with a context acting as them. async fn pushing( app: &App, viewer: Viewer, headers: &HeaderMap, uri: &str, ) -> Result<(Repo, knotless::env::Ctx)> { let did = logged_in(app, viewer, headers, "change its branches")?; let repo = repo::found(app, uri).await?; Ok((repo, crate::login::ctx_for(app, &did))) } /// `dev.knotless.createBranch`. pub async fn create_branch( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { let (repo, ctx) = pushing(&app, viewer, &headers, &input.repo).await?; let from = input.from.as_deref().filter(|f| !f.is_empty()); let made = forge::branch(&ctx, &url_of(&repo), &input.name, from, false).await; let sha = made.map_err(refused)?; wrote(&app, &repo.did, &[]).await?; Ok(answer(Branched { name: input.name, sha, })) } /// `dev.knotless.deleteBranch`. pub async fn delete_branch( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { let (repo, ctx) = pushing(&app, viewer, &headers, &input.repo).await?; let deleted = forge::branch(&ctx, &url_of(&repo), &input.name, None, true).await; deleted.map_err(refused)?; wrote(&app, &repo.did, &[]).await?; Ok(answer(Branched { name: input.name, sha: None, })) } /// `dev.knotless.renameBranch`. pub async fn rename_branch( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { let (repo, ctx) = pushing(&app, viewer, &headers, &input.repo).await?; let to = input.to.unwrap_or_default(); let renamed = forge::rename_branch(&ctx, &url_of(&repo), &input.name, &to).await; renamed.map_err(refused)?; wrote(&app, &repo.did, &[]).await?; Ok(answer(Branched { name: to, sha: None, })) } #[derive(Deserialize)] pub struct ProtectInput { repo: String, branch: String, #[serde(default)] merges: bool, #[serde(default)] queue: bool, #[serde(default)] reviews: Reviews, } /// `dev.knotless.protectBranch`'s and `unprotectBranch`'s answer. #[derive(Serialize)] pub struct Rules { rules: Vec, } /// `dev.knotless.protectBranch`. pub async fn protect_branch( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { protecting(&app, viewer, &headers, input, true).await } /// `dev.knotless.unprotectBranch`. pub async fn unprotect_branch( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { protecting(&app, viewer, &headers, input, false).await } /// Protects `input.branch`, or with `on` false no longer, as `knotless /// protect` and `unprotect` do. async fn protecting( app: &Arc, viewer: Viewer, headers: &HeaderMap, input: ProtectInput, on: bool, ) -> Result { let act = "protect its branches"; let (repo, ctx) = owned(app, viewer, headers, &input.repo, act).await?; let (merges, queue) = (input.merges, input.queue); let rule = on.then(|| Rule::new(&input.branch, merges, queue).reviewed(input.reviews)); let protected = manage::protect(&ctx, &url_of(&repo), &input.branch, rule).await; let rules = protected.map_err(refused)?; rewrote(app, &repo).await?; Ok(answer(Rules { rules })) } #[derive(Deserialize)] pub struct LabelInput { repo: String, name: String, color: Option, description: Option, #[serde(default)] exclusive: bool, } /// `dev.knotless.defineLabel`'s and `removeLabel`'s answer. #[derive(Serialize)] pub struct Labels { labels: Vec