use std::{fmt, ops::ControlFlow};
use crate::git::Oid;
use crate::prelude::Did;
use super::{Object, effects, error};
/// Checks for convergence and ensures that compared objects are of the same
/// type, i.e. commit or tag, to the [`Candidate`].
pub(super) struct Convergence<'r, R> {
repo: &'r R,
checker: Candidate,
}
impl<'r, R> fmt::Debug for Convergence<'r, R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Convergence")
.field("checker", &self.checker)
.finish()
}
}
impl<'r, R> Convergence<'r, R>
where
R: effects::Ancestry,
{
pub fn new(repo: &'r R, candidate: Did, object: Object) -> Self {
Self {
repo,
checker: Candidate::new(candidate, object),
}
}
/// For each voter in `voters`:
/// 1. If the [`Object`] is a commit:
///
/// a. Ensure that the candidate and voting commit have a linear
/// relationship.
/// b. That [`Object`]'s type matches type of the [`Candidate`].
///
/// 2. If the [`Object`] is a tag, then ensure the [`Candidate`] object is
/// a tag.
/// 3. Always skip a vote that is the same as the [`Candidate`].
pub fn check<'a, I>(self, voters: I) -> Result<Option<(Did, Object)>, error::ConvergesError>
where
I: Iterator<Item = (&'a Did, &'a Object)>,
{
let mut converges = false;
for (did, object) in voters {
match self.checker.compare_to_candidate(did, *object) {
ControlFlow::Continue(c) => match c {
Effect::GraphCheck { commit, upstream } => {
converges |= self.repo.graph_ahead_behind(commit, upstream)?.is_linear();
}
Effect::TagConverges => {
converges = true;
continue;
}
Effect::SkipSelf => continue,
},
ControlFlow::Break(ConvergenceMismatch { expected, found }) => {
return Err(error::ConvergesError::mismatched_object(
expected.id(),
found.object_type(),
expected.object_type(),
));
}
}
}
Ok(converges.then_some((self.checker.candidate, self.checker.object)))
}
}
/// The candidate and their [`Object`] they are attempting to converge with.
#[derive(Debug)]
struct Candidate {
candidate: Did,
object: Object,
}
/// The "effect" that needs to be performed due to the result of
/// [`Candidate::compare_to_candidate`].
enum Effect {
/// Perform a check of the commit graph using the `commit` and `upstream`.
GraphCheck { commit: Oid, upstream: Oid },
/// Mark that tags always converge – there is no ancestry check.
TagConverges,
/// Skip the [`Did`] since it is the same as the [`Candidate`].
SkipSelf,
}
/// The two [`Object`]s have different types.
pub(super) struct ConvergenceMismatch {
expected: Object,
found: Object,
}
impl Candidate {
fn new(candidate: Did, object: Object) -> Self {
Self { candidate, object }
}
fn compare_to_candidate(
&self,
did: &Did,
object: Object,
) -> ControlFlow<ConvergenceMismatch, Effect> {
if &self.candidate == did {
return ControlFlow::Continue(Effect::SkipSelf);
}
match (self.object, object) {
(e @ Object::Commit { .. }, f @ Object::Tag { .. })
| (e @ Object::Tag { .. }, f @ Object::Commit { .. }) => {
ControlFlow::Break(ConvergenceMismatch {
expected: e,
found: f,
})
}
(Object::Commit { id: commit }, Object::Commit { id: upstream }) => {
ControlFlow::Continue(Effect::GraphCheck { commit, upstream })
}
(Object::Tag { .. }, Object::Tag { .. }) => ControlFlow::Continue(Effect::TagConverges),
}
}
}