git_next_core/git/validation/
positions.rs1use 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#[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 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 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 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 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 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#[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}