Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: port quorum code to Canonical
Fintan Halpenny committed 1 year ago
commit 54d23545ff70903cfa4e513b33eb2f920debcaa6
parent 7fd9c3be8f2c63c673bd7f105f4672510de89ee2
4 files changed +280 -352
modified radicle-remote-helper/src/push.rs
@@ -105,7 +105,7 @@ pub enum Error {
    Repository(#[from] radicle::storage::RepositoryError),
    /// Quorum error.
    #[error(transparent)]
-
    Quorum(#[from] radicle::storage::git::QuorumError),
+
    Quorum(#[from] radicle::git::canonical::QuorumError),
}

/// Push command.
modified radicle/src/git/canonical.rs
@@ -28,7 +28,7 @@ pub struct Canonical {
#[derive(Debug, Error)]
pub enum QuorumError {
    /// Could not determine a quorum [`Oid`].
-
    #[error("no quorum was  found")]
+
    #[error("no quorum was found")]
    NoQuorum,
    /// An error occurred from [`git2`].
    #[error(transparent)]
@@ -69,7 +69,12 @@ impl Canonical {
                Ok(tip) => {
                    tips.insert(*delegate, tip);
                }
-
                Err(e) if super::ext::is_not_found_err(&e) => {}
+
                Err(e) if super::ext::is_not_found_err(&e) => {
+
                    log::warn!(
+
                        target: "radicle",
+
                        "Missing `refs/namespaces/{delegate}/{name}` while calculating the canonical reference"
+
                    );
+
                }
                Err(e) => return Err(e),
            }
        }
@@ -185,3 +190,270 @@ impl Canonical {
        Ok((*longest).into())
    }
}
+

+
#[cfg(test)]
+
#[allow(clippy::unwrap_used)]
+
mod tests {
+
    use crypto::test::signer::MockSigner;
+
    use radicle_crypto::Signer;
+

+
    use super::*;
+
    use crate::assert_matches;
+
    use crate::git;
+
    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 = heads
+
            .iter()
+
            .enumerate()
+
            .map(|(i, head)| {
+
                let signer = MockSigner::from_seed([(i + 1) as u8; 32]);
+
                let did = Did::from(signer.public_key());
+
                (did, (*head).into())
+
            })
+
            .collect();
+
        Canonical { tips }.quorum(threshold, repo)
+
    }
+

+
    #[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() {
+
        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);
+
        assert_eq!(quorum(&[*c0], 0, &repo).unwrap(), c0);
+
        assert_matches!(quorum(&[], 0, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*c0], 2, &repo), Err(QuorumError::NoQuorum));
+

+
        //  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::NoQuorum)
+
        );
+
        assert_matches!(quorum(&[*c2, *b2], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*b2, *c2], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*c2, *b2], 2, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*b2, *c2], 2, &repo), Err(QuorumError::NoQuorum));
+
        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::NoQuorum)
+
        );
+

+
        // 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::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+

+
        //  B2 C2
+
        //    \|
+
        // A1 C1
+
        //   \|
+
        //   C0
+
        assert_matches!(
+
            quorum(&[*c2, *b2, *a1], 1, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*c2, *b2, *a1], 2, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*c2, *b2, *a1], 3, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        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::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*a1, *a1, *c2], 1, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(
+
            quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+

+
        //    M2  M1
+
        //    /\  /\
+
        //    \ B2 C2
+
        //     \  \|
+
        //     A1 C1
+
        //       \|
+
        //       C0
+
        assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
+
        assert_matches!(quorum(&[*m1, *m2], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m2, *m1], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(
+
            quorum(&[*m1, *m2, *c2], 1, &repo),
+
            Err(QuorumError::NoQuorum)
+
        );
+
        assert_matches!(quorum(&[*m1, *a1], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m1, *a1], 2, &repo), Err(QuorumError::NoQuorum));
+
        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::NoQuorum));
+
        assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
+

+
        let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
+

+
        //   M3/M2 M1
+
        //    /\  /\
+
        //   C1 C2 C3
+
        //     \| /
+
        //      C0
+
        assert_matches!(quorum(&[*m1, *m3], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m1, *m3], 2, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m3, *m1], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m3, *m1], 2, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m3, *m2], 1, &repo), Err(QuorumError::NoQuorum));
+
        assert_matches!(quorum(&[*m3, *m2], 2, &repo), Err(QuorumError::NoQuorum));
+
    }
+
}
modified radicle/src/storage.rs
@@ -16,7 +16,7 @@ pub use radicle_git_ext::Oid;

