use crate::{
git::{self, graph::log, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
BranchName, RepoConfig,
};
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub struct Positions {
pub main: git::Commit,
pub next: git::Commit,
pub dev: git::Commit,
pub dev_commit_history: Vec<git::Commit>,
pub next_is_valid: bool,
}
#[allow(clippy::result_large_err)]
pub fn validate(
open_repository: &dyn OpenRepositoryLike,
repo_details: &git::RepoDetails,
repo_config: &RepoConfig,
) -> Result<Positions> {
let main_branch = repo_config.branches().main();
let next_branch = repo_config.branches().next();
let dev_branch = repo_config.branches().dev();
open_repository.fetch()?;
let commit_histories = get_commit_histories(open_repository, repo_config)?;
let main = commit_histories
.main
.first()
.cloned()
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {main_branch}")))?;
let next = commit_histories
.next
.first()
.cloned()
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {next_branch}")))?;
let dev = commit_histories
.dev
.first()
.cloned()
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {dev_branch}")))?;
if is_not_based_on(&commit_histories.dev, &main) {
return Err(Error::UserIntervention(
UserNotification::DevNotBasedOnMain {
forge_alias: repo_details.forge.forge_alias().clone(),
repo_alias: repo_details.repo_alias.clone(),
dev_branch,
main_branch,
dev_commit: dev,
main_commit: main,
log: log(repo_details),
},
));
}
if is_not_based_on(
commit_histories
.next
.iter()
.take(2)
.cloned()
.collect::<Vec<_>>()
.as_slice(),
&main,
) {
tracing::info!("Main not on same commit as next, or it's parent - resetting next to main",);
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
}
if is_not_based_on(&commit_histories.dev, &next)
&& commit_histories.main.first() == commit_histories.dev.first()
{
tracing::info!("Next is not an ancestor of dev - resetting next to main");
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
}
let next_is_valid = is_based_on(&commit_histories.dev, &next);
Ok(git::validation::positions::Positions {
main,
next,
dev,
dev_commit_history: commit_histories.dev,
next_is_valid,
})
}
#[allow(clippy::result_large_err)]
fn reset_next_to_main(
open_repository: &dyn OpenRepositoryLike,
repo_details: &RepoDetails,
main: &git::Commit,
next: &git::Commit,
next_branch: &BranchName,
) -> Result<Positions> {
git::push::reset(
open_repository,
repo_details,
next_branch,
&main.clone().into(),
&git::push::Force::From(next.clone().into()),
)
.map_err(|err| {
Error::NonRetryable(format!(
"Failed to reset branch '{next_branch}' to commit '{next}': {err}"
))
})?;
Err(Error::Retryable(format!(
"Branch {next_branch} has been reset"
)))
}
fn is_not_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
!is_based_on(commits, needle)
}
fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
commits.iter().any(|commit| commit == needle)
}
fn get_commit_histories(
open_repository: &dyn OpenRepositoryLike,
repo_config: &RepoConfig,
) -> git::commit::log::Result<git::commit::Histories> {
let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
let main_head = [main[0].clone()];
let next = open_repository.commit_log(&repo_config.branches().next(), &main_head)?;
let dev = open_repository.commit_log(&repo_config.branches().dev(), &main_head)?;
let histories = git::commit::Histories { main, next, dev };
Ok(histories)
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0} - will retry")]
Retryable(String),
#[error("{0} - not retrying")]
NonRetryable(String),
#[error("user intervention required")]
UserIntervention(UserNotification),
}
impl From<git::fetch::Error> for Error {
fn from(value: git::fetch::Error) -> Self {
Self::Retryable(value.to_string())
}
}
impl From<git::commit::log::Error> for Error {
fn from(value: git::commit::log::Error) -> Self {
Self::Retryable(value.to_string())
}
}