This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-xrpc / src / branches.rs
5.2 kB 145 lines
1use std::sync::Arc; 2 3use axum::body::Bytes; 4use axum::extract::State; 5use axum::response::Response; 6use http::HeaderMap; 7use serde::Deserialize; 8 9use knot_events::GitRefUpdate; 10use knot_git::{GitError, RefUpdate}; 11use knot_index::Resolved; 12use knot_runtime::{Clock, HttpTransport}; 13use knot_types::{AtUri, BranchName, OwnerDid, RepoDid, RepoRkey}; 14 15use crate::body::RepoAtUri; 16use crate::error::XrpcError; 17use crate::{XrpcState, decode, ok_empty, run_blocking}; 18 19pub(crate) const SET_DEFAULT_ROUTE: &str = "/xrpc/sh.tangled.repo.setDefaultBranch"; 20pub(crate) const DELETE_ROUTE: &str = "/xrpc/sh.tangled.repo.deleteBranch"; 21const REPO_COLLECTION: &str = "sh.tangled.repo"; 22 23#[derive(Deserialize)] 24struct SetDefaultBranchInput { 25 repo: RepoAtUri, 26 #[serde(rename = "defaultBranch")] 27 default_branch: BranchName, 28} 29 30#[derive(Deserialize)] 31struct DeleteBranchInput { 32 repo: RepoAtUri, 33 branch: BranchName, 34} 35 36const BRANCH_DENIED: &str = "only repository owner or a collaborator may change its branches"; 37 38pub(crate) fn resolve_at_uri<H: HttpTransport, C: Clock>( 39 state: &XrpcState<H, C>, 40 at: &AtUri<String>, 41) -> Result<RepoDid, XrpcError> { 42 let owner = OwnerDid::new(at.authority().as_str()) 43 .map_err(|_| XrpcError::invalid_request("at-uri authority must be a DID"))?; 44 if at 45 .collection() 46 .is_none_or(|collection| collection.as_str() != REPO_COLLECTION) 47 { 48 return Err(XrpcError::invalid_request( 49 "at-uri must address an sh.tangled.repo record", 50 )); 51 } 52 let rkey = at 53 .rkey() 54 .ok_or_else(|| XrpcError::invalid_request("at-uri must include a record key"))?; 55 let rkey = RepoRkey::new(rkey.as_str()) 56 .map_err(|_| XrpcError::invalid_request("at-uri record key isn't a valid rkey"))?; 57 match state.index.resolve_repo(&owner, &rkey) { 58 Resolved::Ready(Some(repo_did)) => Ok(repo_did), 59 Resolved::Ready(None) => Err(XrpcError::not_found("no such repository on this knot")), 60 Resolved::Warming => Err(XrpcError::warming("registry projection is still warming")), 61 } 62} 63 64pub(crate) async fn set_default_branch<H: HttpTransport, C: Clock>( 65 State(state): State<Arc<XrpcState<H, C>>>, 66 headers: HeaderMap, 67 method: crate::Method, 68 body: Bytes, 69) -> Result<Response, XrpcError> { 70 let actor = state.authenticate(&headers, &method).await?; 71 let input: SetDefaultBranchInput = decode(&body)?; 72 let repo_did = resolve_at_uri(&state, input.repo.at_uri())?; 73 crate::authorize_push(&state, &actor, &repo_did, BRANCH_DENIED).await?; 74 let refname = input.default_branch.head_ref(); 75 76 let layout = state.layout.clone(); 77 let target = repo_did.clone(); 78 let events = Arc::clone(&state.events); 79 let reservation = run_blocking(move || { 80 let repo = layout.open(&target)?; 81 let target_exists = repo.find_ref(&refname)?.is_some(); 82 let has_branches = !repo.branches()?.is_empty(); 83 if !target_exists && has_branches { 84 return Err(XrpcError::not_found("no such branch to set as default")); 85 } 86 repo.set_head_sealed(&refname, || events.reserve()) 87 .map_err(XrpcError::from) 88 }) 89 .await?; 90 91 let owner = crate::current_owner(&state, &repo_did); 92 reservation.fulfill(&GitRefUpdate::new(repo_did, owner, actor)); 93 94 Ok(ok_empty()) 95} 96 97pub(crate) async fn delete_branch<H: HttpTransport, C: Clock>( 98 State(state): State<Arc<XrpcState<H, C>>>, 99 headers: HeaderMap, 100 method: crate::Method, 101 body: Bytes, 102) -> Result<Response, XrpcError> { 103 let actor = state.authenticate(&headers, &method).await?; 104 let input: DeleteBranchInput = decode(&body)?; 105 let repo_did = resolve_at_uri(&state, input.repo.at_uri())?; 106 crate::authorize_push(&state, &actor, &repo_did, BRANCH_DENIED).await?; 107 let refname = input.branch.head_ref(); 108 109 let layout = state.layout.clone(); 110 let target = repo_did.clone(); 111 let deleted_ref = refname.clone(); 112 let events = Arc::clone(&state.events); 113 let (old, reservation, format) = run_blocking(move || { 114 let repo = layout.open(&target)?; 115 if repo.default_branch().as_ref() == Some(&refname) { 116 return Err(XrpcError::invalid_request( 117 "default branch cannot be deleted until a different default is set", 118 )); 119 } 120 let old = repo 121 .find_ref(&refname)? 122 .ok_or_else(|| XrpcError::not_found("no such branch"))?; 123 let reservation = repo 124 .update_ref_sealed(&RefUpdate::Delete { name: refname, old }, || { 125 events.reserve() 126 }) 127 .map_err(|error| match error { 128 GitError::AtomicRefs(_) => { 129 XrpcError::conflict("branch changed during deletion, retry") 130 } 131 other => XrpcError::internal(other.to_string()), 132 })?; 133 Ok((old, reservation, repo.object_format())) 134 }) 135 .await?; 136 137 let owner = crate::current_owner(&state, &repo_did); 138 reservation.fulfill(&GitRefUpdate::new(repo_did, owner, actor).on_ref( 139 deleted_ref, 140 knot_types::RefTransition::Delete { old }, 141 format, 142 )); 143 144 Ok(ok_empty()) 145}