johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! 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<String>,
to: Option<String>,
}
/// `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<String>,
}
/// 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<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<BranchInput>,
) -> Result<Response> {
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<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<BranchInput>,
) -> Result<Response> {
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<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<BranchInput>,
) -> Result<Response> {
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<Rule>,
}
/// `dev.knotless.protectBranch`.
pub async fn protect_branch(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<ProtectInput>,
) -> Result<Response> {
protecting(&app, viewer, &headers, input, true).await
}
/// `dev.knotless.unprotectBranch`.
pub async fn unprotect_branch(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<ProtectInput>,
) -> Result<Response> {
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<App>,
viewer: Viewer,
headers: &HeaderMap,
input: ProtectInput,
on: bool,
) -> Result<Response> {
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<String>,
description: Option<String>,
#[serde(default)]
exclusive: bool,
}
/// `dev.knotless.defineLabel`'s and `removeLabel`'s answer.
#[derive(Serialize)]
pub struct Labels {
labels: Vec<Label>,
}
/// `dev.knotless.defineLabel`.
pub async fn define_label(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<LabelInput>,
) -> Result<Response> {
let given = |s: Option<String>| s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
let name = input.name.trim();
let label = Label {
color: given(input.color),
description: given(input.description),
exclusive: input.exclusive,
..Label::new(name)
};
labelling(&app, viewer, &headers, &input.repo, name, Some(label)).await
}
#[derive(Deserialize)]
pub struct NamedInput {
repo: String,
name: String,
}
/// `dev.knotless.removeLabel`.
pub async fn remove_label(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<NamedInput>,
) -> Result<Response> {
labelling(&app, viewer, &headers, &input.repo, &input.name, None).await
}
/// Defines `label` as `name`, or with none takes the label `name` away, as
/// `knotless label define` and `remove` do.
async fn labelling(
app: &Arc<App>,
viewer: Viewer,
headers: &HeaderMap,
uri: &str,
name: &str,
label: Option<Label>,
) -> Result<Response> {
let (repo, ctx) = owned(app, viewer, headers, uri, "define its labels").await?;
let defined = manage::define_label(&ctx, &url_of(&repo), name, label).await;
let labels = defined.map_err(refused)?;
rewrote(app, &repo).await?;
Ok(answer(Labels { labels }))
}
#[derive(Deserialize)]
pub struct ShareInput {
repo: String,
with: String,
/// Whether to let them push, or, for a private repository, only read
/// it: pushing unless said.
write: Option<bool>,
}
/// `dev.knotless.shareRepo`'s and `unshareRepo`'s answer.
#[derive(Serialize)]
pub struct Collaborators {
collaborators: Vec<ActorView>,
}
/// `dev.knotless.shareRepo`.
pub async fn share_repo(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<ShareInput>,
) -> Result<Response> {
sharing(&app, viewer, &headers, input, true).await
}
/// `dev.knotless.unshareRepo`.
pub async fn unshare_repo(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<ShareInput>,
) -> Result<Response> {
sharing(&app, viewer, &headers, input, false).await
}
/// Lets `input.with` push to the repository (or with `input.write` false
/// read a private one), or with `on` false no longer, as `knotless repo share`
/// and `unshare` do.
async fn sharing(
app: &Arc<App>,
viewer: Viewer,
headers: &HeaderMap,
input: ShareInput,
on: bool,
) -> Result<Response> {
let (repo, ctx) = owned(app, viewer, headers, &input.repo, "share it").await?;
let url = url_of(&repo);
let who = input.with.trim();
let shared = match on {
true => manage::share(&ctx, &url, who, input.write.unwrap_or(true)).await,
false => manage::unshare(&ctx, &url, who).await,
};
shared.map_err(refused)?;
rewrote(app, &repo).await?;
let members = manage::members(&ctx, &url).await.map_err(refused)?;
let writers = members.iter().filter(|m| m.access == "write");
let collaborators = writers.map(|m| actor(app, &m.did));
let collaborators = futures::future::join_all(collaborators).await;
Ok(answer(Collaborators { collaborators }))
}
#[derive(Deserialize)]
pub struct TriagerInput {
repo: String,
who: String,
#[serde(default)]
scopes: Vec<ScopeInput>,
}
#[derive(Deserialize)]
pub struct ScopeInput {
name: String,
}
/// `dev.knotless.setTriager`'s and `removeTriager`'s answer.
#[derive(Serialize)]
pub struct Triagers {
triagers: Vec<super::settings::TriagerView>,
/// Why a private repository's new triager was not told of it.
#[serde(skip_serializing_if = "Option::is_none")]
warning: Option<String>,
}
/// `dev.knotless.setTriager`.
pub async fn set_triager(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<TriagerInput>,
) -> Result<Response> {
triaging(&app, viewer, &headers, input, true).await
}
/// `dev.knotless.removeTriager`.
pub async fn remove_triager(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<TriagerInput>,
) -> Result<Response> {
triaging(&app, viewer, &headers, input, false).await
}
/// Names `input.who` a triager of the scopes given, or with `on` false no
/// longer, as `knotless triager add` and `remove` do.
async fn triaging(
app: &Arc<App>,
viewer: Viewer,
headers: &HeaderMap,
input: TriagerInput,
on: bool,
) -> Result<Response> {
let (repo, ctx) = owned(app, viewer, headers, &input.repo, "name its triagers").await?;
let names: Vec<&str> = input.scopes.iter().map(|s| s.name.trim()).collect();
let scopes = on.then_some(names.as_slice());
let set = manage::set_triager(&ctx, &url_of(&repo), input.who.trim(), scopes).await;
let (set, warning) = set.map_err(refused)?;
rewrote(app, &repo).await?;
let mut triagers = Vec::new();
for t in set {
triagers.push(super::settings::TriagerView::new(app, t).await);
}
Ok(answer(Triagers { triagers, warning }))
}
#[derive(Deserialize)]
pub struct AgentInput {
repo: String,
agent: String,
}
/// `dev.knotless.addAgent`'s and `removeAgent`'s answer.
#[derive(Serialize)]
pub struct Agents {
agents: Vec<ActorView>,
}
/// `dev.knotless.addAgent`.
pub async fn add_agent(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<AgentInput>,
) -> Result<Response> {
naming_agent(&app, viewer, &headers, input, true).await
}
/// `dev.knotless.removeAgent`.
pub async fn remove_agent(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<AgentInput>,
) -> Result<Response> {
naming_agent(&app, viewer, &headers, input, false).await
}
/// Names `input.agent` among the repository's agents, or with `add` false
/// no longer, as `knotless agent add` and `remove` do.
async fn naming_agent(
app: &Arc<App>,
viewer: Viewer,
headers: &HeaderMap,
input: AgentInput,
add: bool,
) -> Result<Response> {
let (repo, ctx) = owned(app, viewer, headers, &input.repo, "name its agents").await?;
let url = url_of(&repo);
let named = manage::set_agent(&ctx, &url, input.agent.trim(), add).await;
named.map_err(refused)?;
rewrote(app, &repo).await?;
let dids = manage::agents(&ctx, &url).await.map_err(refused)?;
let agents = futures::future::join_all(dids.iter().map(|d| actor(app, d))).await;
Ok(answer(Agents { agents }))
}