git-next-core 0.14.1

core for git-next, the trunk-based development manager
Documentation
//
use crate::{
    git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
    s, BranchName, RepoConfig,
};
use tracing::{debug, instrument};

pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug)]
pub struct Positions {
    pub main: git::Commit,
    pub next: git::Commit,
    pub dev: git::Commit,
    pub dev_commit_history: Vec<git::Commit>,
    pub next_is_valid: bool,
}

/// 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.
#[allow(clippy::result_large_err)]
pub fn validate(
    open_repository: &dyn OpenRepositoryLike,
    repo_details: &git::RepoDetails,
    repo_config: &RepoConfig,
) -> Result<(Positions, git::graph::Log)> {
    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 git_log = git::graph::log(repo_details);

    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,
                dev_commit: dev,
                main_commit: main,
                log: git_log,
            },
        ));
    }
    // 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::<Vec<_>>()
            .as_slice(),
        &main,
    ) {
        tracing::info!("Main not on same commit as next, or it's parent - resetting next to main",);
        return Err(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 dev not ahead of main
    if is_not_based_on(&commit_histories.dev, &next)
        && commit_histories.main.first() == commit_histories.dev.first()
    {
        tracing::info!("Next is not an ancestor of dev - resetting next to main");
        return Err(reset_next_to_main(
            open_repository,
            repo_details,
            &main,
            &next,
            &next_branch,
        ));
    }
    let next_is_valid = is_based_on(&commit_histories.dev, &next);
    Ok((
        git::validation::positions::Positions {
            main,
            next,
            dev,
            dev_commit_history: commit_histories.dev,
            next_is_valid,
        },
        git_log,
    ))
}

#[allow(clippy::result_large_err)]
fn reset_next_to_main(
    open_repository: &dyn OpenRepositoryLike,
    repo_details: &RepoDetails,
    main: &git::Commit,
    next: &git::Commit,
    next_branch: &BranchName,
) -> Error {
    match git::push::reset(
        open_repository,
        repo_details,
        next_branch,
        &main.clone().into(),
        &git::push::Force::From(next.clone().into()),
    ) {
        Ok(()) => Error::Retryable(format!("Branch {next_branch} has been reset")),
        Err(err) => Error::NonRetryable(format!(
            "Failed to reset branch '{next_branch}' to commit '{next}': {err}"
        )),
    }
}

fn is_not_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
    !is_based_on(commits, needle)
}

fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
    commits.iter().any(|commit| commit == needle)
}

/// Returns the commit logs for the main, next and dev branches
///
/// # Errors
///
/// Will return `Err` if there are any problems with the branch names being invalid, or any
/// corruption of the git repository.
#[instrument]
pub fn get_commit_histories(
    open_repository: &dyn OpenRepositoryLike,
    repo_config: &RepoConfig,
) -> git::commit::log::Result<git::commit::Histories> {
    debug!("main...");
    let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
    let main_head = [main[0].clone()];
    debug!("next");
    let next = open_repository.commit_log(&repo_config.branches().next(), &main_head)?;
    debug!("dev");
    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<git::fetch::Error> for Error {
    fn from(value: git::fetch::Error) -> Self {
        Self::Retryable(s!(value))
    }
}
impl From<git::commit::log::Error> for Error {
    fn from(value: git::commit::log::Error) -> Self {
        Self::Retryable(s!(value))
    }
}