// use crate as git; use git_next_config as config; use tracing::{error, info, warn}; 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, } #[allow(clippy::cognitive_complexity)] // TODO: (#83) reduce complexity pub fn validate_positions( repository: &git::OpenRepository, repo_details: &git::RepoDetails, repo_config: 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 repository.fetch()?; let commit_histories = get_commit_histories(repository, &repo_config).map_err(Error::CommitLog)?; // branch tips let main = commit_histories .main .first() .cloned() .ok_or_else(|| Error::BranchHasNoCommits(main_branch.clone()))?; let next = commit_histories .next .first() .cloned() .ok_or_else(|| Error::BranchHasNoCommits(next_branch.clone()))?; let dev = commit_histories .dev .first() .cloned() .ok_or_else(|| Error::BranchHasNoCommits(dev_branch.clone()))?; // Validations: // Dev must be on main branch, else the USER must rebase it if is_not_based_on(&commit_histories.dev, &main) { warn!("Dev '{dev_branch}' not based on main '{main_branch}' - user must rebase",); return Err(Error::DevBranchNotBasedOn { dev: dev_branch, other: 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[0..=1], &main) { info!("Main not on same commit as next, or it's parent - resetting next to main",); return reset_next_to_main(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) { info!("Next is not an ancestor of dev - resetting next to main"); return reset_next_to_main(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( repository: &crate::OpenRepository, repo_details: &crate::RepoDetails, main: &crate::Commit, next: &crate::Commit, next_branch: &config::BranchName, ) -> Result { git::push::reset( repository, repo_details, next_branch, &main.clone().into(), &git::push::Force::From(next.clone().into()), ) .map_err(|err| { warn!(?err, "Failed to reset next to main"); Error::FailedToResetBranch { branch: next_branch.clone(), commit: next.clone(), } })?; Err(Error::NextBranchResetRequired(next_branch.clone())) } fn is_not_based_on(commits: &[crate::commit::Commit], needle: &crate::Commit) -> bool { commits.iter().filter(|commit| *commit == needle).count() == 0 } fn get_commit_histories( repository: &git::repository::OpenRepository, repo_config: &config::RepoConfig, ) -> git::commit::log::Result { let main = (repository.commit_log(&repo_config.branches().main(), &[]))?; let main_head = [main[0].clone()]; let next = repository.commit_log(&repo_config.branches().next(), &main_head)?; let dev = 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("fetch: {0}")] Fetch(#[from] git::fetch::Error), #[error("commit log: {0}")] CommitLog(#[from] git::commit::log::Error), #[error("failed to reset branch '{branch}' to {commit}")] FailedToResetBranch { branch: config::BranchName, commit: git::Commit, }, #[error("next branch '{0}' needs to be reset")] NextBranchResetRequired(config::BranchName), #[error("branch '{0}' has no commits")] BranchHasNoCommits(config::BranchName), #[error("dev branch '{dev}' not based on branch '{other}' ")] DevBranchNotBasedOn { dev: config::BranchName, other: config::BranchName, }, }