[wip] 🦀 knot server and some related AT stuff

refactor(knot): move branches impl to handler

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

tjh.dev 3eca18a2 2fe4465e

verified
+137 -83
+7 -1
crates/gordian-knot/src/model/knot_state.rs
··· 229 229 pub async fn open_repository( 230 230 &self, 231 231 repo_key: &RepositoryKey, 232 - ) -> Result<gix::ThreadSafeRepository, Arc<gix::open::Error>> { 232 + ) -> Result<gix::ThreadSafeRepository, RepositoryOpenError> { 233 233 use gix::open::Options; 234 234 235 235 let repository = self ··· 478 478 fn from(value: DataStoreError) -> Self { 479 479 Self::Lookup(Arc::new(value)) 480 480 } 481 + } 482 + 483 + #[derive(Debug, thiserror::Error)] 484 + pub enum RepositoryOpenError { 485 + #[error(transparent)] 486 + Gix(#[from] Arc<gix::open::Error>), 481 487 } 482 488 483 489 impl AuthorizationClaimsStore<service_auth::Claims> for KnotState {
-46
crates/gordian-knot/src/model/repository.rs
··· 23 23 use gordian_lexicon::sh_tangled::repo::branch; 24 24 use gordian_lexicon::sh_tangled::repo::get_default_branch; 25 25 use gordian_lexicon::sh_tangled::repo::languages; 26 - use gordian_lexicon::sh_tangled::repo::refs; 27 26 use gordian_lexicon::sh_tangled::repo::tree; 28 27 use rustc_hash::FxHashSet; 29 28 use serde::Deserialize; ··· 38 39 use crate::public::xrpc::XrpcResult; 39 40 use crate::types::repository_spec::RepositoryKey; 40 41 use crate::types::repository_spec::RepositoryPath; 41 - use crate::types::sh_tangled::repo::branches; 42 42 use crate::types::sh_tangled::repo::compare; 43 43 use crate::types::sh_tangled::repo::diff; 44 44 use crate::types::sh_tangled::repo::log; ··· 180 182 is_default, 181 183 }) 182 184 .into()) 183 - } 184 - 185 - pub fn branches(&self, _params: branches::Input) -> XrpcResult<Json<branches::Output>> { 186 - // Assume HEAD points to the intended default branch. This *should* be 187 - // true for a bare repository. 188 - let head = self.repository.head()?; 189 - let default_name = head 190 - .referent_name() 191 - .ok_or(errors::HeadDetached)? 192 - .shorten() 193 - .to_string(); 194 - 195 - let mut branches = Vec::new(); 196 - for branch in self.repository.references()?.local_branches()? 197 - // .skip(params.cursor) 198 - // .take(_params.limit.into()) 199 - { 200 - let Ok(branch) = branch.inspect_err(|error| tracing::error!(?error)) else { 201 - continue; 202 - }; 203 - 204 - let name = branch.name().shorten().to_string(); 205 - let Some(id) = branch.try_id() else { 206 - tracing::warn!(?name, "branch unborn, skipping"); 207 - continue; 208 - }; 209 - 210 - let Ok(commit) = self.repository.find_commit(id) else { 211 - tracing::error!(?name, ?id, "failed to find commit for branch"); 212 - continue; 213 - }; 214 - 215 - let is_default = name == default_name; 216 - branches.push(branches::Branch { 217 - reference: refs::Reference { 218 - name, 219 - hash: commit.id.into(), 220 - }, 221 - commit: convert::try_convert_commit(commit)?, 222 - is_default, 223 - }); 224 - } 225 - 226 - Ok(Json(branches::Output { branches }).into()) 227 185 } 228 186 229 187 pub fn compare(&self, params: compare::Input) -> XrpcResult<Json<compare::Output>> {
+43
crates/gordian-knot/src/public/xrpc.rs
··· 257 257 } 258 258 } 259 259 } 260 + 261 + impl From<crate::types::repository_spec::Error> for XrpcError { 262 + fn from(value: crate::types::repository_spec::Error) -> Self { 263 + Self::new(StatusCode::BAD_REQUEST, "InvalidRequest", value.to_string()) 264 + } 265 + } 266 + 267 + impl From<crate::model::knot_state::RepositoryResolveError> for XrpcError { 268 + fn from(value: crate::model::knot_state::RepositoryResolveError) -> Self { 269 + use crate::model::knot_state::RepositoryResolveError as Error; 270 + match value { 271 + Error::Resolve(error) => { 272 + Self::new(StatusCode::BAD_REQUEST, "RepoNotFound", error.to_string()) 273 + } 274 + Error::Lookup(error) => { 275 + Self::new(StatusCode::NOT_FOUND, "RepoNotFound", error.to_string()) 276 + } 277 + Error::NotFound => Self::from_static( 278 + StatusCode::NOT_FOUND, 279 + "RepoNotFound", 280 + "Repository not found", 281 + ), 282 + } 283 + } 284 + } 285 + 286 + impl From<crate::model::knot_state::RepositoryOpenError> for XrpcError { 287 + fn from(value: crate::model::knot_state::RepositoryOpenError) -> Self { 288 + use crate::model::knot_state::RepositoryOpenError as Error; 289 + match value { 290 + Error::Gix(error) => match error.as_ref() { 291 + gix::open::Error::Io(error) if error.kind() == std::io::ErrorKind::NotFound => { 292 + Self::new(StatusCode::NOT_FOUND, "RepoNotFound", error.to_string()) 293 + } 294 + _ => Self::new( 295 + StatusCode::INTERNAL_SERVER_ERROR, 296 + "RepoNotFound", 297 + error.to_string(), 298 + ), 299 + }, 300 + } 301 + } 302 + }
+83 -8
crates/gordian-knot/src/public/xrpc/sh_tangled/repo/impl_branches.rs
··· 1 1 use axum::Json; 2 2 use axum::extract::State; 3 + use gordian_lexicon::sh_tangled::repo::refs::Commit; 4 + use gordian_lexicon::sh_tangled::repo::refs::Reference; 5 + use serde::Serialize; 3 6 use tokio_rayon::AsyncThreadPool as _; 4 7 5 8 use crate::model::Knot; 6 - use crate::model::repository::TangledRepository; 9 + use crate::model::convert; 10 + use crate::model::errors; 7 11 use crate::public::xrpc::XrpcQuery; 8 12 use crate::public::xrpc::XrpcResult; 9 - use crate::types::sh_tangled::repo::branches::Input; 10 - use crate::types::sh_tangled::repo::branches::Output; 13 + 14 + pub use crate::lexicon::sh_tangled::repo::branches::Input; 11 15 12 16 pub const LXM: &str = "/sh.tangled.repo.branches"; 13 17 14 - #[tracing::instrument(target = "sh_tangled::repo::branches", skip(knot, repository), err)] 18 + /// Output of `sh.tangled.repo.branches` query. 19 + #[derive(Debug, Default, Serialize)] 20 + #[serde(rename_all = "camelCase")] 21 + pub struct Output { 22 + #[serde(skip_serializing_if = "Vec::is_empty")] 23 + pub branches: Vec<Branch>, 24 + } 25 + 26 + #[derive(Debug, Serialize)] 27 + #[serde(rename_all = "camelCase")] 28 + pub struct Branch { 29 + pub reference: Reference, 30 + pub commit: Commit, 31 + pub is_default: bool, 32 + } 33 + 34 + pub type Response = XrpcResult<Json<Output>>; 35 + 36 + #[tracing::instrument(target = "sh_tangled::repo::branches", skip(knot), err)] 15 37 pub async fn handle( 16 38 State(knot): State<Knot>, 17 - XrpcQuery(params): XrpcQuery<Input>, 18 - repository: TangledRepository, 19 - ) -> XrpcResult<Json<Output>> { 39 + XrpcQuery(Input { 40 + repo, 41 + limit, 42 + cursor, 43 + }): XrpcQuery<Input>, 44 + ) -> Response { 45 + let repo_path = repo.parse()?; 46 + let repo_key = knot.resolve_repo_key(&repo_path).await?; 47 + let repository = knot.open_repository(&repo_key).await?; 48 + 20 49 knot.pool() 21 - .spawn_async(move || repository.branches(params)) 50 + .spawn_async(move || { 51 + let repository = repository.to_thread_local(); 52 + 53 + // Assume HEAD points to the intended default branch. This *should* be 54 + // true for a bare repository. 55 + let head = repository.head()?; 56 + let default_name = head 57 + .referent_name() 58 + .ok_or(errors::HeadDetached)? 59 + .shorten() 60 + .to_string(); 61 + 62 + let mut branches = Vec::new(); 63 + for branch in repository 64 + .references()? 65 + .local_branches()? 66 + .skip(cursor.into()) 67 + .take(limit.into()) 68 + { 69 + let Ok(branch) = branch.inspect_err(|error| tracing::error!(?error)) else { 70 + continue; 71 + }; 72 + 73 + let name = branch.name().shorten().to_string(); 74 + let Some(id) = branch.try_id() else { 75 + tracing::warn!(?name, "branch unborn, skipping"); 76 + continue; 77 + }; 78 + 79 + let Ok(commit) = repository.find_commit(id) else { 80 + tracing::error!(?name, ?id, "failed to find commit for branch"); 81 + continue; 82 + }; 83 + 84 + let is_default = name == default_name; 85 + branches.push(Branch { 86 + reference: Reference { 87 + name, 88 + hash: commit.id.into(), 89 + }, 90 + commit: convert::try_convert_commit(commit)?, 91 + is_default, 92 + }); 93 + } 94 + 95 + Ok(Json(Output { branches }).into()) 96 + }) 22 97 .await 23 98 }
-26
crates/gordian-knot/src/types/sh_tangled.rs
··· 1 1 pub mod repo { 2 - pub mod branches { 3 - use serde::Serialize; 4 - 5 - pub use crate::lexicon::sh_tangled::repo::branches::Input; 6 - use crate::lexicon::sh_tangled::repo::refs; 7 - 8 - /// Output of `sh.tangled.repo.branches` query. 9 - #[derive(Debug, Default, Serialize)] 10 - #[serde(rename_all = "camelCase")] 11 - pub struct Output { 12 - #[serde(skip_serializing_if = "Vec::is_empty")] 13 - pub branches: Vec<Branch>, 14 - } 15 - 16 - #[derive(Debug, Serialize)] 17 - #[serde(rename_all = "camelCase")] 18 - pub struct Branch { 19 - pub reference: refs::Reference, 20 - pub commit: refs::Commit, 21 - #[serde(rename = "is_deafult")] 22 - pub is_default: bool, 23 - } 24 - 25 - pub type Response = axum::Json<Output>; 26 - } 27 - 28 2 pub mod compare { 29 3 use serde::Serialize; 30 4
+4 -2
crates/gordian-lexicon/src/sh_tangled/repo/branches.rs
··· 21 21 pub limit: u16, 22 22 23 23 /// Pagination cursor 24 - pub cursor: Option<String>, 24 + pub cursor: u16, 25 25 } 26 26 27 27 #[derive(serde::Deserialize)] 28 28 struct UnvalidatedInput { 29 29 repo: String, 30 30 limit: Option<u16>, 31 - cursor: Option<String>, 31 + cursor: Option<u16>, 32 32 } 33 33 34 34 impl TryFrom<UnvalidatedInput> for Input { ··· 45 45 if !(LIMIT_MIN..=LIMIT_MAX).contains(&limit) { 46 46 return Err("'limit' field is outside acceptable range"); 47 47 } 48 + 49 + let cursor = cursor.unwrap_or_default(); 48 50 49 51 Ok(Self { 50 52 repo,