| |
/// 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, their merge base is taken.
|
| + |
/// the threshold, and they are divergent, an error is returned.
|
| |
///
|
| - |
/// Returns an error if `heads` is empty or `threshold` cannot be satisified with
|
| + |
/// 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 direct: HashMap<git::raw::Oid, HashSet<usize>> = HashMap::new();
|
| - |
let mut indirect: HashMap<git::raw::Oid, HashSet<usize>> = HashMap::new();
|
| - |
|
| - |
let Some(init) = heads.first() else {
|
| - |
return Err(QuorumError::NoQuorum);
|
| - |
};
|
| - |
// Nb. The merge base chosen for two merge commits is arbitrary.
|
| - |
let base = heads
|
| - |
.iter()
|
| - |
.try_fold(*init, |base, h| repo.merge_base(base, *h))?;
|
| - |
|
| - |
// Score every commit in the graph with the number of heads
|
| - |
// pointing to it.
|
| - |
// To make sure the votes are not counted twice, we use
|
| - |
// the index in the `heads` slice as the vote identifier.
|
| - |
// Note that it's perfectly legal to have multiple heads
|
| - |
// with the same value.
|
| - |
for (i, head) in heads.iter().enumerate() {
|
| - |
direct.entry(*head).or_default().insert(i);
|
| + |
let mut candidates = BTreeMap::<_, usize>::new();
|
| |
|
| - |
let mut revwalk = repo.revwalk()?;
|
| - |
revwalk.push(*head)?;
|
| + |
// 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);
|
| |
|
| - |
for rev in revwalk {
|
| - |
let rev = rev?;
|
| - |
indirect.entry(rev).or_default().insert(i);
|
| + |
for other in heads.iter().skip(i) {
|
| + |
let base = repo.merge_base(*head, *other)?;
|
| |
|
| - |
if rev == base {
|
| - |
break;
|
| + |
// Nb. This also handles `head` == `other`, which happens once for every head,
|
| + |
// as well as when there is more than one head with the same OID.
|
| + |
if base == *other || base == *head {
|
| + |
*candidates.entry(Oid::from(base)).or_default() += 1;
|
| |
}
|
| |
}
|
| |
}
|
| - |
|
| - |
{
|
| - |
let matches = direct
|
| - |
.iter()
|
| - |
.filter(|(_, tips)| tips.len() >= threshold)
|
| - |
.map(|(h, _)| *h)
|
| - |
.collect::<Vec<_>>();
|
| - |
|
| - |
match matches.as_slice() {
|
| - |
[] => {
|
| - |
// Check indirect votes.
|
| - |
}
|
| - |
[head] => return Ok((*head).into()),
|
| - |
[head, ref rest @ ..] => {
|
| - |
let oid = rest
|
| - |
.iter()
|
| - |
.try_fold(*head, |base, h| repo.merge_base(base, *h))?;
|
| - |
|
| - |
if !direct.contains_key(&oid) {
|
| - |
return Ok(oid.into());
|
| - |
}
|
| - |
}
|
| + |
// 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);
|
| |
}
|
| |
}
|
| - |
|
| - |
let mut combined: HashMap<git::raw::Oid, HashSet<usize>> = HashMap::new();
|
| - |
for (k, v) in direct.into_iter().chain(indirect) {
|
| - |
combined.entry(k).or_default().extend(v);
|
| - |
}
|
| - |
|
| - |
let minimum = combined
|
| - |
.iter()
|
| - |
.filter(|(_, tips)| tips.len() >= threshold)
|
| - |
.map(|(_, tips)| tips.len())
|
| - |
.min()
|
| - |
.ok_or(QuorumError::NoQuorum)?;
|
| - |
|
| - |
let candidates = combined
|
| - |
.iter()
|
| - |
.filter(|(_, v)| v.len() == minimum)
|
| - |
.map(|(h, _)| *h)
|
| - |
.collect::<Vec<_>>();
|
| - |
|
| - |
let oid = match candidates.as_slice() {
|
| - |
[] => return Err(QuorumError::NoQuorum),
|
| - |
[head] => *head,
|
| - |
[head, ref rest @ ..] => rest
|
| - |
.iter()
|
| - |
.try_fold(*head, |base, h| repo.merge_base(base, *h))?,
|
| - |
};
|
| - |
|
| - |
Ok(oid.into())
|
| + |
Ok((*longest).into())
|
| |
}
|
| |
|
| |
pub mod trailers {
|
| |
// C1
|
| |
// |
|
| |
// C0
|
| - |
assert_eq!(quorum(&[*c1, *c2, *b2], 1, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*c2, *b2], 1, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*b2, *c2], 1, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*c2, *b2], 2, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*b2, *c2], 2, &repo).unwrap(), c1);
|
| + |
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_eq!(quorum(&[*b2, *b2, *c2, *c2], 1, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*b2, *c2, *c2], 1, &repo).unwrap(), c1);
|
| + |
assert_matches!(
|
| + |
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
| + |
Err(QuorumError::NoQuorum)
|
| + |
);
|
| |
|
| |
// B2 C2
|
| |
// \|
|
| |
// A1 C1
|
| |
// \|
|
| |
// C0
|
| - |
assert_eq!(quorum(&[*c2, *b2, *a1], 1, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*c2, *b2, *a1], 2, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*c2, *b2, *a1], 3, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0);
|
| - |
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_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_eq!(quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*a1, *a1, *c2], 1, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*b2, *b2, *c2, *c2], 1, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &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
|
| |
// /\ /\
|
| |
// \|
|
| |
// C0
|
| |
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
| - |
assert_eq!(quorum(&[*m1, *m2], 1, &repo).unwrap(), b2);
|
| - |
assert_eq!(quorum(&[*m2, *m1], 1, &repo).unwrap(), b2);
|
| - |
assert_eq!(quorum(&[*m1, *m2], 2, &repo).unwrap(), b2);
|
| - |
assert_eq!(quorum(&[*m1, *m2, *c2], 1, &repo).unwrap(), c1);
|
| - |
assert_eq!(quorum(&[*m1, *a1], 1, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*m1, *a1], 2, &repo).unwrap(), c0);
|
| + |
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(&[*c2, *m1, *m2], 3, &repo).unwrap(), c1);
|
| + |
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]
|
| - |
#[ignore = "failing"]
|
| |
fn test_quorum_merges() {
|
| |
let tmp = tempfile::tempdir().unwrap();
|
| |
let (repo, c0) = fixtures::repository(tmp.path());
|
| |
eprintln!("M1: {m1}");
|
| |
eprintln!("M2: {m2}");
|
| |
|
| - |
assert_eq!(quorum(&[*m1, *m2], 1, &repo).unwrap(), c2);
|
| - |
assert_eq!(quorum(&[*m1, *m2], 2, &repo).unwrap(), c2);
|
| + |
// 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);
|
| |
|
| - |
assert_eq!(quorum(&[*m1, *m3], 1, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*m1, *m3], 2, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*m3, *m1], 1, &repo).unwrap(), c0);
|
| - |
assert_eq!(quorum(&[*m3, *m1], 2, &repo).unwrap(), c0);
|
| + |
// 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]
|