git-next/crates/git/src/validation/positions.rs

199 lines
6.9 KiB
Rust

//
use crate as git;
use git_next_config as config;
use tracing::{debug, error, info, warn};
pub type Result<T> = core::result::Result<T, Error>;
pub struct Positions {
pub main: git::Commit,
pub next: git::Commit,
pub dev: git::Commit,
pub dev_commit_history: Vec<git::Commit>,
}
#[allow(clippy::cognitive_complexity)] // TODO: (#83) reduce complexity
pub fn validate_positions(
repository: &git::OpenRepository,
repo_details: &git::RepoDetails,
repo_config: config::RepoConfig,
) -> Result<Positions> {
// 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::positions::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::positions::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::positions::Error::DevBranchNotBasedOn {
dev: repo_config.branches().dev(),
other: 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::positions::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::positions::Error::FailedToResetBranch {
branch: repo_config.branches().next(),
commit: next,
});
}
return Err(git::validation::positions::Error::NextBranchResetRequired(
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::<Vec<_>>();
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::positions::Error::FailedToResetBranch {
branch: repo_config.branches().next(),
commit: next,
});
}
return Err(git::validation::positions::Error::NextBranchResetRequired(
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::positions::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::positions::Error::DevBranchNotBasedOn {
dev: repo_config.branches().dev(),
other: 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::positions::Error::BranchHasNoCommits(
repo_config.branches().dev(),
));
};
Ok(git::validation::positions::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<git::commit::Histories> {
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, 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,
},
}