//! `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
);
}
}