git_next_core/git/validation/
positions.rs

1//
2use crate::{
3    git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
4    s, BranchName, RepoConfig,
5};
6use tracing::{debug, instrument};
7
8pub type Result<T> = core::result::Result<T, Error>;
9
10#[derive(Debug)]
11pub struct Positions {
12    pub main: git::Commit,
13    pub next: git::Commit,
14    pub dev: git::Commit,
15    pub dev_commit_history: Vec<git::Commit>,
16    pub next_is_valid: bool,
17}
18
19/// Validates the relative positions of the three branches, resetting next back to main if
20/// it has gone astry.
21///
22/// # Errors
23///
24/// Will return an `Err` if any of the branches has no commits, or if user intervention is
25/// required, or if there is an error resetting the next branch back to main.
26#[allow(clippy::result_large_err)]
27pub fn validate(
28    open_repository: &dyn OpenRepositoryLike,
29    repo_details: &git::RepoDetails,
30    repo_config: &RepoConfig,
31) -> Result<(Positions, git::graph::Log)> {
32    let main_branch = repo_config.branches().main();
33    let next_branch = repo_config.branches().next();
34    let dev_branch = repo_config.branches().dev();
35    // Collect Commit Histories for `main`, `next` and `dev` branches
36    open_repository.fetch()?;
37    let git_log = git::graph::log(repo_details);
38
39    let commit_histories = get_commit_histories(open_repository, repo_config)?;
40    // branch tips
41    let main = commit_histories
42        .main
43        .first()
44        .cloned()
45        .ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {main_branch}")))?;
46    let next = commit_histories
47        .next
48        .first()
49        .cloned()
50        .ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {next_branch}")))?;
51    let dev = commit_histories
52        .dev
53        .first()
54        .cloned()
55        .ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {dev_branch}")))?;
56    // Validations:
57    // Dev must be on main branch, else the USER must rebase it
58    if is_not_based_on(&commit_histories.dev, &main) {
59        return Err(Error::UserIntervention(
60            UserNotification::DevNotBasedOnMain {
61                forge_alias: repo_details.forge.forge_alias().clone(),
62                repo_alias: repo_details.repo_alias.clone(),
63                dev_branch,
64                main_branch,
65                dev_commit: dev,
66                main_commit: main,
67                log: git_log,
68            },
69        ));
70    }
71    // verify that next is on main or at most one commit on top of main, else reset it back to main
72    if is_not_based_on(
73        commit_histories
74            .next
75            .iter()
76            .take(2)
77            .cloned()
78            .collect::<Vec<_>>()
79            .as_slice(),
80        &main,
81    ) {
82        tracing::info!("Main not on same commit as next, or it's parent - resetting next to main",);
83        return Err(reset_next_to_main(
84            open_repository,
85            repo_details,
86            &main,
87            &next,
88            &next_branch,
89        ));
90    }
91    // verify that next is an ancestor of dev, else reset it back to main if dev not ahead of main
92    if is_not_based_on(&commit_histories.dev, &next)
93        && commit_histories.main.first() == commit_histories.dev.first()
94    {
95        tracing::info!("Next is not an ancestor of dev - resetting next to main");
96        return Err(reset_next_to_main(
97            open_repository,
98            repo_details,
99            &main,
100            &next,
101            &next_branch,
102        ));
103    }
104    let next_is_valid = is_based_on(&commit_histories.dev, &next);
105    Ok((
106        git::validation::positions::Positions {
107            main,
108            next,
109            dev,
110            dev_commit_history: commit_histories.dev,
111            next_is_valid,
112        },
113        git_log,
114    ))
115}
116
117#[allow(clippy::result_large_err)]
118fn reset_next_to_main(
119    open_repository: &dyn OpenRepositoryLike,
120    repo_details: &RepoDetails,
121    main: &git::Commit,
122    next: &git::Commit,
123    next_branch: &BranchName,
124) -> Error {
125    match git::push::reset(
126        open_repository,
127        repo_details,
128        next_branch,
129        &main.clone().into(),
130        &git::push::Force::From(next.clone().into()),
131    ) {
132        Ok(()) => Error::Retryable(format!("Branch {next_branch} has been reset")),
133        Err(err) => Error::NonRetryable(format!(
134            "Failed to reset branch '{next_branch}' to commit '{next}': {err}"
135        )),
136    }
137}
138
139fn is_not_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
140    !is_based_on(commits, needle)
141}
142
143fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
144    commits.iter().any(|commit| commit == needle)
145}
146
147/// Returns the commit logs for the main, next and dev branches
148///
149/// # Errors
150///
151/// Will return `Err` if there are any problems with the branch names being invalid, or any
152/// corruption of the git repository.
153#[instrument]
154pub fn get_commit_histories(
155    open_repository: &dyn OpenRepositoryLike,
156    repo_config: &RepoConfig,
157) -> git::commit::log::Result<git::commit::Histories> {
158    debug!("main...");
159    let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
160    let main_head = [main[0].clone()];
161    debug!("next");
162    let next = open_repository.commit_log(&repo_config.branches().next(), &main_head)?;
163    debug!("dev");
164    let dev = open_repository.commit_log(&repo_config.branches().dev(), &main_head)?;
165    let histories = git::commit::Histories { main, next, dev };
166    Ok(histories)
167}
168
169#[derive(Debug, thiserror::Error)]
170pub enum Error {
171    #[error("{0} - will retry")]
172    Retryable(String),
173
174    #[error("{0} - not retrying")]
175    NonRetryable(String),
176
177    #[error("user intervention required")]
178    UserIntervention(UserNotification),
179}
180impl From<git::fetch::Error> for Error {
181    fn from(value: git::fetch::Error) -> Self {
182        Self::Retryable(s!(value))
183    }
184}
185impl From<git::commit::log::Error> for Error {
186    fn from(value: git::commit::log::Error) -> Self {
187        Self::Retryable(s!(value))
188    }
189}