don't
5
fork

Configure Feed

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

feat: implement 'sh.tangled.repo.setDefaultBranch'

Signed-off-by: tjh <x@tjh.dev>

tjh fcb8e3f0 6afe7b6c

+226 -1
+2
crates/atproto/src/lib.rs
··· 6 6 pub mod handle; 7 7 pub mod nsid; 8 8 pub mod tid; 9 + pub mod uri; 9 10 10 11 pub use did::Did; 11 12 pub use handle::Handle; 12 13 pub use nsid::Nsid; 14 + pub use uri::RecordUri; 13 15 14 16 #[cfg(feature = "serde")] 15 17 pub mod serde;
+74
crates/atproto/src/uri.rs
··· 1 + use crate::did::OwnedDid; 2 + 3 + /// A fully defined Atmosphere URI pointing to a record. 4 + /// 5 + /// For example: "at://did:plc:65gha4t3avpfpzmvpbwovss7/sh.tangled.repo/3m24udbjajf22" 6 + /// 7 + #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] 8 + pub struct RecordUri { 9 + pub authority: OwnedDid, 10 + pub collection: String, 11 + pub rkey: String, 12 + } 13 + 14 + #[derive(Debug, PartialEq, thiserror::Error)] 15 + pub enum Error { 16 + #[error("Invalid authority: {0}")] 17 + InvalidAuthority(#[from] crate::did::Error), 18 + #[error("Could not parse AT URI")] 19 + Error, 20 + } 21 + 22 + fn parse(s: &str) -> Result<RecordUri, Error> { 23 + let Some(uri) = s.strip_prefix("at://") else { 24 + return Err(Error::Error); 25 + }; 26 + 27 + let mut parts = uri.splitn(3, '/'); 28 + let authority = parts 29 + .next() 30 + .map(OwnedDid::parse) 31 + .transpose()? 32 + .ok_or(Error::Error)?; 33 + let collection = parts.next().ok_or(Error::Error)?.to_string(); 34 + let rkey = parts.next().ok_or(Error::Error)?.to_string(); 35 + 36 + Ok(RecordUri { 37 + authority, 38 + collection, 39 + rkey, 40 + }) 41 + } 42 + 43 + #[cfg(feature = "serde")] 44 + mod impl_serde { 45 + use super::RecordUri; 46 + 47 + #[derive(Default)] 48 + pub struct RecordUriVisitor; 49 + 50 + impl<'de> serde::de::Visitor<'de> for RecordUriVisitor { 51 + type Value = RecordUri; 52 + 53 + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 54 + formatter.write_str("ATproto record URI") 55 + } 56 + 57 + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> 58 + where 59 + E: serde::de::Error, 60 + { 61 + let uri = super::parse(v).map_err(serde::de::Error::custom)?; 62 + Ok(uri) 63 + } 64 + } 65 + 66 + impl<'de> serde::Deserialize<'de> for RecordUri { 67 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 68 + where 69 + D: serde::Deserializer<'de>, 70 + { 71 + deserializer.deserialize_str(RecordUriVisitor::default()) 72 + } 73 + } 74 + }
+1 -1
crates/knot/README.md
··· 30 30 | ❌ | `sh.tangled.repo.merge` | | 31 31 | ❌ | `sh.tangled.repo.languages` | | 32 32 | ✅ | `sh.tangled.repo.log` | | 33 - | ❌ | `sh.tangled.repo.setDefaultBranch` | | 33 + | ✅ | `sh.tangled.repo.setDefaultBranch` | | 34 34 | ✅ | `sh.tangled.repo.tags` | | 35 35 | 🔶 | `sh.tangled.repo.tree` | cheats by not computing most recent commit 🚀 | 36 36
+1
crates/knot/src/public/xrpc.rs
··· 37 37 .merge(sh_tangled::repo::log()) 38 38 .merge(sh_tangled::repo::tags()) 39 39 .merge(sh_tangled::repo::tree()) 40 + .merge(sh_tangled::repo::set_default_branch()) 40 41 } 41 42 42 43 pub type XrpcResult<T> = Result<XrpcResponse<T>, XrpcError>;
+2
crates/knot/src/public/xrpc/sh_tangled/repo.rs
··· 14 14 mod impl_languages; 15 15 mod impl_log; 16 16 mod impl_merge_check; 17 + mod impl_set_default_branch; 17 18 mod impl_tags; 18 19 mod impl_tree; 19 20 ··· 57 56 impl_xrpc!(PROCEDURE, create, impl_create); 58 57 impl_xrpc!(PROCEDURE, delete, impl_delete); 59 58 impl_xrpc!(PROCEDURE, merge_check, impl_merge_check); 59 + impl_xrpc!(PROCEDURE, set_default_branch, impl_set_default_branch);
+103
crates/knot/src/public/xrpc/sh_tangled/repo/impl_set_default_branch.rs
··· 1 + use axum::{Json, extract::State}; 2 + use gix::{ 3 + lock::acquire::Fail, 4 + refs::{ 5 + FullName, Target, 6 + transaction::{Change, LogChange, PreviousValue, RefEdit}, 7 + }, 8 + }; 9 + use lexicon::sh_tangled::repo::set_default_branch::Input; 10 + 11 + use crate::{ 12 + model::{Knot, errors}, 13 + public::xrpc::XrpcError, 14 + services::{ 15 + authorization::{Authorization, Verification}, 16 + rbac::{Action, Policy, PolicyResult::Granted, RepositoryEditPolicy, RepositoryRef}, 17 + }, 18 + types::repository_key::RepositoryKey, 19 + }; 20 + 21 + pub const LXM: &str = "/sh.tangled.repo.setDefaultBranch"; 22 + 23 + #[derive(Debug)] 24 + pub struct SetDefaultBranchVerification; 25 + 26 + impl Verification for SetDefaultBranchVerification { 27 + const LEXICON_METHOD: &'static str = "sh.tangled.repo.setDefaultBranch"; 28 + } 29 + 30 + #[tracing::instrument(target = "sh_tangled::repo::setDefaultBranch", skip(knot), err)] 31 + pub async fn handle( 32 + State(knot): State<Knot>, 33 + authorization: Authorization<SetDefaultBranchVerification>, 34 + Json(Input { 35 + repo, 36 + default_branch, 37 + }): Json<Input>, 38 + ) -> Result<(), XrpcError> { 39 + if repo.collection != "sh.tangled.repo" { 40 + return Err(errors::InvalidRequest( 41 + "Wrong collection in repo URI, expected 'sh.tangled.repo'", 42 + ))?; 43 + } 44 + 45 + let repo_key = RepositoryKey::new(repo.authority, repo.rkey).map_err(errors::InvalidRequest)?; 46 + 47 + let claims = authorization.claims(); 48 + let policy = RepositoryEditPolicy; 49 + let can_create = policy 50 + .evaluate_access( 51 + &claims.iss.as_ref(), 52 + &Action::RepositoryEdit, 53 + &RepositoryRef::from(&repo_key), 54 + &knot, 55 + ) 56 + .await; 57 + 58 + if !matches!(can_create, Granted) { 59 + return Err(errors::Forbidden(format!( 60 + "'{}' does not have permission to modify repositories on this knot", 61 + claims.iss 62 + )))?; 63 + } 64 + 65 + let repository = knot 66 + .open_repository(&repo_key) 67 + .await 68 + .map_err(errors::RepoNotFound)? 69 + .to_thread_local(); 70 + 71 + let target_name: FullName = format!("refs/heads/{default_branch}") 72 + .try_into() 73 + .map_err(errors::InvalidRequest)?; 74 + 75 + let ref_change = RefEdit { 76 + change: Change::Update { 77 + log: LogChange::default(), 78 + expected: PreviousValue::Any, 79 + new: Target::Symbolic(target_name), 80 + }, 81 + name: "HEAD".try_into().expect("HEAD is a valid reference"), 82 + deref: false, 83 + }; 84 + 85 + let ref_log = repository 86 + .refs 87 + .transaction() 88 + .prepare([ref_change], Fail::Immediately, Fail::Immediately) 89 + .map_err(errors::Internal)? 90 + .commit(None) 91 + .map_err(errors::Internal)?; 92 + 93 + let Some(change) = ref_log.first() else { 94 + return Err(errors::Internal("Not ref changes applied"))?; 95 + }; 96 + 97 + let from = change.change.previous_value(); 98 + let to = change.change.new_value(); 99 + 100 + tracing::info!(?from, ?to, "updated HEAD"); 101 + 102 + Ok(()) 103 + }
+28
crates/knot/src/services/rbac.rs
··· 50 50 RepositoryCollaboratorDelete, 51 51 RepositoryCreate, 52 52 RepositoryDelete, 53 + RepositoryEdit, 53 54 RepositoryPush, 54 55 } 55 56 ··· 63 62 64 63 impl<'a> RepositoryRef<'a> { 65 64 pub fn new(did: &'a Did, rkey: &'a str) -> Self { 65 + Self { did, rkey } 66 + } 67 + } 68 + 69 + impl<'a> From<&'a RepositoryKey> for RepositoryRef<'a> { 70 + fn from(RepositoryKey { owner: did, rkey }: &'a RepositoryKey) -> Self { 66 71 Self { did, rkey } 67 72 } 68 73 } ··· 225 218 let is_owner = subject == context.owner() || resource.did == subject; 226 219 match (action, is_owner) { 227 220 (Action::RepositoryDelete, true) => PolicyResult::Granted, 221 + (_, _) => PolicyResult::Denied, 222 + } 223 + } 224 + .boxed() 225 + } 226 + } 227 + 228 + pub struct RepositoryEditPolicy; 229 + 230 + impl Policy<&Did, RepositoryRef<'_>, Action, KnotState> for RepositoryEditPolicy { 231 + fn evaluate_access<'s: 'a, 'a>( 232 + &'s self, 233 + &subject: &'a &Did, 234 + action: &'a Action, 235 + resource: &'a RepositoryRef<'_>, 236 + _: &'a KnotState, 237 + ) -> BoxFuture<'a, PolicyResult> { 238 + async move { 239 + let is_repository_owner = resource.did == subject; 240 + match (action, is_repository_owner) { 241 + (Action::RepositoryEdit, true) => PolicyResult::Granted, 228 242 (_, _) => PolicyResult::Denied, 229 243 } 230 244 }
+1
crates/lexicon/src/sh_tangled/repo.rs
··· 12 12 pub mod log; 13 13 pub mod merge_check; 14 14 pub mod pull; 15 + pub mod set_default_branch; 15 16 pub mod tags; 16 17 pub mod tree; 17 18
+14
crates/lexicon/src/sh_tangled/repo/set_default_branch.rs
··· 1 + use atproto::RecordUri; 2 + use serde::Deserialize; 3 + 4 + /// Parameters for the `sh.tangled.repo.setDefaultBranch` procedure. 5 + /// 6 + /// See: <https://tangled.org/tangled.org/core/blob/master/lexicons/repo/defaultBranch.json> 7 + /// 8 + #[derive(Debug, Deserialize)] 9 + #[serde(rename_all = "camelCase")] 10 + pub struct Input { 11 + pub repo: RecordUri, 12 + 13 + pub default_branch: String, 14 + }