git-next/src/server/actors/repo/branch.rs
Paul Campbell 3735afb2f8 fix: don't reload config when updating next branch
The StartRepo handler loads the config before then sending the
ValidateRepo message. However, we already have the config, so reloading
it isn't needed.

Added todo markers on sleep and dispatching of ValidateRepo where it is
used as a stop-gap until we have working webhook integration.

Add todo markers for checking valid commit messages before advancing
next branch.
2024-04-12 10:14:32 +01:00

258 lines
7.5 KiB
Rust

use std::fmt::Display;
use actix::prelude::*;
use kxio::network;
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<RepoActor>,
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::<Vec<_>>();
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<forge::Commit>,
repo_details: config::RepoDetails,
repo_config: config::RepoConfig,
addr: Addr<RepoActor>,
) {
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;
};
// TODO: (#33) get commit message for 'commit'
// TODO: (#33) verify commit message is valid
match reset(
&repo_config.branches().next(),
commit,
ResetForce::None,
&repo_details,
) {
Ok(_) => {
info!("Success");
// 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")
}
};
}
fn find_next_commit_on_dev(
next: forge::Commit,
dev_commit_history: Vec<forge::Commit>,
) -> Option<forge::Commit> {
let mut next_commit: Option<forge::Commit> = 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,
) {
match reset(
&repo_config.branches().main(),
next,
ResetForce::None,
&repo_details,
) {
Ok(_) => {
info!("Success");
}
Err(err) => {
warn!(?err, "Failed")
}
};
}
#[derive(Clone)]
pub struct GitRef(pub String);
impl From<forge::Commit> for GitRef {
fn from(value: forge::Commit) -> Self {
Self(value.sha)
}
}
impl From<BranchName> 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)
}
}
pub enum ResetForce {
None,
Force(GitRef),
}
pub fn reset(
branch: &BranchName,
gitref: impl Into<GitRef>,
reset_force: ResetForce,
repo_details: &config::RepoDetails,
) -> Result<(), std::io::Error> {
let gitref: GitRef = gitref.into();
let user = &repo_details.forge.user;
let token = &repo_details.forge.token;
let hostname = &repo_details.forge.hostname;
let path = &repo_details.repo;
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}"),
};
let command = format!("/usr/bin/git push {origin} {gitref}:{branch} {force}");
info!("Running command: {}", command);
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");
let dev_commit_history = vec![
forge::Commit::new("dev"),
forge::Commit::new("dev-next"),
forge::Commit::new("current-next"),
forge::Commit::new("current-main"),
];
let next_commit = find_next_commit_on_dev(next, dev_commit_history);
assert_eq!(next_commit, Some(forge::Commit::new("dev-next")));
}
}