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 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, net: network::Network, ) { let commit_histories = match repo_details.forge.forge_type { config::ForgeType::ForgeJo => { forge::forgejo::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; } }; 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"); if branch::reset( &config.branches().next(), main, 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; addr.do_send(super::ValidateRepo) } return; } let next_commits = commit_histories .next .into_iter() .take(2) .collect::>(); if !next_commits.contains(&main) { warn!( "Main branch '{}' is not on the same commit as next branch '{}', or it's parent", config.branches().main(), config.branches().next() ); 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 '{}'", 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 '{}'", config.branches().dev(), 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, }); } // 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, repo_details: config::RepoDetails, repo_config: config::RepoConfig, addr: Addr, ) { let next_commit = find_next_commit_on_dev(next, dev_commit_history); let Some(commit) = next_commit else { warn!("No commits to advance next to"); return; }; if let Some(problem) = validate_commit_message(commit.message()) { warn!("Can't advance next to commit '{}': {}", commit, problem); return; } info!("Advancing next to commit '{}'", commit); match reset( &repo_config.branches().next(), commit, ResetForce::None, &repo_details, ) { Ok(_) => { // TODO : (#18) sleep and restart while we don't have webhooks tokio::time::sleep(std::time::Duration::from_secs(10)).await; addr.do_send(ValidateRepo); } Err(err) => { warn!(?err, "Failed") } }; } #[tracing::instrument] fn validate_commit_message(message: &forge::Message) -> Option { let message = &message.to_string(); if message.to_ascii_lowercase().starts_with("wip") { return Some("Is Work-In-Progress".to_string()); } match git_conventional::Commit::parse(message) { Ok(commit) => { info!(?commit, "Pass"); None } Err(err) => { warn!(?err, "Fail"); Some(err.kind().to_string()) } } } fn find_next_commit_on_dev( next: forge::Commit, dev_commit_history: Vec, ) -> Option { let mut next_commit: Option = None; for commit in dev_commit_history.into_iter() { if commit == next { break; }; next_commit.replace(commit); } next_commit } // advance main branch to the commit 'next' #[tracing::instrument(fields(next), skip_all)] pub async fn advance_main( next: forge::Commit, repo_details: config::RepoDetails, repo_config: config::RepoConfig, ) { info!("Advancing main to next"); match reset( &repo_config.branches().main(), next, ResetForce::None, &repo_details, ) { Ok(_) => { info!("Success"); } Err(err) => { warn!(?err, "Failed") } }; } #[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::*; #[actix_rt::test] async fn test_find_next_commit_on_dev() { let next = forge::Commit::new("current-next", "foo"); let expected = forge::Commit::new("dev-next", "next-should-go-here"); let dev_commit_history = vec![ forge::Commit::new("dev", "future"), expected.clone(), next.clone(), forge::Commit::new("current-main", "history"), ]; let next_commit = find_next_commit_on_dev(next, dev_commit_history); assert_eq!(next_commit, Some(expected)); } }