//! `internal/meta` on the forge: the repo the swarm meta flake is committed to //! (`crate::meta_flake`), and the read grant each hive's account holds on it. use anyhow::{Context, Result}; use base64::Engine as _; use forgejo_api::structs::{ AddCollaboratorOptionPermission, ChangeFileOperation, ChangeFileOperationOperation, ChangeFilesOptions, }; use super::objects::SHARED_ORG; use super::{Client, DefaultBranchFiles, is_repo_not_found}; use crate::meta_flake::{META_REPO, MetaFile, MetaForge}; impl MetaForge for Client { async fn head_files( &self, owner: &str, repo: &str, branch: Option<&str>, paths: &[&str], ) -> Result> { self.branch_files(owner, repo, branch, paths).await } async fn ensure_meta_repo(&self) -> Result<()> { self.create_shared_repo(SHARED_ORG, META_REPO, true).await } async fn grant_meta_read(&self, login: &str) -> Result<()> { self.add_collaborator_to( SHARED_ORG, META_REPO, login, AddCollaboratorOptionPermission::Read, ) .await } async fn commits_ahead( &self, owner: &str, repo: &str, base: &str, head: &str, ) -> Result> { let basehead = format!("{base}...{head}"); match self .api .call(|api| api.repo_compare_diff(owner, repo, &basehead).into_future()) .await { Ok(compare) => Ok(Some( u64::try_from(compare.total_commits.unwrap_or(0)).unwrap_or(0), )), Err(e) if is_repo_not_found(&e) => Ok(None), Err(e) => Err(e).with_context(|| format!("compare {basehead} in {owner}/{repo}")), } } async fn commit_meta(&self, message: &str, files: Vec) -> Result<()> { let ops: Vec = files .into_iter() .map(|f| ChangeFileOperation { content: Some(base64::engine::general_purpose::STANDARD.encode(f.bytes)), from_path: None, operation: if f.sha.is_some() { ChangeFileOperationOperation::Update } else { ChangeFileOperationOperation::Create }, path: f.path.to_owned(), sha: f.sha, }) .collect(); self.api .call(|api| { api.repo_change_files( SHARED_ORG, META_REPO, ChangeFilesOptions { author: None, branch: None, committer: None, dates: None, files: ops.clone(), force_overwrite_new_branch: None, message: Some(message.to_owned()), new_branch: None, signoff: None, }, ) .into_future() }) .await .with_context(|| format!("commit to {SHARED_ORG}/{META_REPO}"))?; Ok(()) } } #[cfg(test)] mod tests { use std::sync::{Arc, Mutex}; use super::*; /// A stand-in forge answering Forgejo's compare endpoint: `total_commits` /// of 3 for `aaa...bbb`, 0 for `bbb...aaa`, 404 for anything else. /// Records every request's path. async fn compare_forge() -> (Client, Arc>>) { use axum::http::{StatusCode, Uri}; let seen = Arc::new(Mutex::new(Vec::new())); let recorded = Arc::clone(&seen); let app = axum::Router::new().fallback(move |uri: Uri| { let recorded = Arc::clone(&recorded); async move { recorded.lock().unwrap().push(uri.path().to_owned()); match uri.path() { "/api/v1/repos/o/r/compare/aaa...bbb" => ( StatusCode::OK, r#"{"total_commits":3,"commits":[],"files":[]}"#, ), "/api/v1/repos/o/r/compare/bbb...aaa" => { (StatusCode::OK, r#"{"total_commits":0,"commits":[]}"#) } _ => (StatusCode::NOT_FOUND, r#"{"message":"not found"}"#), } } }); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let url = url::Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap(); tokio::spawn(async move { axum::serve(listener, app).await }); let api = crate::forge::api::tests::fixed(url.clone(), "stub-token"); (Client { api, url }, seen) } #[tokio::test] async fn commits_ahead_reads_the_compare_endpoint_as_base_dots_head() { let (client, seen) = compare_forge().await; assert_eq!( client.commits_ahead("o", "r", "aaa", "bbb").await.unwrap(), Some(3) ); assert_eq!( client.commits_ahead("o", "r", "bbb", "aaa").await.unwrap(), Some(0) ); assert_eq!( *seen.lock().unwrap(), [ "/api/v1/repos/o/r/compare/aaa...bbb", "/api/v1/repos/o/r/compare/bbb...aaa" ] ); } #[tokio::test] async fn a_commit_the_forge_lacks_is_no_count_not_an_error() { let (client, _) = compare_forge().await; assert_eq!( client.commits_ahead("o", "r", "aaa", "zzz").await.unwrap(), None ); } }