// use crate as git; use git_next_config as config; use tracing::{debug, error, info, warn}; pub type Result = core::result::Result; 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 { // Collect Commit Histories for `main`, `next` and `dev` branches repository.fetch()?; let commit_histories = get_commit_histories(repository, &repo_config); let commit_histories = match commit_histories { Ok(commit_histories) => commit_histories, Err(err) => { error!(?err, "Failed to get commit histories"); return Err(git::validation::Error::CommitLog(err)); } }; // Validations let Some(main) = commit_histories.main.first().cloned() else { warn!( "No commits on main branch '{}'", repo_config.branches().main() ); return Err(git::validation::Error::BranchHasNoCommits( repo_config.branches().main(), )); }; // Dev must be on main branch, or user must rebase it 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 '{}' - user should rebase onto main branch '{}'", repo_config.branches().dev(), repo_config.branches().main(), repo_config.branches().main(), ); return Err(git::validation::Error::DevBranchNotBasedOn( repo_config.branches().main(), )); } // 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 '{}", repo_config.branches().next() ); return Err(git::validation::Error::BranchHasNoCommits( repo_config.branches().next(), )); }; 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 let Err(err) = git::branch::reset( repository, repo_details, repo_config.branches().next(), main.into(), git::push::Force::From(next.clone().into()), ) { warn!(?err, "Failed to reset next to main"); return Err(git::validation::Error::FailedToResetBranch { branch: repo_config.branches().next(), commit: next, }); } return Err(git::validation::Error::BranchReset( repo_config.branches().next(), )); } let next_commits = commit_histories .next .into_iter() .take(2) // next should never be more than one commit ahead of main, so this should be // either be next and main on two adjacent commits, or next and main on the same commit, // plus the parent of main. .collect::>(); if !next_commits.contains(&main) { warn!( "Main branch '{}' is not on the same commit as next branch '{}', or it's parent - resetting next to main", repo_config.branches().main(), repo_config.branches().next() ); if let Err(err) = git::branch::reset( repository, repo_details, repo_config.branches().next(), main.into(), git::push::Force::From(next.clone().into()), ) { warn!(?err, "Failed to reset next to main"); return Err(git::validation::Error::FailedToResetBranch { branch: repo_config.branches().next(), commit: next, }); } return Err(git::validation::Error::BranchReset( repo_config.branches().next(), )); } let Some(next) = next_commits.first().cloned() else { warn!( "No commits on next branch '{}'", repo_config.branches().next() ); return Err(git::validation::Error::BranchHasNoCommits( repo_config.branches().next(), )); }; 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 '{}' - next branch will be updated shortly", repo_config.branches().dev(), repo_config.branches().next() ); return Err(git::validation::Error::DevBranchNotBasedOn( repo_config.branches().next(), )); // dev is not based on next } let Some(dev) = commit_histories.dev.first().cloned() else { warn!( "No commits on dev branch '{}'", repo_config.branches().dev() ); return Err(git::validation::Error::BranchHasNoCommits( repo_config.branches().dev(), )); }; Ok(git::validation::Positions { main, next, dev, dev_commit_history: commit_histories.dev, }) } 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)?; debug!( main = main.len(), next = next.len(), dev = dev.len(), "Commit histories" ); let histories = git::commit::Histories { main, next, dev }; Ok(histories) } #[derive(Debug, derive_more::Display)] pub enum Error { Fetch(git::fetch::Error), CommitLog(git::commit::log::Error), #[display("Failed to Reset Branch {branch} to {commit}")] FailedToResetBranch { branch: config::BranchName, commit: git::Commit, }, BranchReset(config::BranchName), BranchHasNoCommits(config::BranchName), DevBranchNotBasedOn(config::BranchName), } impl std::error::Error for Error {} impl From for Error { fn from(value: git::fetch::Error) -> Self { Self::Fetch(value) } }