feat(server/gitforge): replace git abstraction
This commit is contained in:
parent
968f9dd73d
commit
adb44d18c9
32 changed files with 1066 additions and 661 deletions
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||
[features]
|
||||
default = ["forgejo"]
|
||||
forgejo = []
|
||||
github = []
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
@ -23,6 +24,7 @@ base64 = "0.22"
|
|||
|
||||
# git
|
||||
gix = "0.62"
|
||||
async-trait = "0.1"
|
||||
|
||||
# fs/network
|
||||
kxio = { version = "1.0", features = [
|
||||
|
@ -59,7 +61,7 @@ tokio = { version = "1.37", features = ["full"] }
|
|||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
# assert2 = "0.3"
|
||||
assert2 = "0.3"
|
||||
test-log = "0.2"
|
||||
anyhow = "1.0"
|
||||
|
||||
|
|
|
@ -4,8 +4,6 @@ mod server;
|
|||
use clap::Parser;
|
||||
use kxio::{filesystem, network::Network};
|
||||
|
||||
use crate::server::git::Git;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
|
||||
struct Commands {
|
||||
|
@ -28,7 +26,6 @@ enum Server {
|
|||
async fn main() {
|
||||
let fs = filesystem::FileSystem::new_real(None);
|
||||
let net = Network::new_real();
|
||||
let git = Git::new_real();
|
||||
let commands = Commands::parse();
|
||||
|
||||
match commands.command {
|
||||
|
@ -40,7 +37,7 @@ async fn main() {
|
|||
server::init(fs);
|
||||
}
|
||||
Server::Start => {
|
||||
server::start(fs, net, git).await;
|
||||
server::start(fs, net).await;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,147 +1,17 @@
|
|||
use actix::prelude::*;
|
||||
|
||||
use kxio::network::{self, Network};
|
||||
use tracing::{error, info, warn};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::server::{
|
||||
config::{self, RepoConfig, RepoDetails},
|
||||
forge::{self, CommitHistories},
|
||||
git::Git,
|
||||
types::ResetForce,
|
||||
config, forge,
|
||||
gitforge::{self, Force, Forge},
|
||||
};
|
||||
|
||||
use super::{RepoActor, StartMonitoring};
|
||||
|
||||
#[tracing::instrument(fields(forge_name = %repo_details.forge.name, repo_name = %repo_details.name), skip_all)]
|
||||
pub async fn validate_positions(
|
||||
repo_details: config::RepoDetails,
|
||||
config: config::RepoConfig,
|
||||
addr: Addr<RepoActor>,
|
||||
git: Git,
|
||||
net: network::Network,
|
||||
) {
|
||||
// Collect Commit Histories for `main`, `next` and `dev` branches
|
||||
let commit_histories = get_commit_histories(&repo_details, &config, &net).await;
|
||||
let commit_histories = match commit_histories {
|
||||
Ok(commit_histories) => commit_histories,
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to get commit histories");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Validations
|
||||
let Some(main) = commit_histories.main.first().cloned() else {
|
||||
warn!("No commits on main branch '{}'", config.branches().main());
|
||||
return;
|
||||
};
|
||||
// verify that next is an ancestor of dev, and force update to it main if it isn't
|
||||
let Some(next) = commit_histories.next.first().cloned() else {
|
||||
warn!("No commits on next branch '{}", config.branches().next());
|
||||
return;
|
||||
};
|
||||
let next_is_ancestor_of_dev = commit_histories.dev.iter().any(|dev| dev == &next);
|
||||
if !next_is_ancestor_of_dev {
|
||||
info!("Next is not an ancestor of dev - resetting next to main");
|
||||
reset_next_to_main(next, main, &repo_details, &config, &git);
|
||||
return;
|
||||
}
|
||||
|
||||
let next_commits = commit_histories
|
||||
.next
|
||||
.into_iter()
|
||||
.take(2)
|
||||
.collect::<Vec<_>>();
|
||||
if !next_commits.contains(&main) {
|
||||
warn!(
|
||||
"Main branch '{}' is not on the same commit as next branch '{}', or it's parent - resetting next to main",
|
||||
config.branches().main(),
|
||||
config.branches().next()
|
||||
);
|
||||
reset_next_to_main(next, main, &repo_details, &config, &git);
|
||||
return;
|
||||
}
|
||||
let Some(next) = next_commits.first().cloned() else {
|
||||
warn!("No commits on next branch '{}'", config.branches().next());
|
||||
return;
|
||||
};
|
||||
let dev_has_next = commit_histories
|
||||
.dev
|
||||
.iter()
|
||||
.any(|commit| commit == &next_commits[0]);
|
||||
if !dev_has_next {
|
||||
warn!(
|
||||
"Dev branch '{}' is not based on next branch '{}' - next branch will be updated shortly",
|
||||
config.branches().dev(),
|
||||
config.branches().next()
|
||||
);
|
||||
return; // dev is not based on next
|
||||
}
|
||||
let dev_has_main = commit_histories.dev.iter().any(|commit| commit == &main);
|
||||
if !dev_has_main {
|
||||
warn!(
|
||||
"Dev branch '{}' is not based on main branch '{}' - user should rebase onto main branch '{}'",
|
||||
config.branches().dev(),
|
||||
config.branches().main(),
|
||||
config.branches().main(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let Some(dev) = commit_histories.dev.first().cloned() else {
|
||||
warn!("No commits on dev branch '{}'", config.branches().dev());
|
||||
return;
|
||||
};
|
||||
addr.do_send(StartMonitoring {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history: commit_histories.dev,
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_commit_histories(
|
||||
repo_details: &RepoDetails,
|
||||
config: &RepoConfig,
|
||||
net: &Network,
|
||||
) -> Result<CommitHistories, network::NetworkError> {
|
||||
match repo_details.forge.forge_type {
|
||||
#[cfg(feature = "forgejo")]
|
||||
config::ForgeType::ForgeJo => {
|
||||
forge::forgejo::get_commit_histories(repo_details, config, net).await
|
||||
}
|
||||
#[cfg(test)]
|
||||
config::ForgeType::MockForge => {
|
||||
forge::mock::get_commit_histories(repo_details, config, net).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_next_to_main(
|
||||
next: forge::Commit,
|
||||
main: forge::Commit,
|
||||
repo_details: &RepoDetails,
|
||||
repo_config: &RepoConfig,
|
||||
git: &Git,
|
||||
) {
|
||||
let reset = git.reset(
|
||||
&repo_config.branches().next(),
|
||||
main.into(),
|
||||
ResetForce::Force(next.into()),
|
||||
repo_details,
|
||||
);
|
||||
if let Err(err) = reset {
|
||||
warn!(?err, "Failed");
|
||||
}
|
||||
}
|
||||
|
||||
// advance next to the next commit towards the head of the dev branch
|
||||
#[tracing::instrument(fields(next), skip_all)]
|
||||
pub async fn advance_next(
|
||||
next: forge::Commit,
|
||||
dev_commit_history: Vec<forge::Commit>,
|
||||
repo_details: config::RepoDetails,
|
||||
repo_config: config::RepoConfig,
|
||||
git: Git,
|
||||
forge: Forge,
|
||||
) {
|
||||
let next_commit = find_next_commit_on_dev(next, dev_commit_history);
|
||||
let Some(commit) = next_commit else {
|
||||
|
@ -153,14 +23,9 @@ pub async fn advance_next(
|
|||
return;
|
||||
}
|
||||
info!("Advancing next to commit '{}'", commit);
|
||||
if let Err(err) = git.reset(
|
||||
&repo_config.branches().next(),
|
||||
commit.into(),
|
||||
ResetForce::None,
|
||||
&repo_details,
|
||||
) {
|
||||
if let Err(err) = forge.branch_reset(repo_config.branches().next(), commit.into(), Force::No) {
|
||||
warn!(?err, "Failed")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
@ -199,17 +64,11 @@ fn find_next_commit_on_dev(
|
|||
#[tracing::instrument(fields(next), skip_all)]
|
||||
pub async fn advance_main(
|
||||
next: forge::Commit,
|
||||
repo_details: config::RepoDetails,
|
||||
repo_config: config::RepoConfig,
|
||||
git: Git,
|
||||
forge: gitforge::Forge,
|
||||
) {
|
||||
info!("Advancing main to next");
|
||||
if let Err(err) = git.reset(
|
||||
&repo_config.branches().main(),
|
||||
next.into(),
|
||||
ResetForce::None,
|
||||
&repo_details,
|
||||
) {
|
||||
if let Err(err) = forge.branch_reset(repo_config.branches().main(), next.into(), Force::No) {
|
||||
warn!(?err, "Failed")
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
use actix::prelude::*;
|
||||
use kxio::network::Network;
|
||||
use tracing::error;
|
||||
|
||||
use crate::server::{
|
||||
config::{ForgeType, RepoDetails},
|
||||
forge,
|
||||
forge, gitforge,
|
||||
};
|
||||
|
||||
use super::{LoadedConfig, RepoActor};
|
||||
|
||||
pub async fn load(details: RepoDetails, addr: Addr<RepoActor>, net: Network) {
|
||||
pub async fn load(details: RepoDetails, addr: Addr<RepoActor>, forge: gitforge::Forge) {
|
||||
let config = match details.config {
|
||||
Some(config) => config,
|
||||
None => {
|
||||
let config = match details.forge.forge_type {
|
||||
#[cfg(feature = "forgejo")]
|
||||
ForgeType::ForgeJo => forge::forgejo::config::load(&details, &net).await,
|
||||
ForgeType::ForgeJo => forge::forgejo::config::load(&details, &forge).await,
|
||||
#[cfg(test)]
|
||||
ForgeType::MockForge => forge::mock::config::load(&details, &net).await,
|
||||
ForgeType::MockForge => forge::mock::config::load(&details, &forge).await,
|
||||
};
|
||||
match config {
|
||||
Ok(config) => config,
|
||||
|
|
|
@ -10,8 +10,7 @@ use tracing::{info, warn};
|
|||
use crate::server::{
|
||||
actors::repo::webhook::WebhookAuth,
|
||||
config::{RepoConfig, RepoDetails, Webhook},
|
||||
forge,
|
||||
git::Git,
|
||||
forge, gitforge,
|
||||
};
|
||||
|
||||
use self::webhook::WebhookId;
|
||||
|
@ -25,15 +24,18 @@ pub struct RepoActor {
|
|||
last_next_commit: Option<forge::Commit>,
|
||||
last_dev_commit: Option<forge::Commit>,
|
||||
net: Network,
|
||||
git: Git,
|
||||
forge: gitforge::Forge,
|
||||
}
|
||||
impl RepoActor {
|
||||
pub(crate) const fn new(
|
||||
details: RepoDetails,
|
||||
webhook: Webhook,
|
||||
net: Network,
|
||||
git: Git,
|
||||
) -> Self {
|
||||
pub(crate) fn new(details: RepoDetails, webhook: Webhook, net: Network) -> Self {
|
||||
let forge = match details.forge.forge_type {
|
||||
#[cfg(feature = "forgejo")]
|
||||
crate::server::config::ForgeType::ForgeJo => {
|
||||
gitforge::Forge::new_forgejo(details.clone(), net.clone())
|
||||
}
|
||||
#[cfg(test)]
|
||||
crate::server::config::ForgeType::MockForge => gitforge::Forge::new_mock(),
|
||||
};
|
||||
Self {
|
||||
details,
|
||||
webhook,
|
||||
|
@ -43,7 +45,7 @@ impl RepoActor {
|
|||
last_next_commit: None,
|
||||
last_dev_commit: None,
|
||||
net,
|
||||
git,
|
||||
forge,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,8 +75,10 @@ impl Handler<StartRepo> for RepoActor {
|
|||
info!(%self.details, "Starting Repo");
|
||||
let details = self.details.clone();
|
||||
let addr = ctx.address();
|
||||
let net = self.net.clone();
|
||||
config::load(details, addr, net).into_actor(self).wait(ctx);
|
||||
let forge = self.forge.clone();
|
||||
config::load(details, addr, forge)
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,11 +112,9 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
type Result = ();
|
||||
fn handle(&mut self, _msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||
if let Some(repo_config) = self.details.config.clone() {
|
||||
let repo_details = self.details.clone();
|
||||
let forge = self.forge.clone();
|
||||
let addr = ctx.address();
|
||||
let net = self.net.clone();
|
||||
let git = self.git.clone();
|
||||
branch::validate_positions(repo_details, repo_config, addr, git, net)
|
||||
async move { forge.branches_validate_positions(repo_config, addr).await }
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
|
@ -143,22 +145,16 @@ impl Handler<StartMonitoring> for RepoActor {
|
|||
let webhook = self.webhook.clone();
|
||||
let addr = ctx.address();
|
||||
let net = self.net.clone();
|
||||
let git = self.git.clone();
|
||||
let forge = self.forge.clone();
|
||||
|
||||
if next_ahead_of_main {
|
||||
status::check_next(msg.next, repo_details, addr, net)
|
||||
status::check_next(msg.next, addr, forge)
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
} else if dev_ahead_of_next {
|
||||
branch::advance_next(
|
||||
msg.next,
|
||||
msg.dev_commit_history,
|
||||
repo_details,
|
||||
repo_config,
|
||||
git,
|
||||
)
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
branch::advance_next(msg.next, msg.dev_commit_history, repo_config, forge)
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
} else if self.webhook_id.is_none() {
|
||||
webhook::register(repo_details, webhook, addr, net)
|
||||
.into_actor(self)
|
||||
|
@ -184,13 +180,12 @@ pub struct AdvanceMainTo(pub forge::Commit);
|
|||
impl Handler<AdvanceMainTo> for RepoActor {
|
||||
type Result = ();
|
||||
fn handle(&mut self, msg: AdvanceMainTo, ctx: &mut Self::Context) -> Self::Result {
|
||||
let repo_details = self.details.clone();
|
||||
let Some(repo_config) = self.details.config.clone() else {
|
||||
warn!("No config loaded");
|
||||
return;
|
||||
};
|
||||
let git = self.git.clone();
|
||||
branch::advance_main(msg.0, repo_details, repo_config, git)
|
||||
let forge = self.forge.clone();
|
||||
branch::advance_main(msg.0, repo_config, forge)
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
|
|
|
@ -2,49 +2,24 @@ use actix::prelude::*;
|
|||
use gix::trace::warn;
|
||||
use tracing::info;
|
||||
|
||||
use crate::server::{
|
||||
actors::repo::ValidateRepo,
|
||||
config::{self, ForgeType},
|
||||
forge,
|
||||
};
|
||||
use crate::server::{actors::repo::ValidateRepo, forge, gitforge};
|
||||
|
||||
use super::AdvanceMainTo;
|
||||
|
||||
pub async fn check_next(
|
||||
next: forge::Commit,
|
||||
repo_details: config::RepoDetails,
|
||||
addr: Addr<super::RepoActor>,
|
||||
net: kxio::network::Network,
|
||||
) {
|
||||
pub async fn check_next(next: forge::Commit, addr: Addr<super::RepoActor>, forge: gitforge::Forge) {
|
||||
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
||||
let status = match repo_details.forge.forge_type {
|
||||
#[cfg(feature = "forgejo")]
|
||||
ForgeType::ForgeJo => {
|
||||
forge::forgejo::get_commit_status(next.clone(), &repo_details, net).await
|
||||
}
|
||||
#[cfg(test)]
|
||||
ForgeType::MockForge => {
|
||||
forge::mock::get_commit_status(next.clone(), &repo_details, &net).await
|
||||
}
|
||||
};
|
||||
let status = forge.commit_status(&next).await;
|
||||
info!(?status, "Checking next branch");
|
||||
match status {
|
||||
Status::Pass => {
|
||||
gitforge::CommitStatus::Pass => {
|
||||
addr.do_send(AdvanceMainTo(next));
|
||||
}
|
||||
Status::Pending => {
|
||||
gitforge::CommitStatus::Pending => {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
|
||||
addr.do_send(ValidateRepo);
|
||||
}
|
||||
Status::Fail => {
|
||||
gitforge::CommitStatus::Fail => {
|
||||
warn!("Checks have failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Status {
|
||||
Pass,
|
||||
Fail,
|
||||
Pending,
|
||||
}
|
||||
|
|
|
@ -61,9 +61,13 @@ pub struct RepoConfig {
|
|||
branches: RepoBranches,
|
||||
}
|
||||
impl RepoConfig {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn load(toml: &str) -> Result<Self, OneOf<(toml::de::Error,)>> {
|
||||
toml::from_str(toml).map_err(OneOf::new)
|
||||
#[cfg(test)]
|
||||
pub const fn new(branches: RepoBranches) -> Self {
|
||||
Self { branches }
|
||||
}
|
||||
// #[cfg(test)]
|
||||
pub fn load(toml: &str) -> Result<Self, toml::de::Error> {
|
||||
toml::from_str(toml)
|
||||
}
|
||||
|
||||
pub const fn branches(&self) -> &RepoBranches {
|
||||
|
@ -84,6 +88,14 @@ pub struct RepoBranches {
|
|||
dev: String,
|
||||
}
|
||||
impl RepoBranches {
|
||||
#[cfg(test)]
|
||||
pub fn new(main: impl Into<String>, next: impl Into<String>, dev: impl Into<String>) -> Self {
|
||||
Self {
|
||||
main: main.into(),
|
||||
next: next.into(),
|
||||
dev: dev.into(),
|
||||
}
|
||||
}
|
||||
pub fn main(&self) -> BranchName {
|
||||
BranchName(self.main.clone())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use base64::Engine;
|
||||
use kxio::network::{self, Network};
|
||||
use kxio::network;
|
||||
use terrors::OneOf;
|
||||
use tracing::{error, info};
|
||||
use tracing::error;
|
||||
|
||||
use crate::server::config::{BranchName, RepoConfig, RepoDetails};
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoConfig, RepoDetails},
|
||||
gitforge::{self, ForgeFileError},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RepoConfigFileNotFound;
|
||||
|
@ -16,177 +18,56 @@ 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)
|
||||
forge: &gitforge::Forge,
|
||||
) -> Result<RepoConfig, OneOf<(ForgeFileError, toml::de::Error, RepoConfigValidationErrors)>> {
|
||||
let contents = forge
|
||||
.file_contents_get(&details.branch, ".git-next.toml")
|
||||
.await
|
||||
.map_err(OneOf::broaden)?;
|
||||
.map_err(OneOf::new)?;
|
||||
let config = RepoConfig::load(&contents).map_err(OneOf::new)?;
|
||||
let config = validate(config, forge).await.map_err(OneOf::new)?;
|
||||
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 enum RepoConfigValidationErrors {
|
||||
Forge(gitforge::ForgeBranchError),
|
||||
BranchNotFound(BranchName),
|
||||
}
|
||||
|
||||
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| {
|
||||
forge: &gitforge::Forge,
|
||||
) -> Result<RepoConfig, RepoConfigValidationErrors> {
|
||||
let branches = forge.branches_get_all().await.map_err(|e| {
|
||||
error!(?e, "Failed to list branches");
|
||||
OneOf::new(RepoConfigUnknownError(
|
||||
network::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
RepoConfigValidationErrors::Forge(e)
|
||||
})?;
|
||||
let branches = response.response_body().unwrap_or_default();
|
||||
if !branches
|
||||
.iter()
|
||||
.any(|branch| branch.name() == config.branches().main())
|
||||
.any(|branch| branch.name() == &config.branches().main())
|
||||
{
|
||||
return Err(OneOf::new(RepoConfigBranchNotFound(
|
||||
return Err(RepoConfigValidationErrors::BranchNotFound(
|
||||
config.branches().main(),
|
||||
)));
|
||||
));
|
||||
}
|
||||
if !branches
|
||||
.iter()
|
||||
.any(|branch| branch.name() == config.branches().next())
|
||||
.any(|branch| branch.name() == &config.branches().next())
|
||||
{
|
||||
return Err(OneOf::new(RepoConfigBranchNotFound(
|
||||
return Err(RepoConfigValidationErrors::BranchNotFound(
|
||||
config.branches().next(),
|
||||
)));
|
||||
));
|
||||
}
|
||||
if !branches
|
||||
.iter()
|
||||
.any(|branch| branch.name() == config.branches().dev())
|
||||
.any(|branch| branch.name() == &config.branches().dev())
|
||||
{
|
||||
return Err(OneOf::new(RepoConfigBranchNotFound(
|
||||
return Err(RepoConfigValidationErrors::BranchNotFound(
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,10 @@
|
|||
use kxio::network;
|
||||
use secrecy::ExposeSecret;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::server;
|
||||
use crate::server::{actors::repo::status::Status, config::BranchName, forge};
|
||||
|
||||
use super::CommitHistories;
|
||||
use crate::server::{self, config::BranchName, forge};
|
||||
|
||||
pub mod config;
|
||||
|
||||
pub async fn get_commit_histories(
|
||||
repo_details: &server::config::RepoDetails,
|
||||
config: &server::config::RepoConfig,
|
||||
net: &kxio::network::Network,
|
||||
) -> Result<CommitHistories, network::NetworkError> {
|
||||
let main = (get_commit_history(repo_details, &config.branches().main(), vec![], net).await)?;
|
||||
let main_head = main[0].clone();
|
||||
let next = (get_commit_history(
|
||||
repo_details,
|
||||
&config.branches().next(),
|
||||
vec![main_head.clone()],
|
||||
net,
|
||||
)
|
||||
.await)?;
|
||||
let next_head = next[0].clone();
|
||||
let dev = (get_commit_history(
|
||||
repo_details,
|
||||
&config.branches().dev(),
|
||||
vec![next_head, main_head],
|
||||
net,
|
||||
)
|
||||
.await)?;
|
||||
debug!(
|
||||
main = main.len(),
|
||||
next = next.len(),
|
||||
dev = dev.len(),
|
||||
"Commit histories"
|
||||
);
|
||||
let histories = CommitHistories { main, next, dev };
|
||||
Ok(histories)
|
||||
}
|
||||
|
||||
#[tracing::instrument(fields(%branch_name),skip_all)]
|
||||
async fn get_commit_history(
|
||||
repo_details: &server::config::RepoDetails,
|
||||
|
@ -114,51 +78,6 @@ impl From<Commit> for forge::Commit {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn get_commit_status(
|
||||
next: forge::Commit,
|
||||
repo_details: &crate::server::config::RepoDetails,
|
||||
net: network::Network,
|
||||
) -> Status {
|
||||
let hostname = &repo_details.forge.hostname;
|
||||
let path = &repo_details.repo;
|
||||
let token = repo_details.forge.token.expose_secret();
|
||||
let url = network::NetUrl::new(format!(
|
||||
"https://{hostname}/api/v1/repos/{path}/commits/{next}/status?token={token}"
|
||||
));
|
||||
|
||||
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::<CombinedStatus>(request).await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
match response.response_body() {
|
||||
Some(status) => match status.state {
|
||||
CommitStatusState::Success => Status::Pass,
|
||||
CommitStatusState::Pending => Status::Pending,
|
||||
CommitStatusState::Failure => Status::Fail,
|
||||
CommitStatusState::Error => Status::Fail,
|
||||
CommitStatusState::Blank => Status::Pending,
|
||||
},
|
||||
None => {
|
||||
warn!("No status found for commit");
|
||||
Status::Pending // assume issue is transient and allow retry
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e, "Failed to get commit status");
|
||||
Status::Pending // assume issue is transient and allow retry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct CombinedStatus {
|
||||
pub state: CommitStatusState,
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
use terrors::OneOf;
|
||||
|
||||
use crate::server::{
|
||||
config::RepoConfig,
|
||||
forge::forgejo::config::RepoConfigValidationErrors,
|
||||
gitforge::{self, ForgeFileError},
|
||||
};
|
||||
|
||||
pub async fn load(
|
||||
_details: &crate::server::config::RepoDetails,
|
||||
_net: &kxio::network::Network,
|
||||
) -> Result<
|
||||
crate::server::config::RepoConfig,
|
||||
terrors::OneOf<(
|
||||
crate::server::forge::forgejo::config::RepoConfigFileNotFound,
|
||||
crate::server::forge::forgejo::config::RepoConfigIsNotFile,
|
||||
crate::server::forge::forgejo::config::RepoConfigDecodeError,
|
||||
crate::server::forge::forgejo::config::RepoConfigParseError,
|
||||
crate::server::forge::forgejo::config::RepoConfigUnknownError,
|
||||
crate::server::forge::forgejo::config::RepoConfigBranchNotFound,
|
||||
)>,
|
||||
> {
|
||||
_forge: &gitforge::Forge,
|
||||
) -> Result<RepoConfig, OneOf<(ForgeFileError, toml::de::Error, RepoConfigValidationErrors)>> {
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -1,24 +1 @@
|
|||
use kxio::network::{Network, NetworkError};
|
||||
|
||||
use crate::server::{
|
||||
config::{RepoConfig, RepoDetails},
|
||||
forge::CommitHistories,
|
||||
};
|
||||
|
||||
pub mod config;
|
||||
|
||||
pub async fn get_commit_histories(
|
||||
_repo_details: &RepoDetails,
|
||||
_config: &RepoConfig,
|
||||
_net: &Network,
|
||||
) -> Result<CommitHistories, NetworkError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_commit_status(
|
||||
_clone: super::Commit,
|
||||
_repo_details: &RepoDetails,
|
||||
_net: &Network,
|
||||
) -> crate::server::actors::repo::status::Status {
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoDetails},
|
||||
git::GitResetResult,
|
||||
types::{GitRef, ResetForce},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MockGit {
|
||||
reset: HashMap<(BranchName, GitRef), GitResetResult>,
|
||||
}
|
||||
impl MockGit {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
reset: HashMap::new(),
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)] // TODO: (#38) Add tests
|
||||
pub const fn into_git(self) -> super::Git {
|
||||
super::Git::Mock(self)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // TODO: (#38) Add tests
|
||||
pub fn expect_reset_result(
|
||||
&mut self,
|
||||
branch_name: &BranchName,
|
||||
gitref: &GitRef,
|
||||
result: GitResetResult,
|
||||
) {
|
||||
self.reset
|
||||
.insert((branch_name.clone(), gitref.clone()), result);
|
||||
}
|
||||
}
|
||||
impl super::GitLike for MockGit {
|
||||
fn reset(
|
||||
&self,
|
||||
branch: &BranchName,
|
||||
gitref: GitRef,
|
||||
_reset_force: ResetForce,
|
||||
_repo_details: &RepoDetails,
|
||||
) -> GitResetResult {
|
||||
self.reset
|
||||
.get(&(branch.clone(), gitref.clone()))
|
||||
.map_or_else(|| panic!("unexpected: {} to {}", branch, gitref), |r| *r)
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoDetails},
|
||||
git::{mock::MockGit, real::RealGit},
|
||||
types::{GitRef, ResetForce},
|
||||
};
|
||||
|
||||
mod mock;
|
||||
mod real;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Git {
|
||||
Real(RealGit),
|
||||
#[allow(dead_code)] // TODO: (#38) Add tests
|
||||
Mock(MockGit),
|
||||
}
|
||||
impl Git {
|
||||
pub const fn new_real() -> Self {
|
||||
Self::Real(RealGit::new())
|
||||
}
|
||||
#[allow(dead_code)] // TODO: (#38) Add tests
|
||||
pub fn new_mock() -> MockGit {
|
||||
MockGit::new()
|
||||
}
|
||||
}
|
||||
impl Deref for Git {
|
||||
type Target = dyn GitLike;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Real(git) => git,
|
||||
Self::Mock(git) => git,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GitLike: Sync + Send {
|
||||
fn reset(
|
||||
&self,
|
||||
branch: &BranchName,
|
||||
gitref: GitRef,
|
||||
reset_force: ResetForce,
|
||||
repo_details: &RepoDetails,
|
||||
) -> GitResetResult;
|
||||
}
|
||||
|
||||
pub type GitResetResult = Result<(), ()>;
|
|
@ -1,57 +0,0 @@
|
|||
use secrecy::ExposeSecret;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoDetails},
|
||||
git::GitResetResult,
|
||||
types::{GitRef, ResetForce},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealGit;
|
||||
impl RealGit {
|
||||
pub(super) const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
impl super::GitLike for RealGit {
|
||||
fn reset(
|
||||
&self,
|
||||
branch: &BranchName,
|
||||
gitref: GitRef,
|
||||
reset_force: ResetForce,
|
||||
repo_details: &RepoDetails,
|
||||
) -> GitResetResult {
|
||||
let user = &repo_details.forge.user;
|
||||
let hostname = &repo_details.forge.hostname;
|
||||
let path = &repo_details.repo;
|
||||
let token = &repo_details.forge.token.expose_secret();
|
||||
let origin = format!("https://{user}:{token}@{hostname}/{path}.git");
|
||||
let force = match reset_force {
|
||||
ResetForce::None => "".to_string(),
|
||||
ResetForce::Force(old_ref) => format!("--force-with-lease={branch}:{old_ref}"),
|
||||
};
|
||||
// INFO: never log the command as it contains the API token
|
||||
let command = format!("/usr/bin/git push {origin} {gitref}:{branch} {force}");
|
||||
drop(origin);
|
||||
info!("Resetting {branch} to {gitref}");
|
||||
match gix::command::prepare(command)
|
||||
.with_shell_allow_argument_splitting()
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(mut child) => match child.wait() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
warn!(?err, "Failed (wait)");
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(?err, "Failed (spawn)");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
src/server/gitforge/errors.rs
Normal file
41
src/server/gitforge/errors.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use crate::server::config::BranchName;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ForgeFileError {
|
||||
NotFound(String),
|
||||
ParseContent,
|
||||
DecodeFromBase64,
|
||||
DecodeFromUtf8,
|
||||
UnknownEncoding(String),
|
||||
NotFile(String),
|
||||
Unknown(String),
|
||||
}
|
||||
impl std::error::Error for ForgeFileError {}
|
||||
impl std::fmt::Display for ForgeFileError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound(file_path) => write!(f, "File not found: {file_path}"),
|
||||
Self::NotFile(file_path) => write!(f, "Not a file: {file_path}"),
|
||||
Self::DecodeFromBase64 => write!(f, "Unable to decode from base64"),
|
||||
Self::DecodeFromUtf8 => write!(f, "Unable to decode from UTF-8"),
|
||||
Self::UnknownEncoding(encoding) => write!(f, "Unknown file encoding: {encoding}"),
|
||||
Self::ParseContent => write!(f, "Unable to parse file contents"),
|
||||
Self::Unknown(status) => write!(f, "Unknown error (status: {status})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ForgeBranchError {
|
||||
NotFound(BranchName),
|
||||
NoneFound,
|
||||
}
|
||||
impl std::error::Error for ForgeBranchError {}
|
||||
impl std::fmt::Display for ForgeBranchError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound(branch_name) => write!(f, "Branch not found: {branch_name}"),
|
||||
Self::NoneFound => write!(f, "Unable to find any branches"),
|
||||
}
|
||||
}
|
||||
}
|
54
src/server/gitforge/forgejo/branch/get_all.rs
Normal file
54
src/server/gitforge/forgejo/branch/get_all.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use kxio::network::{self, Network};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoDetails},
|
||||
gitforge::{self, ForgeBranchError},
|
||||
};
|
||||
|
||||
pub async fn get_all(
|
||||
repo_details: &RepoDetails,
|
||||
net: &Network,
|
||||
) -> Result<Vec<gitforge::Branch>, ForgeBranchError> {
|
||||
let hostname = &repo_details.forge.hostname;
|
||||
let path = &repo_details.repo;
|
||||
use secrecy::ExposeSecret;
|
||||
let token = repo_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::<Vec<Branch>>(request).await;
|
||||
let response = result.map_err(|e| {
|
||||
error!(?e, "Failed to list branches");
|
||||
ForgeBranchError::NoneFound // BranchListNotAvailable
|
||||
})?;
|
||||
let branches = response
|
||||
.response_body()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|b| b.name())
|
||||
.map(gitforge::Branch)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct Branch {
|
||||
name: String,
|
||||
}
|
||||
impl Branch {
|
||||
fn name(&self) -> BranchName {
|
||||
BranchName(self.name.clone())
|
||||
}
|
||||
}
|
7
src/server/gitforge/forgejo/branch/mod.rs
Normal file
7
src/server/gitforge/forgejo/branch/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod get_all;
|
||||
mod reset;
|
||||
mod validate_positions;
|
||||
|
||||
pub use get_all::get_all;
|
||||
pub use reset::reset;
|
||||
pub use validate_positions::validate_positions;
|
48
src/server/gitforge/forgejo/branch/reset.rs
Normal file
48
src/server/gitforge/forgejo/branch/reset.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use tracing::{info, warn};
|
||||
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoDetails},
|
||||
gitforge::{BranchResetResult, Force},
|
||||
types::GitRef,
|
||||
};
|
||||
|
||||
pub fn reset(
|
||||
repo_details: &RepoDetails,
|
||||
branch_name: BranchName,
|
||||
to_commit: GitRef,
|
||||
force: Force,
|
||||
) -> BranchResetResult {
|
||||
let user = &repo_details.forge.user;
|
||||
let hostname = &repo_details.forge.hostname;
|
||||
let path = &repo_details.repo;
|
||||
use secrecy::ExposeSecret;
|
||||
let expose_secret = &repo_details.forge.token;
|
||||
let token = expose_secret.expose_secret();
|
||||
let origin = format!("https://{user}:{token}@{hostname}/{path}.git");
|
||||
let force = match force {
|
||||
Force::No => "".to_string(),
|
||||
Force::From(old_ref) => format!("--force-with-lease={branch_name}:{old_ref}"),
|
||||
};
|
||||
// INFO: never log the command as it contains the API token within the 'origin'
|
||||
let command = format!("/usr/bin/git push {origin} {to_commit}:{branch_name} {force}");
|
||||
drop(origin);
|
||||
info!("Resetting {branch_name} to {to_commit}");
|
||||
match gix::command::prepare(command)
|
||||
.with_shell_allow_argument_splitting()
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(mut child) => match child.wait() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
warn!(?err, "Failed (wait)");
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(?err, "Failed (spawn)");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
229
src/server/gitforge/forgejo/branch/validate_positions.rs
Normal file
229
src/server/gitforge/forgejo/branch/validate_positions.rs
Normal file
|
@ -0,0 +1,229 @@
|
|||
use actix::prelude::*;
|
||||
|
||||
use kxio::network;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::server::{
|
||||
self,
|
||||
actors::repo::{RepoActor, StartMonitoring},
|
||||
config::{BranchName, RepoConfig, RepoDetails},
|
||||
forge,
|
||||
gitforge::{forgejo::ForgeJoEnv, Force, ForgeLike},
|
||||
};
|
||||
|
||||
pub async fn validate_positions(
|
||||
forge: &ForgeJoEnv,
|
||||
repo_config: RepoConfig,
|
||||
addr: Addr<RepoActor>,
|
||||
) {
|
||||
let repo_details = &forge.repo_details;
|
||||
// Collect Commit Histories for `main`, `next` and `dev` branches
|
||||
let commit_histories = get_commit_histories(repo_details, &repo_config, &forge.net).await;
|
||||
let commit_histories = match commit_histories {
|
||||
Ok(commit_histories) => commit_histories,
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to get commit histories");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Validations
|
||||
let Some(main) = commit_histories.main.first().cloned() else {
|
||||
warn!(
|
||||
"No commits on main branch '{}'",
|
||||
repo_config.branches().main()
|
||||
);
|
||||
return;
|
||||
};
|
||||
// verify that next is an ancestor of dev, and force update to it main if it isn't
|
||||
let Some(next) = commit_histories.next.first().cloned() else {
|
||||
warn!(
|
||||
"No commits on next branch '{}",
|
||||
repo_config.branches().next()
|
||||
);
|
||||
return;
|
||||
};
|
||||
let next_is_ancestor_of_dev = commit_histories.dev.iter().any(|dev| dev == &next);
|
||||
if !next_is_ancestor_of_dev {
|
||||
info!("Next is not an ancestor of dev - resetting next to main");
|
||||
if let Err(err) = forge.branch_reset(
|
||||
repo_config.branches().next(),
|
||||
main.into(),
|
||||
Force::From(next.into()),
|
||||
) {
|
||||
warn!(?err, "Failed to reset next to main");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let next_commits = commit_histories
|
||||
.next
|
||||
.into_iter()
|
||||
.take(2)
|
||||
.collect::<Vec<_>>();
|
||||
if !next_commits.contains(&main) {
|
||||
warn!(
|
||||
"Main branch '{}' is not on the same commit as next branch '{}', or it's parent - resetting next to main",
|
||||
repo_config.branches().main(),
|
||||
repo_config.branches().next()
|
||||
);
|
||||
if let Err(err) = forge.branch_reset(
|
||||
repo_config.branches().next(),
|
||||
main.into(),
|
||||
Force::From(next.into()),
|
||||
) {
|
||||
warn!(?err, "Failed to reset next to main");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(next) = next_commits.first().cloned() else {
|
||||
warn!(
|
||||
"No commits on next branch '{}'",
|
||||
repo_config.branches().next()
|
||||
);
|
||||
return;
|
||||
};
|
||||
let dev_has_next = commit_histories
|
||||
.dev
|
||||
.iter()
|
||||
.any(|commit| commit == &next_commits[0]);
|
||||
if !dev_has_next {
|
||||
warn!(
|
||||
"Dev branch '{}' is not based on next branch '{}' - next branch will be updated shortly",
|
||||
repo_config.branches().dev(),
|
||||
repo_config.branches().next()
|
||||
);
|
||||
return; // dev is not based on next
|
||||
}
|
||||
let dev_has_main = commit_histories.dev.iter().any(|commit| commit == &main);
|
||||
if !dev_has_main {
|
||||
warn!(
|
||||
"Dev branch '{}' is not based on main branch '{}' - user should rebase onto main branch '{}'",
|
||||
repo_config.branches().dev(),
|
||||
repo_config.branches().main(),
|
||||
repo_config.branches().main(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let Some(dev) = commit_histories.dev.first().cloned() else {
|
||||
warn!(
|
||||
"No commits on dev branch '{}'",
|
||||
repo_config.branches().dev()
|
||||
);
|
||||
return;
|
||||
};
|
||||
addr.do_send(StartMonitoring {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history: commit_histories.dev,
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_commit_histories(
|
||||
repo_details: &RepoDetails,
|
||||
repo_config: &RepoConfig,
|
||||
net: &network::Network,
|
||||
) -> Result<forge::CommitHistories, network::NetworkError> {
|
||||
let main =
|
||||
(get_commit_history(repo_details, &repo_config.branches().main(), vec![], net).await)?;
|
||||
let main_head = main[0].clone();
|
||||
let next = (get_commit_history(
|
||||
repo_details,
|
||||
&repo_config.branches().next(),
|
||||
vec![main_head.clone()],
|
||||
net,
|
||||
)
|
||||
.await)?;
|
||||
let next_head = next[0].clone();
|
||||
let dev = (get_commit_history(
|
||||
repo_details,
|
||||
&repo_config.branches().dev(),
|
||||
vec![next_head, main_head],
|
||||
net,
|
||||
)
|
||||
.await)?;
|
||||
debug!(
|
||||
main = main.len(),
|
||||
next = next.len(),
|
||||
dev = dev.len(),
|
||||
"Commit histories"
|
||||
);
|
||||
let histories = forge::CommitHistories { main, next, dev };
|
||||
Ok(histories)
|
||||
}
|
||||
|
||||
#[tracing::instrument(fields(%branch_name),skip_all)]
|
||||
async fn get_commit_history(
|
||||
repo_details: &server::config::RepoDetails,
|
||||
branch_name: &BranchName,
|
||||
find_commits: Vec<forge::Commit>,
|
||||
net: &kxio::network::Network,
|
||||
) -> Result<Vec<forge::Commit>, network::NetworkError> {
|
||||
let hostname = &repo_details.forge.hostname;
|
||||
let path = &repo_details.repo;
|
||||
|
||||
let mut page = 1;
|
||||
let limit = match find_commits.is_empty() {
|
||||
true => 1,
|
||||
false => 50,
|
||||
};
|
||||
let options = "stat=false&verification=false&files=false";
|
||||
let mut all_commits = Vec::new();
|
||||
loop {
|
||||
let api_token = &repo_details.forge.token;
|
||||
use secrecy::ExposeSecret;
|
||||
let token = api_token.expose_secret();
|
||||
let url = network::NetUrl::new(format!(
|
||||
"https://{hostname}/api/v1/repos/{path}/commits?sha={branch_name}&{options}&token={token}&page={page}&limit={limit}"
|
||||
));
|
||||
|
||||
let request = network::NetRequest::new(
|
||||
network::RequestMethod::Get,
|
||||
url,
|
||||
network::NetRequestHeaders::new(),
|
||||
network::RequestBody::None,
|
||||
network::ResponseType::Json,
|
||||
None,
|
||||
network::NetRequestLogging::None,
|
||||
);
|
||||
let response = net.get::<Vec<Commit>>(request).await?;
|
||||
let commits = response
|
||||
.response_body()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(forge::Commit::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let found = find_commits.is_empty()
|
||||
|| find_commits
|
||||
.clone()
|
||||
.into_iter()
|
||||
.any(|find_commit| commits.iter().any(|commit| commit == &find_commit));
|
||||
let at_end = commits.len() < limit;
|
||||
all_commits.extend(commits);
|
||||
if found || at_end {
|
||||
break;
|
||||
}
|
||||
page += 1;
|
||||
}
|
||||
|
||||
Ok(all_commits)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
struct Commit {
|
||||
sha: String,
|
||||
commit: RepoCommit,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
struct RepoCommit {
|
||||
message: String,
|
||||
}
|
||||
impl From<Commit> for forge::Commit {
|
||||
fn from(value: Commit) -> Self {
|
||||
Self::new(&value.sha, &value.commit.message)
|
||||
}
|
||||
}
|
88
src/server/gitforge/forgejo/file/mod.rs
Normal file
88
src/server/gitforge/forgejo/file/mod.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use kxio::network::{self, Network};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoDetails},
|
||||
gitforge::ForgeFileError,
|
||||
};
|
||||
|
||||
pub(super) async fn contents_get(
|
||||
repo_details: &RepoDetails,
|
||||
net: &Network,
|
||||
branch: &BranchName,
|
||||
file_path: &str,
|
||||
) -> Result<String, ForgeFileError> {
|
||||
let hostname = &repo_details.forge.hostname;
|
||||
let path = &repo_details.repo;
|
||||
let api_token = &repo_details.forge.token;
|
||||
use secrecy::ExposeSecret;
|
||||
let token = api_token.expose_secret();
|
||||
let url = network::NetUrl::new(format!(
|
||||
"https://{hostname}/api/v1/repos/{path}/contents/{file_path}?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| {
|
||||
warn!(?e, "");
|
||||
ForgeFileError::NotFound(file_path.to_string())
|
||||
})?;
|
||||
let status = response.status_code();
|
||||
let contents = 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),
|
||||
_ => Err(ForgeFileError::NotFile(file_path.to_string())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!(%status, "Failed to fetch repo config file");
|
||||
Err(ForgeFileError::Unknown(status.to_string()))
|
||||
}
|
||||
}?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
fn decode_config(body: ForgeContentsResponse) -> Result<String, ForgeFileError> {
|
||||
use base64::Engine;
|
||||
match body.encoding.as_str() {
|
||||
"base64" => {
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
.decode(body.content)
|
||||
.map_err(|_| ForgeFileError::DecodeFromBase64)?;
|
||||
let decoded = String::from_utf8(decoded).map_err(|_| ForgeFileError::DecodeFromUtf8)?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
encoding => Err(ForgeFileError::UnknownEncoding(encoding.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
121
src/server/gitforge/forgejo/mod.rs
Normal file
121
src/server/gitforge/forgejo/mod.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
mod branch;
|
||||
mod file;
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use kxio::network::{self, Network};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::server::{
|
||||
actors::repo::RepoActor,
|
||||
config::{BranchName, RepoConfig, RepoDetails},
|
||||
forge::Commit,
|
||||
gitforge::{BranchResetResult, CommitStatus, Force, ForgeBranchError, ForgeFileError},
|
||||
types::GitRef,
|
||||
};
|
||||
|
||||
struct ForgeJo;
|
||||
#[derive(Clone)]
|
||||
pub struct ForgeJoEnv {
|
||||
repo_details: RepoDetails,
|
||||
net: Network,
|
||||
}
|
||||
impl ForgeJoEnv {
|
||||
pub(super) const fn new(repo_details: RepoDetails, net: Network) -> Self {
|
||||
Self { repo_details, net }
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl super::ForgeLike for ForgeJoEnv {
|
||||
fn name(&self) -> String {
|
||||
"forgejo".to_string()
|
||||
}
|
||||
|
||||
async fn branches_get_all(&self) -> Result<Vec<super::Branch>, ForgeBranchError> {
|
||||
branch::get_all(&self.repo_details, &self.net).await
|
||||
}
|
||||
|
||||
async fn file_contents_get(
|
||||
&self,
|
||||
branch: &BranchName,
|
||||
file_path: &str,
|
||||
) -> Result<String, ForgeFileError> {
|
||||
file::contents_get(&self.repo_details, &self.net, branch, file_path).await
|
||||
}
|
||||
|
||||
async fn branches_validate_positions(&self, repo_config: RepoConfig, addr: Addr<RepoActor>) {
|
||||
branch::validate_positions(self, repo_config, addr).await
|
||||
}
|
||||
|
||||
fn branch_reset(
|
||||
&self,
|
||||
branch_name: BranchName,
|
||||
to_commit: GitRef,
|
||||
force: Force,
|
||||
) -> BranchResetResult {
|
||||
branch::reset(&self.repo_details, branch_name, to_commit, force)
|
||||
}
|
||||
|
||||
async fn commit_status(&self, commit: &Commit) -> CommitStatus {
|
||||
let repo_details = &self.repo_details;
|
||||
let hostname = &repo_details.forge.hostname;
|
||||
let path = &repo_details.repo;
|
||||
let api_token = &repo_details.forge.token;
|
||||
use secrecy::ExposeSecret;
|
||||
let token = api_token.expose_secret();
|
||||
let url = network::NetUrl::new(format!(
|
||||
"https://{hostname}/api/v1/repos/{path}/commits/{commit}/status?token={token}"
|
||||
));
|
||||
|
||||
let request = network::NetRequest::new(
|
||||
network::RequestMethod::Get,
|
||||
url,
|
||||
network::NetRequestHeaders::new(),
|
||||
network::RequestBody::None,
|
||||
network::ResponseType::Json,
|
||||
None,
|
||||
network::NetRequestLogging::None,
|
||||
);
|
||||
let result = self.net.get::<CombinedStatus>(request).await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
match response.response_body() {
|
||||
Some(status) => match status.state {
|
||||
CommitStatusState::Success => CommitStatus::Pass,
|
||||
CommitStatusState::Pending => CommitStatus::Pending,
|
||||
CommitStatusState::Failure => CommitStatus::Fail,
|
||||
CommitStatusState::Error => CommitStatus::Fail,
|
||||
CommitStatusState::Blank => CommitStatus::Pending,
|
||||
},
|
||||
None => {
|
||||
warn!("No status found for commit");
|
||||
CommitStatus::Pending // assume issue is transient and allow retry
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e, "Failed to get commit status");
|
||||
CommitStatus::Pending // assume issue is transient and allow retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct CombinedStatus {
|
||||
pub state: CommitStatusState,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub enum CommitStatusState {
|
||||
#[serde(rename = "success")]
|
||||
Success,
|
||||
#[serde(rename = "pending")]
|
||||
Pending,
|
||||
#[serde(rename = "failure")]
|
||||
Failure,
|
||||
#[serde(rename = "error")]
|
||||
Error,
|
||||
#[serde(rename = "")]
|
||||
Blank,
|
||||
}
|
21
src/server/gitforge/github.rs
Normal file
21
src/server/gitforge/github.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use crate::network::Network;
|
||||
|
||||
struct Github;
|
||||
pub(super) struct GithubEnv {
|
||||
net: Network,
|
||||
}
|
||||
impl GithubEnv {
|
||||
pub(crate) const fn new(net: Network) -> GithubEnv {
|
||||
Self { net }
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl super::ForgeLike for GithubEnv {
|
||||
fn name(&self) -> String {
|
||||
"github".to_string()
|
||||
}
|
||||
|
||||
async fn branches_get_all(&self) -> Vec<super::Branch> {
|
||||
todo!()
|
||||
}
|
||||
}
|
55
src/server/gitforge/mock_forge.rs
Normal file
55
src/server/gitforge/mock_forge.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use crate::server::{
|
||||
actors::repo::RepoActor,
|
||||
config::{BranchName, RepoConfig},
|
||||
forge::Commit,
|
||||
gitforge::{BranchResetResult, CommitStatus, Force, ForgeBranchError, ForgeFileError},
|
||||
types::GitRef,
|
||||
};
|
||||
|
||||
struct MockForge;
|
||||
#[derive(Clone)]
|
||||
pub struct MockForgeEnv;
|
||||
impl MockForgeEnv {
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl super::ForgeLike for MockForgeEnv {
|
||||
fn name(&self) -> String {
|
||||
"mock".to_string()
|
||||
}
|
||||
|
||||
async fn branches_get_all(&self) -> Result<Vec<super::Branch>, ForgeBranchError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn file_contents_get(
|
||||
&self,
|
||||
_branch: &BranchName,
|
||||
_file_path: &str,
|
||||
) -> Result<String, ForgeFileError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn branches_validate_positions(
|
||||
&self,
|
||||
_repo_config: RepoConfig,
|
||||
_addr: actix::prelude::Addr<RepoActor>,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn branch_reset(
|
||||
&self,
|
||||
_branch_name: BranchName,
|
||||
_to_commit: GitRef,
|
||||
_force: Force,
|
||||
) -> BranchResetResult {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn commit_status(&self, _commit: &Commit) -> CommitStatus {
|
||||
todo!()
|
||||
}
|
||||
}
|
85
src/server/gitforge/mod.rs
Normal file
85
src/server/gitforge/mod.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use kxio::network::Network;
|
||||
|
||||
#[cfg(feature = "forgejo")]
|
||||
mod forgejo;
|
||||
|
||||
#[cfg(feature = "github")]
|
||||
mod github;
|
||||
|
||||
mod mock_forge;
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
mod errors;
|
||||
pub use errors::*;
|
||||
|
||||
use crate::server::{
|
||||
config::{BranchName, RepoConfig, RepoDetails},
|
||||
forge::Commit,
|
||||
types::GitRef,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ForgeLike {
|
||||
fn name(&self) -> String;
|
||||
async fn branches_get_all(&self) -> Result<Vec<Branch>, ForgeBranchError>;
|
||||
async fn file_contents_get(
|
||||
&self,
|
||||
branch: &super::config::BranchName,
|
||||
file_path: &str,
|
||||
) -> Result<String, ForgeFileError>;
|
||||
async fn branches_validate_positions(
|
||||
&self,
|
||||
repo_config: RepoConfig,
|
||||
addr: actix::prelude::Addr<super::actors::repo::RepoActor>,
|
||||
);
|
||||
fn branch_reset(
|
||||
&self,
|
||||
branch_name: BranchName,
|
||||
to_commit: GitRef,
|
||||
force: Force,
|
||||
) -> BranchResetResult;
|
||||
|
||||
async fn commit_status(&self, commit: &Commit) -> CommitStatus;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Forge {
|
||||
Mock(mock_forge::MockForgeEnv),
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[cfg(feature = "forgejo")]
|
||||
ForgeJo(forgejo::ForgeJoEnv),
|
||||
#[cfg(feature = "github")]
|
||||
Github(github::GithubEnv),
|
||||
}
|
||||
impl Forge {
|
||||
pub const fn new_mock() -> Self {
|
||||
Self::Mock(mock_forge::MockForgeEnv::new())
|
||||
}
|
||||
#[cfg(feature = "forgejo")]
|
||||
pub const fn new_forgejo(repo_details: RepoDetails, net: Network) -> Self {
|
||||
Self::ForgeJo(forgejo::ForgeJoEnv::new(repo_details, net))
|
||||
}
|
||||
#[cfg(feature = "github")]
|
||||
pub const fn new_github(net: Network) -> Self {
|
||||
Self::Github(github::GithubEnv::new(net))
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for Forge {
|
||||
type Target = dyn ForgeLike;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Mock(env) => env,
|
||||
#[cfg(feature = "forgejo")]
|
||||
Self::ForgeJo(env) => env,
|
||||
#[cfg(feature = "github")]
|
||||
Forge::Github(env) => env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
59
src/server/gitforge/tests/common.rs
Normal file
59
src/server/gitforge/tests/common.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use crate::server::config::{
|
||||
ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, Hostname, RepoAlias, RepoBranches,
|
||||
RepoConfig, RepoDetails, RepoPath, User,
|
||||
};
|
||||
|
||||
pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
|
||||
ForgeDetails {
|
||||
name: forge_name(n),
|
||||
forge_type,
|
||||
hostname: hostname(n),
|
||||
user: user(n),
|
||||
token: api_token(n),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_token(n: u32) -> ApiToken {
|
||||
ApiToken::from(format!("api-{}", n))
|
||||
}
|
||||
|
||||
pub fn user(n: u32) -> User {
|
||||
User(format!("user-{}", n))
|
||||
}
|
||||
|
||||
pub fn hostname(n: u32) -> Hostname {
|
||||
Hostname(format!("hostname-{}", n))
|
||||
}
|
||||
|
||||
pub fn forge_name(n: u32) -> ForgeName {
|
||||
ForgeName(format!("forge-name-{}", n))
|
||||
}
|
||||
pub fn repo_details(n: u32, forge: ForgeDetails, config: Option<RepoConfig>) -> RepoDetails {
|
||||
RepoDetails {
|
||||
name: repo_alias(n),
|
||||
repo: repo_path(n),
|
||||
branch: branch_name(n),
|
||||
forge,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn branch_name(n: u32) -> BranchName {
|
||||
BranchName(format!("branch-name-{}", n))
|
||||
}
|
||||
|
||||
pub fn repo_path(n: u32) -> RepoPath {
|
||||
RepoPath(format!("repo-path-{}", n))
|
||||
}
|
||||
|
||||
pub fn repo_alias(n: u32) -> RepoAlias {
|
||||
RepoAlias(format!("repo-alias-{}", n))
|
||||
}
|
||||
|
||||
pub fn config(n: u32) -> RepoConfig {
|
||||
RepoConfig::new(RepoBranches::new(
|
||||
format!("main-{n}"),
|
||||
format!("next-{n}"),
|
||||
format!("dev-{n}"),
|
||||
))
|
||||
}
|
50
src/server/gitforge/tests/data-forgejo-branches-get.json
Normal file
50
src/server/gitforge/tests/data-forgejo-branches-get.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
[
|
||||
{
|
||||
"commit": {
|
||||
"added": [
|
||||
"string"
|
||||
],
|
||||
"author": {
|
||||
"email": "user@example.com",
|
||||
"name": "string",
|
||||
"username": "string"
|
||||
},
|
||||
"committer": {
|
||||
"email": "user@example.com",
|
||||
"name": "string",
|
||||
"username": "string"
|
||||
},
|
||||
"id": "string",
|
||||
"message": "string",
|
||||
"modified": [
|
||||
"string"
|
||||
],
|
||||
"removed": [
|
||||
"string"
|
||||
],
|
||||
"timestamp": "2024-04-16T21:35:56.331Z",
|
||||
"url": "string",
|
||||
"verification": {
|
||||
"payload": "string",
|
||||
"reason": "string",
|
||||
"signature": "string",
|
||||
"signer": {
|
||||
"email": "user@example.com",
|
||||
"name": "string",
|
||||
"username": "string"
|
||||
},
|
||||
"verified": true
|
||||
}
|
||||
},
|
||||
"effective_branch_protection_name": "string",
|
||||
"enable_status_check": true,
|
||||
"name": "string",
|
||||
"protected": true,
|
||||
"required_approvals": 0,
|
||||
"status_check_contexts": [
|
||||
"string"
|
||||
],
|
||||
"user_can_merge": true,
|
||||
"user_can_push": true
|
||||
}
|
||||
]
|
48
src/server/gitforge/tests/forgejo.rs
Normal file
48
src/server/gitforge/tests/forgejo.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use assert2::let_assert;
|
||||
|
||||
use kxio::network::{MockNetwork, StatusCode};
|
||||
|
||||
use crate::server::config::{BranchName, ForgeType};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_name() {
|
||||
let net = Network::new_mock();
|
||||
let repo_details = common::repo_details(
|
||||
1,
|
||||
common::forge_details(1, ForgeType::MockForge),
|
||||
Some(common::config(1)),
|
||||
);
|
||||
let forge = Forge::new_forgejo(repo_details, net);
|
||||
assert_eq!(forge.name(), "forgejo");
|
||||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn test_branches_get() {
|
||||
let mut net = MockNetwork::new();
|
||||
let hostname = common::hostname(1);
|
||||
let path = common::repo_path(1);
|
||||
let api_token = common::api_token(1);
|
||||
use secrecy::ExposeSecret;
|
||||
let token = api_token.expose_secret();
|
||||
let url = format!("https://{hostname}/api/v1/repos/{path}/branches?token={token}");
|
||||
let body = include_str!("./data-forgejo-branches-get.json");
|
||||
net.add_get_response(&url, StatusCode::OK, body);
|
||||
let net = Network::from(net);
|
||||
|
||||
let repo_details = common::repo_details(
|
||||
1,
|
||||
common::forge_details(1, ForgeType::MockForge),
|
||||
Some(common::config(1)),
|
||||
);
|
||||
|
||||
let forge = Forge::new_forgejo(repo_details, net.clone());
|
||||
|
||||
let_assert!(Ok(branches) = forge.branches_get_all().await);
|
||||
|
||||
let_assert!(Some(requests) = net.mocked_requests());
|
||||
assert_eq!(requests.len(), 1);
|
||||
|
||||
assert_eq!(branches, vec![Branch(BranchName("string".into()))]);
|
||||
}
|
8
src/server/gitforge/tests/github.rs
Normal file
8
src/server/gitforge/tests/github.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_name() {
|
||||
let net = Network::new_mock();
|
||||
let forge = Forge::new_github(net);
|
||||
assert_eq!(forge.name(), "github");
|
||||
}
|
15
src/server/gitforge/tests/mod.rs
Normal file
15
src/server/gitforge/tests/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use super::*;
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "forgejo")]
|
||||
mod forgejo;
|
||||
|
||||
#[cfg(feature = "github")]
|
||||
mod github;
|
||||
|
||||
#[test]
|
||||
fn test_mock_name() {
|
||||
let forge = Forge::new_mock();
|
||||
assert_eq!(forge.name(), "mock");
|
||||
}
|
29
src/server/gitforge/types.rs
Normal file
29
src/server/gitforge/types.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use crate::server::config::BranchName;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Branch(pub BranchName);
|
||||
impl Branch {
|
||||
pub const fn name(&self) -> &BranchName {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Force {
|
||||
No,
|
||||
From(crate::server::types::GitRef),
|
||||
}
|
||||
impl std::fmt::Display for Force {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub type BranchResetResult = Result<(), ()>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommitStatus {
|
||||
Pass,
|
||||
Fail,
|
||||
Pending,
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
mod actors;
|
||||
mod config;
|
||||
pub mod forge;
|
||||
pub mod git;
|
||||
pub mod gitforge;
|
||||
pub mod types;
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use kxio::network::Network;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
@ -16,7 +17,6 @@ use crate::{
|
|||
server::{
|
||||
actors::webhook,
|
||||
config::{Forge, ForgeName, RepoAlias, Webhook},
|
||||
git::Git,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -40,7 +40,7 @@ pub fn init(fs: FileSystem) {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn start(fs: FileSystem, net: Network, git: Git) {
|
||||
pub async fn start(fs: FileSystem, net: Network) {
|
||||
let Ok(_) = init_logging() else {
|
||||
eprintln!("Failed to initialize logging.");
|
||||
return;
|
||||
|
@ -58,7 +58,7 @@ pub async fn start(fs: FileSystem, net: Network, git: Git) {
|
|||
let webhook = server_config.webhook();
|
||||
server_config
|
||||
.forges()
|
||||
.flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, webhook, &net, &git))
|
||||
.flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, webhook, &net))
|
||||
.map(start_actor)
|
||||
.map(|(alias, addr)| webhook::AddWebhookRecipient(alias, addr.recipient()))
|
||||
.for_each(|msg| webhook_router.do_send(msg));
|
||||
|
@ -73,7 +73,6 @@ fn create_forge_repos(
|
|||
forge_name: ForgeName,
|
||||
webhook: &Webhook,
|
||||
net: &Network,
|
||||
git: &Git,
|
||||
) -> Vec<(ForgeName, RepoAlias, RepoActor)> {
|
||||
let forge = forge.clone();
|
||||
let span = tracing::info_span!("Forge", %forge_name, %forge);
|
||||
|
@ -81,7 +80,7 @@ fn create_forge_repos(
|
|||
info!("Creating Forge");
|
||||
forge
|
||||
.repos()
|
||||
.map(create_actor(forge_name, forge.clone(), webhook, net, git))
|
||||
.map(create_actor(forge_name, forge.clone(), webhook, net))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
|
@ -90,11 +89,9 @@ fn create_actor(
|
|||
forge: config::Forge,
|
||||
webhook: &Webhook,
|
||||
net: &Network,
|
||||
git: &Git,
|
||||
) -> impl Fn((RepoAlias, &Repo)) -> (ForgeName, RepoAlias, RepoActor) {
|
||||
let webhook = webhook.clone();
|
||||
let net = net.clone();
|
||||
let git = git.clone();
|
||||
move |(repo_name, repo)| {
|
||||
let span = tracing::info_span!("Repo", %repo_name, %repo);
|
||||
let _guard = span.enter();
|
||||
|
@ -103,7 +100,6 @@ fn create_actor(
|
|||
config::RepoDetails::new(&repo_name, repo, &forge_name, &forge),
|
||||
webhook.clone(),
|
||||
net.clone(),
|
||||
git.clone(),
|
||||
);
|
||||
info!("Created Repo");
|
||||
(forge_name.clone(), repo_name, actor)
|
||||
|
|
|
@ -19,14 +19,3 @@ impl Display for GitRef {
|
|||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResetForce {
|
||||
None,
|
||||
Force(GitRef),
|
||||
}
|
||||
impl Display for ResetForce {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue