// use crate::{ git::{self, 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 fn validate_positions( 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, }, )); } // 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 is_not_based_on(&commit_histories.dev, &next) { 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); } Ok(git::validation::positions::Positions { main, next, dev, dev_commit_history: commit_histories.dev, }) } 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 { commits.iter().filter(|commit| *commit == needle).count() == 0 } 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()) } }