This repository has no description
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}