use crate::cob;
use crate::collections::RandomMap;
-
use crate::git::ext as git_ext;
+
use crate::git::{canonical, ext as git_ext};
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, RefString};
use crate::identity::{Did, PayloadError};
use crate::identity::{Doc, DocAt, DocError};
@@ -116,7 +116,7 @@ pub enum RepositoryError {
    #[error(transparent)]
    GitExt(#[from] git_ext::Error),
    #[error(transparent)]
-
    Quorum(#[from] git::QuorumError),
+
    Quorum(#[from] canonical::QuorumError),
    #[error(transparent)]
    Refs(#[from] refs::Error),
}
modified radicle/src/storage/git.rs
@@ -12,6 +12,7 @@ use once_cell::sync::Lazy;
use tempfile::TempDir;

use crate::crypto::Unverified;
+
use crate::git::canonical::Canonical;
use crate::identity::doc::DocError;
use crate::identity::{doc::DocAt, Doc, RepoId};
use crate::identity::{Identity, Project};
@@ -743,25 +744,8 @@ impl ReadRepository for Repository {
        let project = doc.project()?;
        let branch_ref = git::refs::branch(project.default_branch());
        let raw = self.raw();
-
        let mut heads = Vec::new();
-

-
        for delegate in doc.delegates.iter() {
-
            let r = match self.reference_oid(delegate, &branch_ref) {
-
                Ok(oid) => oid,
-
                Err(e) if ext::is_not_found_err(&e) => {
-
                    log::warn!(
-
                        target: "radicle",
-
                        "Missing `refs/namespaces/{delegate}/{branch_ref}` while calculating the canonical head"
-
                    );
-
                    continue;
-
                }
-
                Err(e) => return Err(e.into()),
-
            };
-

-
            heads.push(*r);
-
        }
-

-
        let oid = self::quorum(&heads, doc.threshold, raw)?;
+
        let oid = Canonical::default_branch(self, &project, &doc.delegates)?
+
            .quorum(doc.threshold, raw)?;
        Ok((branch_ref, oid))
    }

@@ -896,96 +880,6 @@ impl SignRepository for Repository {
        Ok(signed)
    }
}
-

-
#[derive(Debug, Error)]
-
pub enum QuorumError {
-
    #[error("no quorum was found")]
-
    NoQuorum,
-
    #[error(transparent)]
-
    Git(#[from] git2::Error),
-
}
-

-
/// Computes the quorum or "canonical" head based on the given heads and the
-
/// threshold. This can be described as the latest commit that is included in
-
/// at least `threshold` histories. In case there are multiple heads 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(
-
    heads: &[git::raw::Oid],
-
    threshold: usize,
-
    repo: &git::raw::Repository,
-
) -> Result<Oid, QuorumError> {
-
    let mut candidates = BTreeMap::<_, usize>::new();
-

-
    // 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. Only commits given to the function are considered.
-
    for (i, head) in heads.iter().enumerate() {
-
        let head = Oid::from(*head);
-

-
        // Add a direct vote for this head.
-
        *candidates.entry(head).or_default() += 1;
-

-
        // Compare this head to all other heads ahead of it in the list.
-
        for other in heads.iter().skip(i + 1) {
-
            // N.b. if heads are equal then skip it, otherwise it will end up as
-
            // a double vote.
-
            if *head == *other {
-
                continue;
-
            }
-
            let base = repo.merge_base(*head, *other)?;
-

-
            if base == *other || base == *head {
-
                *candidates.entry(Oid::from(base)).or_default() += 1;
-
            }
-
        }
-
    }
-
    // Keep commits which pass the threshold.
-
    candidates.retain(|_, votes| *votes >= threshold);
-

-
    // Keep track of the longest identity branch.
-
    let (mut longest, _) = candidates.pop_first().ok_or(QuorumError::NoQuorum)?;
-

-
    // 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 candidates.keys() {
-
        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::NoQuorum);
-
        }
-
    }
-
    Ok((*longest).into())
-
}
-

pub mod trailers {
    use std::str::FromStr;

@@ -1045,7 +939,6 @@ mod tests {
    use crypto::test::signer::MockSigner;

    use super::*;
-
    use crate::assert_matches;
    use crate::git;
    use crate::storage::refs::SIGREFS_BRANCH;
    use crate::storage::{ReadRepository, ReadStorage};
@@ -1053,243 +946,6 @@ mod tests {
    use crate::test::fixtures;

    #[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() {
-
        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);
-
        assert_eq!(quorum(&[*c0], 0, &repo).unwrap(), c0);
-
        assert_matches!(quorum(&[], 0, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*c0], 2, &repo), Err(QuorumError::NoQuorum));
-

-
        //  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::NoQuorum)
-
        );
-
        assert_matches!(quorum(&[*c2, *b2], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*b2, *c2], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*c2, *b2], 2, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*b2, *c2], 2, &repo), Err(QuorumError::NoQuorum));
-
        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::NoQuorum)
-
        );
-

-
        // 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::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-

-
        //  B2 C2
-
        //    \|
-
        // A1 C1
-
        //   \|
-
        //   C0
-
        assert_matches!(
-
            quorum(&[*c2, *b2, *a1], 1, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*c2, *b2, *a1], 2, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*c2, *b2, *a1], 3, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        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::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*a1, *a1, *c2], 1, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(
-
            quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-

-
        //    M2  M1
-
        //    /\  /\
-
        //    \ B2 C2
-
        //     \  \|
-
        //     A1 C1
-
        //       \|
-
        //       C0
-
        assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
-
        assert_matches!(quorum(&[*m1, *m2], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m2, *m1], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(
-
            quorum(&[*m1, *m2, *c2], 1, &repo),
-
            Err(QuorumError::NoQuorum)
-
        );
-
        assert_matches!(quorum(&[*m1, *a1], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m1, *a1], 2, &repo), Err(QuorumError::NoQuorum));
-
        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::NoQuorum));
-
        assert_matches!(quorum(&[*m1, *m2], 2, &repo), Err(QuorumError::NoQuorum));
-

-
        let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
-

-
        //   M3/M2 M1
-
        //    /\  /\
-
        //   C1 C2 C3
-
        //     \| /
-
        //      C0
-
        assert_matches!(quorum(&[*m1, *m3], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m1, *m3], 2, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m3, *m1], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m3, *m1], 2, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m3, *m2], 1, &repo), Err(QuorumError::NoQuorum));
-
        assert_matches!(quorum(&[*m3, *m2], 2, &repo), Err(QuorumError::NoQuorum));
-
    }
-

-
    #[test]
    fn test_remote_refs() {
        let dir = tempfile::tempdir().unwrap();
        let signer = MockSigner::default();