// use crate::{ git::{self, graph::log, repository::open::OpenRepositoryLike, RepoDetails, UserNotification}, BranchName, RepoConfig, }; pub type Result = core::result::Result; #[derive(Debug)] pub struct Positions { pub main: git::Commit, pub next: git::Commit, pub dev: git::Commit, pub dev_commit_history: Vec, pub next_is_valid: bool, } /// Validates the relative positions of the three branches, resetting next back to main if /// it has gone astry. /// /// # Errors /// /// Will return an `Err` if any of the branches has no commits, or if user intervention is /// required, or if there is an error resetting the next branch back to main. #[allow(clippy::result_large_err)] pub fn validate( open_repository: &dyn OpenRepositoryLike, repo_details: &git::RepoDetails, repo_config: &RepoConfig, ) -> Result { let main_branch = repo_config.branches().main(); let next_branch = repo_config.branches().next(); let dev_branch = repo_config.branches().dev(); // Collect Commit Histories for `main`, `next` and `dev` branches open_repository.fetch()?; let commit_histories = get_commit_histories(open_repository, repo_config)?; // branch tips 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}")))?; // Validations: // Dev must be on main branch, else the USER must rebase it 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), }, )); } // verify that next is on main or at most one commit on top of main, else reset it back to main if is_not_based_on( commit_histories .next .iter() .take(2) .cloned() .collect::>() .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); } // verify that next is an ancestor of dev, else reset it back to main if dev not ahead of main 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 { 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 { 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 for Error { fn from(value: git::fetch::Error) -> Self { Self::Retryable(value.to_string()) } } impl From for Error { fn from(value: git::commit::log::Error) -> Self { Self::Retryable(value.to_string()) } }