forked from kemitix/git-next
199 lines
6.9 KiB
Rust
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,
|
|
},
|
|
}
|