forked from kemitix/git-next
Paul Campbell
3735afb2f8
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.
258 lines
7.5 KiB
Rust
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")));
|
|
}
|
|
}
|