forked from kemitix/git-next
192 lines
5.8 KiB
Rust
192 lines
5.8 KiB
Rust
use base64::Engine;
|
|
use kxio::network::{self, Network};
|
|
use terrors::OneOf;
|
|
use tracing::{error, info};
|
|
|
|
use crate::server::config::{BranchName, RepoConfig, RepoDetails};
|
|
|
|
#[derive(Debug)]
|
|
pub struct RepoConfigFileNotFound;
|
|
#[derive(Debug)]
|
|
pub struct RepoConfigIsNotFile;
|
|
#[derive(Debug)]
|
|
pub struct RepoConfigDecodeError;
|
|
#[derive(Debug)]
|
|
pub struct RepoConfigParseError;
|
|
#[derive(Debug)]
|
|
pub struct RepoConfigUnknownError(pub network::StatusCode);
|
|
|
|
type RepoConfigLoadErrors = (
|
|
RepoConfigFileNotFound,
|
|
RepoConfigIsNotFile,
|
|
RepoConfigDecodeError,
|
|
RepoConfigParseError,
|
|
RepoConfigUnknownError,
|
|
RepoConfigBranchNotFound,
|
|
);
|
|
|
|
pub async fn load(
|
|
details: &RepoDetails,
|
|
net: &Network,
|
|
) -> Result<RepoConfig, OneOf<RepoConfigLoadErrors>> {
|
|
let hostname = &details.forge.hostname;
|
|
let path = &details.repo;
|
|
let filepath = ".git-next.toml";
|
|
let branch = &details.branch;
|
|
use secrecy::ExposeSecret;
|
|
let token = details.forge.token.expose_secret();
|
|
let url = network::NetUrl::new(format!(
|
|
"https://{hostname}/api/v1/repos/{path}/contents/{filepath}?ref={branch}&token={token}"
|
|
));
|
|
|
|
info!(%url, "Loading config");
|
|
let request = network::NetRequest::new(
|
|
network::RequestMethod::Get,
|
|
url,
|
|
network::NetRequestHeaders::new(),
|
|
network::RequestBody::None,
|
|
network::ResponseType::Json,
|
|
None,
|
|
network::NetRequestLogging::None,
|
|
);
|
|
let result = net.get::<ForgeContentsResponse>(request).await;
|
|
let response = result.map_err(|e| match e {
|
|
network::NetworkError::RequestFailed(_, status_code, _)
|
|
| network::NetworkError::RequestError(_, status_code, _) => match status_code {
|
|
network::StatusCode::NOT_FOUND => OneOf::new(RepoConfigFileNotFound),
|
|
_ => OneOf::new(RepoConfigUnknownError(status_code)),
|
|
},
|
|
_ => OneOf::new(RepoConfigUnknownError(
|
|
network::StatusCode::INTERNAL_SERVER_ERROR,
|
|
)),
|
|
})?;
|
|
let status = response.status_code();
|
|
let config = match response.response_body() {
|
|
Some(body) => {
|
|
// we need to decode (see encoding field) the value of 'content' from the response
|
|
match body.content_type {
|
|
ForgeContentsType::File => decode_config(body).map_err(OneOf::broaden),
|
|
_ => Err(OneOf::new(RepoConfigIsNotFile)),
|
|
}
|
|
}
|
|
None => {
|
|
error!(%status, "Failed to fetch repo config file");
|
|
Err(OneOf::new(RepoConfigUnknownError(status)))
|
|
}
|
|
}?;
|
|
let config = validate(details, config, net)
|
|
.await
|
|
.map_err(OneOf::broaden)?;
|
|
Ok(config)
|
|
}
|
|
|
|
fn decode_config(
|
|
body: ForgeContentsResponse,
|
|
) -> Result<RepoConfig, OneOf<(RepoConfigDecodeError, RepoConfigParseError)>> {
|
|
match body.encoding.as_str() {
|
|
"base64" => {
|
|
let decoded = base64::engine::general_purpose::STANDARD
|
|
.decode(body.content)
|
|
.map_err(|_| OneOf::new(RepoConfigDecodeError))?;
|
|
let decoded =
|
|
String::from_utf8(decoded).map_err(|_| OneOf::new(RepoConfigDecodeError))?;
|
|
let config = toml::from_str(&decoded).map_err(|_| OneOf::new(RepoConfigParseError))?;
|
|
Ok(config)
|
|
}
|
|
_ => Err(OneOf::new(RepoConfigDecodeError)),
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct ForgeContentsResponse {
|
|
#[serde(rename = "type")]
|
|
pub content_type: ForgeContentsType,
|
|
pub content: String,
|
|
pub encoding: String,
|
|
}
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
enum ForgeContentsType {
|
|
#[serde(rename = "file")]
|
|
File,
|
|
#[serde(rename = "dir")]
|
|
Dir,
|
|
#[serde(rename = "symlink")]
|
|
Symlink,
|
|
#[serde(rename = "submodule")]
|
|
Submodule,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct RepoConfigBranchNotFound(pub BranchName);
|
|
|
|
type RepoConfigValidateErrors = (RepoConfigBranchNotFound, RepoConfigUnknownError);
|
|
|
|
pub async fn validate(
|
|
details: &RepoDetails,
|
|
config: RepoConfig,
|
|
net: &Network,
|
|
) -> Result<RepoConfig, OneOf<RepoConfigValidateErrors>> {
|
|
let hostname = &details.forge.hostname;
|
|
let path = &details.repo;
|
|
use secrecy::ExposeSecret;
|
|
let token = details.forge.token.expose_secret();
|
|
let url = network::NetUrl::new(format!(
|
|
"https://{hostname}/api/v1/repos/{path}/branches?token={token}"
|
|
));
|
|
|
|
info!(%url, "Listing branches");
|
|
let request = network::NetRequest::new(
|
|
network::RequestMethod::Get,
|
|
url,
|
|
network::NetRequestHeaders::new(),
|
|
network::RequestBody::None,
|
|
network::ResponseType::Json,
|
|
None,
|
|
network::NetRequestLogging::None,
|
|
);
|
|
let result = net.get::<BranchList>(request).await;
|
|
let response = result.map_err(|e| {
|
|
error!(?e, "Failed to list branches");
|
|
OneOf::new(RepoConfigUnknownError(
|
|
network::StatusCode::INTERNAL_SERVER_ERROR,
|
|
))
|
|
})?;
|
|
let branches = response.response_body().unwrap_or_default();
|
|
if !branches
|
|
.iter()
|
|
.any(|branch| branch.name() == config.branches().main())
|
|
{
|
|
return Err(OneOf::new(RepoConfigBranchNotFound(
|
|
config.branches().main(),
|
|
)));
|
|
}
|
|
if !branches
|
|
.iter()
|
|
.any(|branch| branch.name() == config.branches().next())
|
|
{
|
|
return Err(OneOf::new(RepoConfigBranchNotFound(
|
|
config.branches().next(),
|
|
)));
|
|
}
|
|
if !branches
|
|
.iter()
|
|
.any(|branch| branch.name() == config.branches().dev())
|
|
{
|
|
return Err(OneOf::new(RepoConfigBranchNotFound(
|
|
config.branches().dev(),
|
|
)));
|
|
}
|
|
Ok(config)
|
|
}
|
|
|
|
type BranchList = Vec<Branch>;
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
struct Branch {
|
|
name: String,
|
|
}
|
|
impl Branch {
|
|
fn name(&self) -> BranchName {
|
|
BranchName(self.name.clone())
|
|
}
|
|
}
|