diff --git a/src/main.rs b/src/main.rs index 794dd5a..4845e95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ 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 { @@ -26,6 +28,7 @@ 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 { @@ -37,7 +40,7 @@ async fn main() { server::init(fs); } Server::Start => { - server::start(fs, net).await; + server::start(fs, net, git).await; } }, } diff --git a/src/server/actors/repo/branch.rs b/src/server/actors/repo/branch.rs index 452ac26..382eb66 100644 --- a/src/server/actors/repo/branch.rs +++ b/src/server/actors/repo/branch.rs @@ -1,16 +1,9 @@ -use std::fmt::Display; - use actix::prelude::*; use kxio::network; -use secrecy::ExposeSecret; use tracing::{error, info, warn}; -use crate::server::{ - actors::repo::{branch, ValidateRepo}, - config::{self, BranchName}, - forge, -}; +use crate::server::{actors::repo::ValidateRepo, config, forge, git::Git, types::ResetForce}; use super::{RepoActor, StartMonitoring}; @@ -19,6 +12,7 @@ pub async fn validate_positions( repo_details: config::RepoDetails, config: config::RepoConfig, addr: Addr, + git: Git, net: network::Network, ) { let commit_histories = match repo_details.forge.forge_type { @@ -45,13 +39,15 @@ pub async fn validate_positions( 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 branch::reset( - &config.branches().next(), - main, - ResetForce::Force(next.into()), - &repo_details, - ) - .is_ok() + + if git + .reset( + &config.branches().next(), + main.into(), + ResetForce::Force(next.into()), + &repo_details, + ) + .is_ok() { // TODO : (#18) sleep and restart while we don't have webhooks tokio::time::sleep(std::time::Duration::from_secs(10)).await; @@ -118,6 +114,7 @@ pub async fn advance_next( repo_details: config::RepoDetails, repo_config: config::RepoConfig, addr: Addr, + git: Git, ) { let next_commit = find_next_commit_on_dev(next, dev_commit_history); let Some(commit) = next_commit else { @@ -129,9 +126,9 @@ pub async fn advance_next( return; } info!("Advancing next to commit '{}'", commit); - match reset( + match git.reset( &repo_config.branches().next(), - commit, + commit.into(), ResetForce::None, &repo_details, ) { @@ -184,11 +181,12 @@ pub async fn advance_main( next: forge::Commit, repo_details: config::RepoDetails, repo_config: config::RepoConfig, + git: Git, ) { info!("Advancing main to next"); - match reset( + match git.reset( &repo_config.branches().main(), - next, + next.into(), ResetForce::None, &repo_details, ) { @@ -201,75 +199,6 @@ pub async fn advance_main( }; } -#[derive(Clone, Debug)] -pub struct GitRef(pub String); -impl From for GitRef { - fn from(value: forge::Commit) -> Self { - Self(value.sha().to_string()) - } -} -impl From for GitRef { - fn from(value: BranchName) -> Self { - Self(value.0) - } -} -impl Display for GitRef { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - 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) - } -} -pub fn reset( - branch: &BranchName, - gitref: impl Into, - reset_force: ResetForce, - repo_details: &config::RepoDetails, -) -> Result<(), std::io::Error> { - let gitref: GitRef = gitref.into(); - 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, "Advance Next Failed (wait)"); - Err(err) - } - }, - Err(err) => { - warn!(?err, "Advance Next Failed (spawn)"); - Err(err) - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/server/actors/repo/mod.rs b/src/server/actors/repo/mod.rs index c8c7a1e..6a6d0a1 100644 --- a/src/server/actors/repo/mod.rs +++ b/src/server/actors/repo/mod.rs @@ -10,6 +10,7 @@ use tracing::{info, warn}; use crate::server::{ config::{RepoConfig, RepoDetails}, forge, + git::Git, }; use self::webhook::WebhookId; @@ -19,14 +20,16 @@ pub struct RepoActor { config: Option, // INFO: if [None] then send [StartRepo] to populate it webhook_id: Option, // INFO: if [None] then no webhook is configured net: Network, + git: Git, } impl RepoActor { - pub(crate) const fn new(details: RepoDetails, net: Network) -> Self { + pub(crate) const fn new(details: RepoDetails, net: Network, git: Git) -> Self { Self { details, config: None, webhook_id: None, net, + git, } } } @@ -85,7 +88,8 @@ impl Handler for RepoActor { let repo_details = self.details.clone(); let addr = ctx.address(); let net = self.net.clone(); - branch::validate_positions(repo_details, repo_config, addr, net) + let git = self.git.clone(); + branch::validate_positions(repo_details, repo_config, addr, git, net) .into_actor(self) .wait(ctx); } @@ -115,6 +119,7 @@ impl Handler for RepoActor { let repo_details = self.details.clone(); let addr = ctx.address(); let net = self.net.clone(); + let git = self.git.clone(); if next_ahead_of_main { status::check_next(msg.next, repo_details, addr, net) @@ -127,6 +132,7 @@ impl Handler for RepoActor { repo_details, repo_config, addr, + git, ) .into_actor(self) .wait(ctx); @@ -159,7 +165,8 @@ impl Handler for RepoActor { warn!("No config loaded"); return; }; - branch::advance_main(msg.0, repo_details, repo_config) + let git = self.git.clone(); + branch::advance_main(msg.0, repo_details, repo_config, git) .into_actor(self) .wait(ctx); } diff --git a/src/server/config.rs b/src/server/config.rs index ea7ea44..b138a5a 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -205,7 +205,7 @@ impl Display for RepoPath { write!(f, "{}", self.0) } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct BranchName(pub String); impl Display for BranchName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/src/server/git/mock.rs b/src/server/git/mock.rs new file mode 100644 index 0000000..4aa6809 --- /dev/null +++ b/src/server/git/mock.rs @@ -0,0 +1,47 @@ +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) + } +} diff --git a/src/server/git/mod.rs b/src/server/git/mod.rs new file mode 100644 index 0000000..5cafbe0 --- /dev/null +++ b/src/server/git/mod.rs @@ -0,0 +1,48 @@ +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<(), ()>; diff --git a/src/server/git/real.rs b/src/server/git/real.rs new file mode 100644 index 0000000..f5cf78d --- /dev/null +++ b/src/server/git/real.rs @@ -0,0 +1,57 @@ +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(()) + } + } + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index b9ff602..6109df4 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,6 +1,8 @@ mod actors; mod config; pub mod forge; +pub mod git; +pub mod types; use actix::prelude::*; use kxio::network::Network; @@ -11,7 +13,10 @@ use tracing::{error, info, level_filters::LevelFilter}; use crate::{ filesystem::FileSystem, - server::config::{Forge, ForgeName, RepoName}, + server::{ + config::{Forge, ForgeName, RepoName}, + git::Git, + }, }; use self::{actors::repo::RepoActor, config::Repo}; @@ -34,7 +39,7 @@ pub fn init(fs: FileSystem) { } } -pub async fn start(fs: FileSystem, net: Network) { +pub async fn start(fs: FileSystem, net: Network, git: Git) { let Ok(_) = init_logging() else { eprintln!("Failed to initialize logging."); return; @@ -51,7 +56,7 @@ pub async fn start(fs: FileSystem, net: Network) { info!("Config loaded"); let addresses = config .forges() - .flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, &net)) + .flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, &net, &git)) .map(start_actor) .collect::>(); let _ = actix_rt::signal::ctrl_c().await; @@ -63,6 +68,7 @@ fn create_forge_repos( forge: &Forge, forge_name: ForgeName, net: &Network, + git: &Git, ) -> Vec<(ForgeName, RepoName, RepoActor)> { let forge = forge.clone(); let span = tracing::info_span!("Forge", %forge_name, %forge); @@ -70,7 +76,7 @@ fn create_forge_repos( info!("Creating Forge"); forge .repos() - .map(create_actor(forge_name, forge.clone(), net)) + .map(create_actor(forge_name, forge.clone(), net, git)) .collect::>() } @@ -78,8 +84,10 @@ fn create_actor( forge_name: ForgeName, forge: config::Forge, net: &Network, + git: &Git, ) -> impl Fn((RepoName, &Repo)) -> (ForgeName, RepoName, RepoActor) { 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(); @@ -87,6 +95,7 @@ fn create_actor( let actor = actors::repo::RepoActor::new( config::RepoDetails::new(&repo_name, repo, &forge_name, &forge), net.clone(), + git.clone(), ); info!("Created Repo"); (forge_name.clone(), repo_name, actor) diff --git a/src/server/types.rs b/src/server/types.rs new file mode 100644 index 0000000..57a9cd3 --- /dev/null +++ b/src/server/types.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +use crate::server::{config::BranchName, forge}; + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct GitRef(pub String); +impl From for GitRef { + fn from(value: forge::Commit) -> Self { + Self(value.sha().to_string()) + } +} +impl From for GitRef { + fn from(value: BranchName) -> Self { + Self(value.0) + } +} +impl Display for GitRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + 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) + } +}