957 lines
31 KiB
Rust
957 lines
31 KiB
Rust
pub mod error;
|
|
use error::*;
|
|
|
|
pub mod rules;
|
|
|
|
use nonempty::NonEmpty;
|
|
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::fmt;
|
|
|
|
use raw::ObjectType;
|
|
use raw::Repository;
|
|
|
|
use crate::prelude::Did;
|
|
use crate::storage::git;
|
|
|
|
use super::raw;
|
|
use super::{Oid, Qualified};
|
|
|
|
/// A collection of [`Did`]s and their [`Oid`]s that is the tip for a given
|
|
/// reference for that [`Did`].
|
|
///
|
|
/// The general construction of `Canonical` is by using the [`Canonical::new`]
|
|
/// constructor.
|
|
///
|
|
/// `Canonical` can then be used for performing calculations about the
|
|
/// canonicity of the reference, most importantly the [`Canonical::quorum`].
|
|
///
|
|
/// References to the refname and the matched rule are kept, as they
|
|
/// are very handy for generating error messages.
|
|
#[derive(Debug)]
|
|
pub struct Canonical<'a, 'b> {
|
|
refname: Qualified<'a>,
|
|
rule: &'b ValidRule,
|
|
tips: BTreeMap<Did, (Oid, CanonicalObjectType)>,
|
|
}
|
|
|
|
/// Supported Git object types for canonical computation.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum CanonicalObjectType {
|
|
/// The Git object is a commit.
|
|
Commit,
|
|
/// The Git object is a tag.
|
|
Tag,
|
|
}
|
|
|
|
impl fmt::Display for CanonicalObjectType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
CanonicalObjectType::Commit => f.write_str("commit"),
|
|
CanonicalObjectType::Tag => f.write_str("tag"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CanonicalObjectType {
|
|
/// Construct the [`CanonicalObjectType`] from a [`git2::ObjectType`].
|
|
pub fn new(kind: git::raw::ObjectType) -> Option<Self> {
|
|
match kind {
|
|
ObjectType::Commit => Some(Self::Commit),
|
|
ObjectType::Tag => Some(Self::Tag),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b> Canonical<'a, 'b> {
|
|
/// Construct the set of canonical tips given for the given `rule` and
|
|
/// the reference `refname`.
|
|
pub fn new(
|
|
repo: &Repository,
|
|
refname: Qualified<'a>,
|
|
rule: &'b ValidRule,
|
|
) -> Result<Self, CanonicalError> {
|
|
let mut tips = BTreeMap::new();
|
|
for delegate in rule.allowed().iter() {
|
|
let name = &refname.with_namespace(delegate.as_key().into());
|
|
|
|
let reference = match repo.find_reference(name) {
|
|
Ok(reference) => reference,
|
|
Err(e) if super::ext::is_not_found_err(&e) => {
|
|
log::warn!(
|
|
target: "radicle",
|
|
"Missing `{name}` while calculating the canonical reference",
|
|
);
|
|
continue;
|
|
}
|
|
Err(e) => return Err(CanonicalError::find_reference(name, e)),
|
|
};
|
|
|
|
let Some(oid) = reference.target() else {
|
|
log::warn!(target: "radicle", "Missing target for reference `{name}`");
|
|
continue;
|
|
};
|
|
|
|
let kind = Self::find_object_for(delegate, oid.into(), repo)?;
|
|
|
|
tips.insert(*delegate, (oid.into(), kind));
|
|
}
|
|
Ok(Canonical {
|
|
refname,
|
|
tips,
|
|
rule,
|
|
})
|
|
}
|
|
|
|
pub fn find_object_for(
|
|
did: &Did,
|
|
oid: Oid,
|
|
repo: &raw::Repository,
|
|
) -> Result<CanonicalObjectType, CanonicalError> {
|
|
match repo.find_object(*oid, None) {
|
|
Ok(object) => object
|
|
.kind()
|
|
.and_then(CanonicalObjectType::new)
|
|
.ok_or_else(|| {
|
|
CanonicalError::invalid_object_type(
|
|
repo.path().to_path_buf(),
|
|
*did,
|
|
oid,
|
|
object.kind(),
|
|
)
|
|
}),
|
|
Err(err) if super::ext::is_not_found_err(&err) => Err(CanonicalError::missing_object(
|
|
repo.path().to_path_buf(),
|
|
*did,
|
|
oid,
|
|
err,
|
|
)),
|
|
Err(err) => Err(CanonicalError::find_object(oid, err)),
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if there were no tips found for any of the DIDs for
|
|
/// the given reference.
|
|
///
|
|
/// N.b. this may be the case when a new reference is being created.
|
|
pub fn has_no_tips(&self) -> bool {
|
|
self.tips.is_empty()
|
|
}
|
|
|
|
pub fn refname(&self) -> &Qualified {
|
|
&self.refname
|
|
}
|
|
|
|
/// In some cases, we allow the vote to be modified. For example, when the
|
|
/// `did` is pushing a new commit, we may want to see if the new commit will
|
|
/// reach a quorum.
|
|
pub fn modify_vote(&mut self, did: Did, oid: Oid, kind: CanonicalObjectType) {
|
|
self.tips.insert(did, (oid, kind));
|
|
}
|
|
|
|
/// Check that the provided `did` is part of the set of allowed
|
|
/// DIDs of the matching rule.
|
|
pub fn is_allowed(&self, did: &Did) -> bool {
|
|
self.rule.allowed().contains(did)
|
|
}
|
|
|
|
/// Check that the provided `did` is the only DID in the set of allowed
|
|
/// DIDs of the matching rule.
|
|
pub fn is_only(&self, did: &Did) -> bool {
|
|
self.rule.allowed().is_only(did)
|
|
}
|
|
|
|
/// Checks that setting the given candidate tip would converge with at least
|
|
/// one other known tip.
|
|
///
|
|
/// It converges if the candidate Oid is either equal to, ahead of, or behind any of
|
|
/// the tips.
|
|
pub fn converges(
|
|
&self,
|
|
repo: &Repository,
|
|
(candidate, commit): (&Did, &Oid),
|
|
) -> Result<bool, ConvergesError> {
|
|
/// Ensures [`Oid`]s are of the same object type
|
|
enum Objects {
|
|
Commits(NonEmpty<Oid>),
|
|
Tags(NonEmpty<Oid>),
|
|
}
|
|
|
|
impl Objects {
|
|
fn new(oid: Oid, kind: CanonicalObjectType) -> Self {
|
|
match kind {
|
|
CanonicalObjectType::Commit => Self::Commits(NonEmpty::new(oid)),
|
|
CanonicalObjectType::Tag => Self::Tags(NonEmpty::new(oid)),
|
|
}
|
|
}
|
|
|
|
fn insert(
|
|
mut self,
|
|
oid: Oid,
|
|
kind: CanonicalObjectType,
|
|
) -> Result<Self, CanonicalObjectType> {
|
|
match self {
|
|
Objects::Commits(ref mut commits) => match kind {
|
|
CanonicalObjectType::Commit => {
|
|
commits.push(oid);
|
|
Ok(self)
|
|
}
|
|
CanonicalObjectType::Tag => Err(CanonicalObjectType::Tag),
|
|
},
|
|
Objects::Tags(ref mut tags) => match kind {
|
|
CanonicalObjectType::Commit => {
|
|
tags.push(oid);
|
|
Ok(self)
|
|
}
|
|
CanonicalObjectType::Tag => Err(CanonicalObjectType::Commit),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
let heads = {
|
|
let heads = self
|
|
.tips
|
|
.iter()
|
|
.filter_map(|(did, tip)| (did != candidate).then_some((did, tip)));
|
|
|
|
let mut objects = None;
|
|
|
|
for (did, (oid, _)) in heads {
|
|
let kind = find_object_for(did, *oid, repo)?;
|
|
let oid = *oid;
|
|
match objects {
|
|
None => objects = Some(Objects::new(oid, kind)),
|
|
Some(objs) => {
|
|
objects = Some(objs.insert(oid, kind).map_err(|expected| {
|
|
ConvergesError::mismatched_object(
|
|
repo.path().to_path_buf(),
|
|
oid,
|
|
kind,
|
|
expected,
|
|
)
|
|
})?)
|
|
}
|
|
}
|
|
}
|
|
|
|
objects
|
|
};
|
|
|
|
match heads {
|
|
None => Ok(true),
|
|
Some(Objects::Tags(_)) => Ok(true),
|
|
Some(Objects::Commits(heads)) => {
|
|
for head in heads {
|
|
let (ahead, behind) = repo
|
|
.graph_ahead_behind(**commit, *head)
|
|
.map_err(|err| ConvergesError::graph_descendant(*commit, head, err))?;
|
|
if ahead * behind == 0 {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn quorum_tag(&self) -> Result<Oid, QuorumError> {
|
|
let voting =
|
|
TagVoting::from_targets(self.tips.values().filter_map(|(commit, kind)| {
|
|
(*kind == CanonicalObjectType::Tag).then_some(*commit)
|
|
}));
|
|
let mut votes = voting.votes();
|
|
|
|
// Keep tags which pass the threshold.
|
|
votes.votes_past_threshold(self.threshold());
|
|
|
|
if votes.number_of_candidates() > 1 {
|
|
return Err(QuorumError::DivergingTags {
|
|
refname: self.refname.to_string(),
|
|
threshold: self.threshold(),
|
|
candidates: votes.candidates().cloned().collect(),
|
|
});
|
|
}
|
|
|
|
let tag = votes
|
|
.pop_first_candidate()
|
|
.ok_or(QuorumError::NoCandidates {
|
|
refname: self.refname.to_string(),
|
|
threshold: self.threshold(),
|
|
})?;
|
|
|
|
Ok((*tag).into())
|
|
}
|
|
|
|
/// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
|
|
/// and the threshold. This can be described as the latest commit that is
|
|
/// included in at least `threshold` histories. In case there are multiple tips
|
|
/// passing the threshold, and they are divergent, an error is returned.
|
|
///
|
|
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
|
/// satisified with the number of heads given.
|
|
fn quorum_commit(&self, repo: &raw::Repository) -> Result<Oid, QuorumError> {
|
|
let mut voting =
|
|
CommitVoting::from_targets(self.tips.values().filter_map(|(commit, kind)| {
|
|
(*kind == CanonicalObjectType::Commit).then_some(*commit)
|
|
}));
|
|
while let Some(targets) = voting.next_candidate() {
|
|
for (candidate, other) in targets {
|
|
let base = Oid::from(repo.merge_base(*candidate, *other)?);
|
|
voting.found_merge_base(MergeBase {
|
|
candidate,
|
|
other,
|
|
base,
|
|
});
|
|
}
|
|
}
|
|
let mut votes = voting.votes();
|
|
|
|
// Keep commits which pass the threshold.
|
|
votes.votes_past_threshold(self.threshold());
|
|
|
|
let mut longest = votes
|
|
.pop_first_candidate()
|
|
.ok_or(QuorumError::NoCandidates {
|
|
refname: self.refname.to_string(),
|
|
threshold: self.threshold(),
|
|
})?;
|
|
|
|
// Now that all scores are calculated, figure out what is the longest branch
|
|
// that passes the threshold. In case of divergence, return an error.
|
|
for head in votes.candidates() {
|
|
let base = repo.merge_base(**head, *longest)?;
|
|
|
|
if base == *longest {
|
|
// `head` is a successor of `longest`. Update `longest`.
|
|
//
|
|
// o head
|
|
// |
|
|
// o longest (base)
|
|
// |
|
|
//
|
|
longest = *head;
|
|
} else if base == **head || *head == longest {
|
|
// `head` is an ancestor of `longest`, or equal to it. Do nothing.
|
|
//
|
|
// o longest o longest, head (base)
|
|
// | |
|
|
// o head (base) OR o
|
|
// | |
|
|
//
|
|
} else {
|
|
// The merge base between `head` and `longest` (`base`)
|
|
// is neither `head` nor `longest`. Therefore, the branches have
|
|
// diverged.
|
|
//
|
|
// longest head
|
|
// \ /
|
|
// o (base)
|
|
// |
|
|
//
|
|
return Err(QuorumError::DivergingCommits {
|
|
refname: self.refname.to_string(),
|
|
threshold: self.threshold(),
|
|
base: base.into(),
|
|
longest,
|
|
head: *head,
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok((*longest).into())
|
|
}
|
|
|
|
/// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
|
|
/// and the threshold. This can be described as the latest commit that is
|
|
/// included in at least `threshold` histories. In case there are multiple tips
|
|
/// passing the threshold, and they are divergent, an error is returned.
|
|
///
|
|
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
|
/// satisified with the number of heads given.
|
|
pub fn quorum(
|
|
self,
|
|
repo: &raw::Repository,
|
|
) -> Result<(Qualified<'a>, ObjectType, Oid), QuorumError> {
|
|
let (oid, kind) = match (self.quorum_commit(repo), self.quorum_tag()) {
|
|
(Ok(commit), Err(_)) => Ok((commit, ObjectType::Commit)),
|
|
(Err(_), Ok(tag)) => Ok((tag, ObjectType::Tag)),
|
|
(Ok(_), Ok(_)) => Err(QuorumError::DifferentTypes {
|
|
refname: self.refname.clone().to_string(),
|
|
}),
|
|
(Err(commit), Err(QuorumError::NoCandidates { .. })) => Err(commit),
|
|
(Err(QuorumError::NoCandidates { .. }), Err(tag)) => Err(tag),
|
|
(Err(err), _) => Err(err),
|
|
}?;
|
|
|
|
Ok((self.refname, kind, oid))
|
|
}
|
|
|
|
fn threshold(&self) -> usize {
|
|
(*self.rule.threshold()).into()
|
|
}
|
|
}
|
|
|
|
/// Keep track of [`Votes`] for quorums involving tag objects.
|
|
struct TagVoting {
|
|
votes: Votes,
|
|
}
|
|
|
|
impl TagVoting {
|
|
fn from_targets(targets: impl Iterator<Item = Oid>) -> Self {
|
|
let votes = targets.fold(Votes::default(), |mut votes, oid| {
|
|
votes.vote(oid);
|
|
votes
|
|
});
|
|
Self { votes }
|
|
}
|
|
|
|
fn votes(self) -> Votes {
|
|
self.votes
|
|
}
|
|
}
|
|
|
|
/// Keep track of [`Votes`] for quorums involving commit objects.
|
|
///
|
|
/// Build a list of candidate commits and count how many "votes" each of them
|
|
/// has. Commits get a point for each direct vote, as well as for being part of
|
|
/// the ancestry of a commit given to this function.
|
|
#[derive(Debug)]
|
|
struct CommitVoting {
|
|
candidates: Vec<(Oid, Vec<Oid>)>,
|
|
votes: Votes,
|
|
}
|
|
|
|
impl CommitVoting {
|
|
/// Build the initial set voting.
|
|
fn from_targets(targets: impl Iterator<Item = Oid> + Clone) -> Self {
|
|
let ts = targets.clone();
|
|
let (candidates, votes) = targets.enumerate().fold(
|
|
(Vec::new(), Votes::default()),
|
|
|(mut candidates, mut votes), (i, oid)| {
|
|
candidates.push((oid, ts.clone().skip(i + 1).collect()));
|
|
votes.vote(oid);
|
|
(candidates, votes)
|
|
},
|
|
);
|
|
Self { candidates, votes }
|
|
}
|
|
|
|
/// Get the next candidate to be considered for ancestry votes.
|
|
///
|
|
/// The first of each pair will be the candidate commit, which should be
|
|
/// compared to the other commit to see what their common merge base is. The
|
|
/// merge base is then recorded using [`MergeBase`] and is recorded using
|
|
/// [`CommitVoting::found_merge_base`].
|
|
fn next_candidate(&mut self) -> Option<impl Iterator<Item = (Oid, Oid)>> {
|
|
self.candidates
|
|
.pop()
|
|
.map(|(oid, others)| others.into_iter().map(move |other| (oid, other)))
|
|
}
|
|
|
|
/// Record a merge base, and add to the vote if necessary.
|
|
fn found_merge_base(
|
|
&mut self,
|
|
MergeBase {
|
|
candidate,
|
|
other,
|
|
base,
|
|
}: MergeBase,
|
|
) {
|
|
// Avoid double counting the same commits
|
|
let is_same = candidate == other;
|
|
if !is_same && (base == candidate || base == other) {
|
|
self.votes.vote(base);
|
|
}
|
|
}
|
|
|
|
/// Finish the voting process and get the [`Votes`] from the
|
|
/// [`CommitVoting`].
|
|
fn votes(self) -> Votes {
|
|
self.votes
|
|
}
|
|
}
|
|
|
|
/// Record a merge base between `candidate` and `other`.
|
|
struct MergeBase {
|
|
/// The candidate commit for the merge base.
|
|
candidate: Oid,
|
|
/// The commit that is being compared against for the merge base.
|
|
other: Oid,
|
|
/// The computed merge base commit.
|
|
base: Oid,
|
|
}
|
|
|
|
/// Count the number of votes per [`Oid`].
|
|
///
|
|
/// Note that the count cannot exceed 255, since that is the maximum number the
|
|
/// `threshold` value can be.
|
|
#[derive(Debug, Default, PartialEq, Eq)]
|
|
struct Votes {
|
|
inner: BTreeMap<Oid, u8>,
|
|
}
|
|
|
|
impl Votes {
|
|
/// Increase the vote count for `oid`.
|
|
///
|
|
/// If `oid` does not exist in the set of [`Votes`] yet, then no vote will
|
|
/// be added.
|
|
#[inline]
|
|
fn vote(&mut self, oid: Oid) {
|
|
self.safe_inc(oid, 1);
|
|
}
|
|
|
|
/// Filter the candidates by the ones that have a number of votes that pass
|
|
/// the `threshold`.
|
|
#[inline]
|
|
fn votes_past_threshold(&mut self, threshold: usize) {
|
|
self.inner.retain(|_, votes| *votes as usize >= threshold);
|
|
}
|
|
|
|
/// Get the number of candidates this set of votes has.
|
|
#[inline]
|
|
fn number_of_candidates(&self) -> usize {
|
|
self.inner.len()
|
|
}
|
|
|
|
/// Get the set candidates.
|
|
#[inline]
|
|
fn candidates(&self) -> impl Iterator<Item = &Oid> {
|
|
self.inner.keys()
|
|
}
|
|
|
|
/// Pop off the first candidate from the set of votes.
|
|
#[inline]
|
|
fn pop_first_candidate(&mut self) -> Option<Oid> {
|
|
self.inner.pop_first().map(|(oid, _)| oid)
|
|
}
|
|
|
|
#[inline]
|
|
fn safe_inc(&mut self, oid: Oid, n: u8) {
|
|
let votes = self.inner.entry(oid).or_default();
|
|
*votes = votes.saturating_add(n);
|
|
}
|
|
}
|
|
|
|
fn find_object_for(
|
|
did: &Did,
|
|
oid: Oid,
|
|
repo: &raw::Repository,
|
|
) -> Result<CanonicalObjectType, FindObjectError> {
|
|
match repo.find_object(*oid, None) {
|
|
Ok(object) => object
|
|
.kind()
|
|
.and_then(CanonicalObjectType::new)
|
|
.ok_or_else(|| {
|
|
FindObjectError::invalid_object_type(
|
|
repo.path().to_path_buf(),
|
|
*did,
|
|
oid,
|
|
object.kind(),
|
|
)
|
|
}),
|
|
Err(err) if super::ext::is_not_found_err(&err) => Err(FindObjectError::missing_object(
|
|
repo.path().to_path_buf(),
|
|
*did,
|
|
oid,
|
|
err,
|
|
)),
|
|
Err(err) => Err(FindObjectError::find_object(oid, err)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used)]
|
|
mod tests {
|
|
|
|
use super::*;
|
|
use crate::assert_matches;
|
|
use crate::git;
|
|
use crate::node::device::Device;
|
|
use crate::test::fixtures;
|
|
|
|
/// Test helper to construct a Canonical and get the quorum
|
|
fn quorum(
|
|
heads: &[git::raw::Oid],
|
|
threshold: usize,
|
|
repo: &git::raw::Repository,
|
|
) -> Result<Oid, QuorumError> {
|
|
let tips: BTreeMap<Did, (Oid, CanonicalObjectType)> = heads
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, head)| {
|
|
let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
|
|
let did = Did::from(signer.public_key());
|
|
let kind = repo
|
|
.find_object(*head, None)
|
|
.unwrap()
|
|
.kind()
|
|
.and_then(CanonicalObjectType::new)
|
|
.unwrap();
|
|
(did, ((*head).into(), kind))
|
|
})
|
|
.collect();
|
|
|
|
let refname =
|
|
git::refs::branch(git_ext::ref_format::RefStr::try_from_str("master").unwrap());
|
|
|
|
let rule: RawRule = crate::git::canonical::rules::Rule::new(
|
|
crate::git::canonical::rules::Allowed::Delegates,
|
|
threshold,
|
|
);
|
|
let delegates = crate::identity::doc::Delegates::new(tips.keys().cloned()).unwrap();
|
|
let rule = rule.validate(&mut || delegates.clone()).unwrap();
|
|
|
|
Canonical {
|
|
refname,
|
|
tips,
|
|
rule: &rule,
|
|
}
|
|
.quorum(repo)
|
|
.map(|(_, _, oid)| oid)
|
|
}
|
|
|
|
#[test]
|
|
fn test_quorum_properties() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, c0) = fixtures::repository(tmp.path());
|
|
let c0: git::Oid = c0.into();
|
|
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
|
let a2 = fixtures::commit("A2", &[*a1], &repo);
|
|
let d1 = fixtures::commit("D1", &[*c0], &repo);
|
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
let c2 = fixtures::commit("C2", &[*c1], &repo);
|
|
let b2 = fixtures::commit("B2", &[*c1], &repo);
|
|
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
|
let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
|
|
let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
|
|
let mut rng = fastrand::Rng::new();
|
|
let choices = [*c0, *c1, *c2, *b2, *a1, *a2, *d1, *m1, *m2];
|
|
|
|
for _ in 0..100 {
|
|
let count = rng.usize(1..=choices.len());
|
|
let threshold = rng.usize(1..=count);
|
|
let mut heads = Vec::new();
|
|
|
|
for _ in 0..count {
|
|
let ix = rng.usize(0..choices.len());
|
|
heads.push(choices[ix]);
|
|
}
|
|
rng.shuffle(&mut heads);
|
|
|
|
if let Ok(canonical) = quorum(&heads, threshold, &repo) {
|
|
assert!(heads.contains(&canonical));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_quorum_groups() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, c0) = fixtures::repository(tmp.path());
|
|
let c0: git::Oid = c0.into();
|
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
let c2 = fixtures::commit("C2", &[*c0], &repo);
|
|
|
|
eprintln!("C0: {c0}");
|
|
eprintln!("C1: {c1}");
|
|
eprintln!("C2: {c2}");
|
|
|
|
assert_matches!(
|
|
quorum(&[*c1, *c2, *c1, *c2], 2, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
|
|
assert_matches!(
|
|
quorum(&[*c1, *c2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quorum_tag() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, c0) = fixtures::repository(tmp.path());
|
|
let c0: git::Oid = c0.into();
|
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
let t1 = fixtures::tag("v1", "T1", *c1, &repo);
|
|
let t2 = fixtures::tag("v2", "T2", *c1, &repo);
|
|
|
|
eprintln!("C0: {c0}");
|
|
eprintln!("C1: {c1}");
|
|
eprintln!("T1: {t1}");
|
|
eprintln!("T2: {t2}");
|
|
|
|
assert_eq!(quorum(&[*t1], 1, &repo).unwrap(), t1);
|
|
assert_eq!(quorum(&[*t1, *t1], 2, &repo).unwrap(), t1);
|
|
|
|
assert_matches!(
|
|
quorum(&[*t1, *t2], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
|
|
assert_matches!(
|
|
quorum(&[*t1, *c1], 1, &repo),
|
|
Err(QuorumError::DifferentTypes { .. })
|
|
);
|
|
|
|
assert_matches!(
|
|
quorum(&[*t1, *t2], 1, &repo),
|
|
Err(QuorumError::DivergingTags { .. })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quorum() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, c0) = fixtures::repository(tmp.path());
|
|
let c0: git::Oid = c0.into();
|
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
let c2 = fixtures::commit("C2", &[*c1], &repo);
|
|
let c3 = fixtures::commit("C3", &[*c1], &repo);
|
|
let b2 = fixtures::commit("B2", &[*c1], &repo);
|
|
let a1 = fixtures::commit("A1", &[*c0], &repo);
|
|
let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
|
|
let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
|
|
|
|
eprintln!("C0: {c0}");
|
|
eprintln!("C1: {c1}");
|
|
eprintln!("C2: {c2}");
|
|
eprintln!("C3: {c3}");
|
|
eprintln!("B2: {b2}");
|
|
eprintln!("A1: {a1}");
|
|
eprintln!("M1: {m1}");
|
|
eprintln!("M2: {m2}");
|
|
|
|
assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
|
|
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
|
assert_eq!(quorum(&[*c2], 1, &repo).unwrap(), c2);
|
|
|
|
// C1
|
|
// |
|
|
// C0
|
|
assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
|
|
|
|
// C2
|
|
// |
|
|
// C1
|
|
// |
|
|
// C0
|
|
assert_eq!(quorum(&[*c1, *c2], 1, &repo).unwrap(), c2);
|
|
assert_eq!(quorum(&[*c1, *c2], 2, &repo).unwrap(), c1);
|
|
assert_eq!(quorum(&[*c0, *c1, *c2], 3, &repo).unwrap(), c0);
|
|
assert_eq!(quorum(&[*c1, *c1, *c2], 2, &repo).unwrap(), c1);
|
|
assert_eq!(quorum(&[*c1, *c1, *c2], 1, &repo).unwrap(), c2);
|
|
assert_eq!(quorum(&[*c2, *c2, *c1], 1, &repo).unwrap(), c2);
|
|
|
|
// B2 C2
|
|
// \|
|
|
// C1
|
|
// |
|
|
// C0
|
|
assert_matches!(
|
|
quorum(&[*c1, *c2, *b2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*c2, *b2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*b2, *c2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*c2, *b2], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*b2, *c2], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_eq!(quorum(&[*c1, *c2, *b2], 2, &repo).unwrap(), c1);
|
|
assert_eq!(quorum(&[*c1, *c2, *b2], 3, &repo).unwrap(), c1);
|
|
assert_eq!(quorum(&[*b2, *b2, *c2], 2, &repo).unwrap(), b2);
|
|
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
|
assert_matches!(
|
|
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
|
|
// B2 C2 C3
|
|
// \ | /
|
|
// C1
|
|
// |
|
|
// C0
|
|
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
|
assert_matches!(
|
|
quorum(&[*b2, *c2, *c2], 3, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
|
|
// B2 C2
|
|
// \|
|
|
// A1 C1
|
|
// \|
|
|
// C0
|
|
assert_matches!(
|
|
quorum(&[*c2, *b2, *a1], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*c2, *b2, *a1], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*c2, *b2, *a1], 3, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 2, &repo).unwrap(), c1,);
|
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 3, &repo).unwrap(), c1,);
|
|
assert_eq!(quorum(&[*c0, *c2, *b2, *a1], 3, &repo).unwrap(), c0);
|
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
|
assert_matches!(
|
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*a1, *a1, *c2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
|
|
// M2 M1
|
|
// /\ /\
|
|
// \ B2 C2
|
|
// \ \|
|
|
// A1 C1
|
|
// \|
|
|
// C0
|
|
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
|
assert_matches!(
|
|
quorum(&[*m1, *m2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m2, *m1], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m1, *m2], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m1, *m2, *c2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m1, *a1], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m1, *a1], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_eq!(quorum(&[*m1, *m2, *b2, *c1], 4, &repo).unwrap(), c1);
|
|
assert_eq!(quorum(&[*m1, *m1, *b2], 2, &repo).unwrap(), m1);
|
|
assert_eq!(quorum(&[*m1, *m1, *c2], 2, &repo).unwrap(), m1);
|
|
assert_eq!(quorum(&[*m2, *m2, *b2], 2, &repo).unwrap(), m2);
|
|
assert_eq!(quorum(&[*m2, *m2, *a1], 2, &repo).unwrap(), m2);
|
|
assert_eq!(quorum(&[*m1, *m1, *b2, *b2], 2, &repo).unwrap(), m1);
|
|
assert_eq!(quorum(&[*m1, *m1, *c2, *c2], 2, &repo).unwrap(), m1);
|
|
assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 3, &repo).unwrap(), c1);
|
|
assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 4, &repo).unwrap(), c0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_quorum_merges() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, c0) = fixtures::repository(tmp.path());
|
|
let c0: git::Oid = c0.into();
|
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
|
let c2 = fixtures::commit("C2", &[*c0], &repo);
|
|
let c3 = fixtures::commit("C3", &[*c0], &repo);
|
|
|
|
let m1 = fixtures::commit("M1", &[*c1, *c2], &repo);
|
|
let m2 = fixtures::commit("M2", &[*c2, *c3], &repo);
|
|
|
|
eprintln!("C0: {c0}");
|
|
eprintln!("C1: {c1}");
|
|
eprintln!("C2: {c2}");
|
|
eprintln!("C3: {c3}");
|
|
eprintln!("M1: {m1}");
|
|
eprintln!("M2: {m2}");
|
|
|
|
// M2 M1
|
|
// /\ /\
|
|
// C1 C2 C3
|
|
// \| /
|
|
// C0
|
|
assert_matches!(
|
|
quorum(&[*m1, *m2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m1, *m2], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
|
|
let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
|
|
|
|
// M3/M2 M1
|
|
// /\ /\
|
|
// C1 C2 C3
|
|
// \| /
|
|
// C0
|
|
assert_matches!(
|
|
quorum(&[*m1, *m3], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m1, *m3], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m3, *m1], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m3, *m1], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m3, *m2], 1, &repo),
|
|
Err(QuorumError::DivergingCommits { .. })
|
|
);
|
|
assert_matches!(
|
|
quorum(&[*m3, *m2], 2, &repo),
|
|
Err(QuorumError::NoCandidates { .. })
|
|
);
|
|
}
|
|
}
|