2024-05-26 08:35:45 +01:00
|
|
|
//
|
2024-07-26 06:49:09 +01:00
|
|
|
use crate::{
|
2024-08-06 20:48:53 +01:00
|
|
|
git::{self, graph::log, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
|
2024-07-26 06:49:09 +01:00
|
|
|
BranchName, RepoConfig,
|
|
|
|
};
|
2024-05-23 16:50:36 +01:00
|
|
|
|
2024-05-26 08:35:45 +01:00
|
|
|
pub type Result<T> = core::result::Result<T, Error>;
|
2024-05-23 16:50:36 +01:00
|
|
|
|
2024-06-09 10:21:09 +01:00
|
|
|
#[derive(Debug)]
|
2024-05-23 16:50:36 +01:00
|
|
|
pub struct Positions {
|
|
|
|
pub main: git::Commit,
|
|
|
|
pub next: git::Commit,
|
|
|
|
pub dev: git::Commit,
|
|
|
|
pub dev_commit_history: Vec<git::Commit>,
|
2024-07-28 18:50:27 +01:00
|
|
|
pub next_is_valid: bool,
|
2024-05-23 16:50:36 +01:00
|
|
|
}
|
|
|
|
|
2024-08-06 07:43:28 +01:00
|
|
|
/// 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.
|
2024-08-01 19:48:59 +01:00
|
|
|
#[allow(clippy::result_large_err)]
|
2024-08-06 07:43:28 +01:00
|
|
|
pub fn validate(
|
2024-07-25 09:02:43 +01:00
|
|
|
open_repository: &dyn OpenRepositoryLike,
|
2024-05-26 08:35:45 +01:00
|
|
|
repo_details: &git::RepoDetails,
|
2024-08-06 07:43:28 +01:00
|
|
|
repo_config: &RepoConfig,
|
2024-05-26 08:35:45 +01:00
|
|
|
) -> Result<Positions> {
|
2024-06-09 10:21:09 +01:00
|
|
|
let main_branch = repo_config.branches().main();
|
|
|
|
let next_branch = repo_config.branches().next();
|
|
|
|
let dev_branch = repo_config.branches().dev();
|
2024-05-26 08:35:45 +01:00
|
|
|
// Collect Commit Histories for `main`, `next` and `dev` branches
|
2024-06-19 16:40:10 +01:00
|
|
|
open_repository.fetch()?;
|
2024-08-06 07:43:28 +01:00
|
|
|
let commit_histories = get_commit_histories(open_repository, repo_config)?;
|
2024-06-09 10:21:09 +01:00
|
|
|
// branch tips
|
2024-08-06 07:43:28 +01:00
|
|
|
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}")))?;
|
2024-06-09 10:21:09 +01:00
|
|
|
let dev = commit_histories
|
|
|
|
.dev
|
|
|
|
.first()
|
|
|
|
.cloned()
|
2024-08-06 07:43:28 +01:00
|
|
|
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {dev_branch}")))?;
|
2024-06-09 10:21:09 +01:00
|
|
|
// Validations:
|
|
|
|
// Dev must be on main branch, else the USER must rebase it
|
|
|
|
if is_not_based_on(&commit_histories.dev, &main) {
|
2024-07-21 09:32:45 +01:00
|
|
|
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,
|
2024-08-01 19:48:59 +01:00
|
|
|
dev_commit: dev,
|
|
|
|
main_commit: main,
|
2024-08-06 20:48:53 +01:00
|
|
|
log: log(repo_details),
|
2024-07-21 09:32:45 +01:00
|
|
|
},
|
|
|
|
));
|
2024-05-26 08:35:45 +01:00
|
|
|
}
|
2024-06-09 10:21:09 +01:00
|
|
|
// verify that next is on main or at most one commit on top of main, else reset it back to main
|
2024-06-28 20:40:34 +01:00
|
|
|
if is_not_based_on(
|
|
|
|
commit_histories
|
|
|
|
.next
|
|
|
|
.iter()
|
|
|
|
.take(2)
|
|
|
|
.cloned()
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.as_slice(),
|
|
|
|
&main,
|
|
|
|
) {
|
2024-06-19 07:03:08 +01:00
|
|
|
tracing::info!("Main not on same commit as next, or it's parent - resetting next to main",);
|
2024-06-19 16:40:10 +01:00
|
|
|
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
|
2024-05-26 08:35:45 +01:00
|
|
|
}
|
2024-07-28 18:50:27 +01:00
|
|
|
// 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()
|
|
|
|
{
|
2024-06-19 07:03:08 +01:00
|
|
|
tracing::info!("Next is not an ancestor of dev - resetting next to main");
|
2024-06-19 16:40:10 +01:00
|
|
|
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
|
2024-05-26 08:35:45 +01:00
|
|
|
}
|
2024-07-28 18:50:27 +01:00
|
|
|
let next_is_valid = is_based_on(&commit_histories.dev, &next);
|
2024-05-26 08:56:01 +01:00
|
|
|
Ok(git::validation::positions::Positions {
|
2024-05-26 08:35:45 +01:00
|
|
|
main,
|
|
|
|
next,
|
|
|
|
dev,
|
|
|
|
dev_commit_history: commit_histories.dev,
|
2024-07-28 18:50:27 +01:00
|
|
|
next_is_valid,
|
2024-05-26 08:35:45 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-08-01 19:48:59 +01:00
|
|
|
#[allow(clippy::result_large_err)]
|
2024-06-09 10:21:09 +01:00
|
|
|
fn reset_next_to_main(
|
2024-07-25 09:02:43 +01:00
|
|
|
open_repository: &dyn OpenRepositoryLike,
|
|
|
|
repo_details: &RepoDetails,
|
|
|
|
main: &git::Commit,
|
|
|
|
next: &git::Commit,
|
|
|
|
next_branch: &BranchName,
|
2024-06-09 10:21:09 +01:00
|
|
|
) -> Result<Positions> {
|
|
|
|
git::push::reset(
|
2024-06-19 07:03:08 +01:00
|
|
|
open_repository,
|
2024-06-09 10:21:09 +01:00
|
|
|
repo_details,
|
|
|
|
next_branch,
|
|
|
|
&main.clone().into(),
|
|
|
|
&git::push::Force::From(next.clone().into()),
|
|
|
|
)
|
|
|
|
.map_err(|err| {
|
2024-06-30 18:39:43 +01:00
|
|
|
Error::NonRetryable(format!(
|
|
|
|
"Failed to reset branch '{next_branch}' to commit '{next}': {err}"
|
|
|
|
))
|
2024-06-09 10:21:09 +01:00
|
|
|
})?;
|
2024-06-30 18:39:43 +01:00
|
|
|
Err(Error::Retryable(format!(
|
|
|
|
"Branch {next_branch} has been reset"
|
|
|
|
)))
|
2024-06-09 10:21:09 +01:00
|
|
|
}
|
|
|
|
|
2024-07-26 06:49:09 +01:00
|
|
|
fn is_not_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
|
2024-07-28 18:50:27 +01:00
|
|
|
!is_based_on(commits, needle)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
|
|
|
|
commits.iter().any(|commit| commit == needle)
|
2024-06-09 10:21:09 +01:00
|
|
|
}
|
|
|
|
|
2024-05-26 08:35:45 +01:00
|
|
|
fn get_commit_histories(
|
2024-07-25 09:02:43 +01:00
|
|
|
open_repository: &dyn OpenRepositoryLike,
|
|
|
|
repo_config: &RepoConfig,
|
2024-05-26 08:35:45 +01:00
|
|
|
) -> git::commit::log::Result<git::commit::Histories> {
|
2024-06-19 07:03:08 +01:00
|
|
|
let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
|
2024-05-26 08:35:45 +01:00
|
|
|
let main_head = [main[0].clone()];
|
2024-06-19 07:03:08 +01:00
|
|
|
let next = open_repository.commit_log(&repo_config.branches().next(), &main_head)?;
|
|
|
|
let dev = open_repository.commit_log(&repo_config.branches().dev(), &main_head)?;
|
2024-05-26 08:35:45 +01:00
|
|
|
let histories = git::commit::Histories { main, next, dev };
|
|
|
|
Ok(histories)
|
|
|
|
}
|
2024-06-09 10:21:09 +01:00
|
|
|
|
2024-06-03 08:04:48 +01:00
|
|
|
#[derive(Debug, thiserror::Error)]
|
2024-05-23 16:50:36 +01:00
|
|
|
pub enum Error {
|
2024-06-30 18:39:43 +01:00
|
|
|
#[error("{0} - will retry")]
|
|
|
|
Retryable(String),
|
2024-05-25 11:29:08 +01:00
|
|
|
|
2024-06-30 18:39:43 +01:00
|
|
|
#[error("{0} - not retrying")]
|
|
|
|
NonRetryable(String),
|
2024-07-16 20:00:29 +01:00
|
|
|
|
2024-07-21 09:32:45 +01:00
|
|
|
#[error("user intervention required")]
|
|
|
|
UserIntervention(UserNotification),
|
2024-06-30 18:39:43 +01:00
|
|
|
}
|
|
|
|
impl From<git::fetch::Error> for Error {
|
|
|
|
fn from(value: git::fetch::Error) -> Self {
|
|
|
|
Self::Retryable(value.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl From<git::commit::log::Error> for Error {
|
|
|
|
fn from(value: git::commit::log::Error) -> Self {
|
|
|
|
Self::Retryable(value.to_string())
|
|
|
|
}
|
2024-05-25 11:29:08 +01:00
|
|
|
}
|