From 54d23545ff70903cfa4e513b33eb2f920debcaa6 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Mon, 24 Jun 2024 14:45:45 +0200 Subject: [PATCH] radicle: port quorum code to Canonical Remove the old `quorum` function in favour of using the newly introduced `Canonical` code. Port over the tests to use the `Canonical` type. The change is minimised by introducing a helper function called `quorum` in the test module that acts like the old function. Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- radicle-remote-helper/src/push.rs | 2 +- radicle/src/git/canonical.rs | 276 ++++++++++++++++++++++- radicle/src/storage.rs | 4 +- radicle/src/storage/git.rs | 350 +----------------------------- 4 files changed, 280 insertions(+), 352 deletions(-) diff --git a/radicle-remote-helper/src/push.rs b/radicle-remote-helper/src/push.rs index b9ec007f..fb63b40d 100644 --- a/radicle-remote-helper/src/push.rs +++ b/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. diff --git a/radicle/src/git/canonical.rs b/radicle/src/git/canonical.rs index 780c193c..c2ef74d7 100644 --- a/radicle/src/git/canonical.rs +++ b/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 { + 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)); + } +} diff --git a/radicle/src/storage.rs b/radicle/src/storage.rs index 71101e24..f3e34f83 100644 --- a/radicle/src/storage.rs +++ b/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), } diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index ba94c3ff..aa7abfa3 100644 --- a/radicle/src/storage/git.rs +++ b/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 { - 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,250 +939,12 @@ 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}; use crate::test::arbitrary; 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();