1use std::collections::BTreeMap;
2use std::fmt;
3
4use nonempty::NonEmpty;
5use raw::Repository;
6use thiserror::Error;
7
8use crate::prelude::Did;
9use crate::prelude::Project;
10use crate::storage::ReadRepository;
11
12use super::raw;
13use super::{lit, Oid, Qualified};
14
15pub struct Canonical {
25 tips: BTreeMap<Did, Oid>,
26}
27
28#[derive(Debug, Error)]
30pub enum QuorumError {
31 #[error("could not determine canonical reference tip, {0}")]
33 Diverging(Diverging),
34 #[error("could not determine canonical reference tip, {0}")]
36 NoCandidates(NoCandidates),
37 #[error(transparent)]
39 Git(#[from] git2::Error),
40}
41
42#[derive(Debug)]
47pub struct NoCandidates {
48 threshold: usize,
49}
50
51impl fmt::Display for NoCandidates {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 let NoCandidates { threshold } = self;
54 write!(
55 f,
56 "no commit found with at least {threshold} vote(s) (threshold not met)"
57 )
58 }
59}
60
61#[derive(Debug)]
67pub struct Diverging {
68 threshold: usize,
69 base: Oid,
70 longest: Oid,
71 head: Oid,
72}
73
74impl fmt::Display for Diverging {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 let Diverging {
77 threshold,
78 base,
79 longest,
80 head,
81 } = self;
82 write!(f, "found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")
83 }
84}
85
86impl Canonical {
87 pub fn default_branch<S>(
90 repo: &S,
91 project: &Project,
92 delegates: &NonEmpty<Did>,
93 ) -> Result<Self, raw::Error>
94 where
95 S: ReadRepository,
96 {
97 Self::reference(
98 repo,
99 delegates,
100 &lit::refs_heads(project.default_branch()).into(),
101 )
102 }
103
104 pub fn reference<S>(
107 repo: &S,
108 delegates: &NonEmpty<Did>,
109 name: &Qualified,
110 ) -> Result<Self, raw::Error>
111 where
112 S: ReadRepository,
113 {
114 let mut tips = BTreeMap::new();
115 for delegate in delegates.iter() {
116 match repo.reference_oid(delegate, name) {
117 Ok(tip) => {
118 tips.insert(*delegate, tip);
119 }
120 Err(e) if super::ext::is_not_found_err(&e) => {
121 log::warn!(
122 target: "radicle",
123 "Missing `refs/namespaces/{}/{name}` while calculating the canonical reference",
124 delegate.as_key()
125 );
126 }
127 Err(e) => return Err(e),
128 }
129 }
130 Ok(Canonical { tips })
131 }
132
133 pub fn tips(&self) -> impl Iterator<Item = (&Did, &Oid)> {
135 self.tips.iter()
136 }
137}
138
139pub fn converges<'a>(
144 tips: impl Iterator<Item = &'a Oid>,
145 target: Oid,
146 repo: &Repository,
147) -> Result<bool, raw::Error> {
148 for tip in tips {
149 match repo.graph_ahead_behind(*target, **tip)? {
150 (0, 0) => return Ok(true),
151 (ahead, behind) if ahead > 0 && behind == 0 => return Ok(true),
152 (ahead, behind) if behind > 0 && ahead == 0 => return Ok(true),
153 (_, _) => {}
154 }
155 }
156 Ok(false)
157}
158
159impl Canonical {
160 pub fn modify_vote(&mut self, did: Did, new: Oid) {
164 self.tips.insert(did, new);
165 }
166
167 pub fn quorum(&self, threshold: usize, repo: &raw::Repository) -> Result<Oid, QuorumError> {
175 let mut candidates = BTreeMap::<_, usize>::new();
176
177 for (i, head) in self.tips.values().enumerate() {
181 *candidates.entry(*head).or_default() += 1;
183
184 for other in self.tips.values().skip(i + 1) {
186 if *head == *other {
189 continue;
190 }
191 let base = Oid::from(repo.merge_base(**head, **other)?);
192
193 if base == *other || base == *head {
194 *candidates.entry(base).or_default() += 1;
195 }
196 }
197 }
198 candidates.retain(|_, votes| *votes >= threshold);
200
201 let (mut longest, _) = candidates
202 .pop_first()
203 .ok_or_else(|| QuorumError::NoCandidates(NoCandidates { threshold }))?;
204
205 for head in candidates.keys() {
208 let base = repo.merge_base(**head, *longest)?;
209
210 if base == *longest {
211 longest = *head;
219 } else if base == **head || *head == longest {
220 } else {
228 return Err(QuorumError::Diverging(Diverging {
238 threshold,
239 base: base.into(),
240 longest,
241 head: *head,
242 }));
243 }
244 }
245 Ok((*longest).into())
246 }
247}
248
249#[cfg(test)]
250#[allow(clippy::unwrap_used)]
251mod tests {
252 use crypto::test::signer::MockSigner;
253 use radicle_crypto::Signer;
254
255 use super::*;
256 use crate::assert_matches;
257 use crate::git;
258 use crate::test::fixtures;
259
260 fn quorum(
262 heads: &[git::raw::Oid],
263 threshold: usize,
264 repo: &git::raw::Repository,
265 ) -> Result<Oid, QuorumError> {
266 let tips = heads
267 .iter()
268 .enumerate()
269 .map(|(i, head)| {
270 let signer = MockSigner::from_seed([(i + 1) as u8; 32]);
271 let did = Did::from(signer.public_key());
272 (did, (*head).into())
273 })
274 .collect();
275 Canonical { tips }.quorum(threshold, repo)
276 }
277
278 #[test]
279 fn test_quorum_properties() {
280 let tmp = tempfile::tempdir().unwrap();
281 let (repo, c0) = fixtures::repository(tmp.path());
282 let c0: git::Oid = c0.into();
283 let a1 = fixtures::commit("A1", &[*c0], &repo);
284 let a2 = fixtures::commit("A2", &[*a1], &repo);
285 let d1 = fixtures::commit("D1", &[*c0], &repo);
286 let c1 = fixtures::commit("C1", &[*c0], &repo);
287 let c2 = fixtures::commit("C2", &[*c1], &repo);
288 let b2 = fixtures::commit("B2", &[*c1], &repo);
289 let a1 = fixtures::commit("A1", &[*c0], &repo);
290 let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
291 let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
292 let mut rng = fastrand::Rng::new();
293 let choices = [*c0, *c1, *c2, *b2, *a1, *a2, *d1, *m1, *m2];
294
295 for _ in 0..100 {
296 let count = rng.usize(1..=choices.len());
297 let threshold = rng.usize(1..=count);
298 let mut heads = Vec::new();
299
300 for _ in 0..count {
301 let ix = rng.usize(0..choices.len());
302 heads.push(choices[ix]);
303 }
304 rng.shuffle(&mut heads);
305
306 if let Ok(canonical) = quorum(&heads, threshold, &repo) {
307 assert!(heads.contains(&canonical));
308 }
309 }
310 }
311
312 #[test]
313 fn test_quorum() {
314 let tmp = tempfile::tempdir().unwrap();
315 let (repo, c0) = fixtures::repository(tmp.path());
316 let c0: git::Oid = c0.into();
317 let c1 = fixtures::commit("C1", &[*c0], &repo);
318 let c2 = fixtures::commit("C2", &[*c1], &repo);
319 let c3 = fixtures::commit("C3", &[*c1], &repo);
320 let b2 = fixtures::commit("B2", &[*c1], &repo);
321 let a1 = fixtures::commit("A1", &[*c0], &repo);
322 let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
323 let m2 = fixtures::commit("M2", &[*a1, *b2], &repo);
324
325 eprintln!("C0: {c0}");
326 eprintln!("C1: {c1}");
327 eprintln!("C2: {c2}");
328 eprintln!("C3: {c3}");
329 eprintln!("B2: {b2}");
330 eprintln!("A1: {a1}");
331 eprintln!("M1: {m1}");
332 eprintln!("M2: {m2}");
333
334 assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
335 assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
336 assert_eq!(quorum(&[*c2], 1, &repo).unwrap(), c2);
337 assert_eq!(quorum(&[*c0], 0, &repo).unwrap(), c0);
338 assert_matches!(quorum(&[], 0, &repo), Err(QuorumError::NoCandidates(_)));
339 assert_matches!(quorum(&[*c0], 2, &repo), Err(QuorumError::NoCandidates(_)));
340
341 assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
345
346 assert_eq!(quorum(&[*c1, *c2], 1, &repo).unwrap(), c2);
352 assert_eq!(quorum(&[*c1, *c2], 2, &repo).unwrap(), c1);
353 assert_eq!(quorum(&[*c0, *c1, *c2], 3, &repo).unwrap(), c0);
354 assert_eq!(quorum(&[*c1, *c1, *c2], 2, &repo).unwrap(), c1);
355 assert_eq!(quorum(&[*c1, *c1, *c2], 1, &repo).unwrap(), c2);
356 assert_eq!(quorum(&[*c2, *c2, *c1], 1, &repo).unwrap(), c2);
357
358 assert_matches!(
364 quorum(&[*c1, *c2, *b2], 1, &repo),
365 Err(QuorumError::Diverging(_))
366 );
367 assert_matches!(
368 quorum(&[*c2, *b2], 1, &repo),
369 Err(QuorumError::Diverging(_))
370 );
371 assert_matches!(
372 quorum(&[*b2, *c2], 1, &repo),
373 Err(QuorumError::Diverging(_))
374 );
375 assert_matches!(
376 quorum(&[*c2, *b2], 2, &repo),
377 Err(QuorumError::NoCandidates(_))
378 );
379 assert_matches!(
380 quorum(&[*b2, *c2], 2, &repo),
381 Err(QuorumError::NoCandidates(_))
382 );
383 assert_eq!(quorum(&[*c1, *c2, *b2], 2, &repo).unwrap(), c1);
384 assert_eq!(quorum(&[*c1, *c2, *b2], 3, &repo).unwrap(), c1);
385 assert_eq!(quorum(&[*b2, *b2, *c2], 2, &repo).unwrap(), b2);
386 assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
387 assert_matches!(
388 quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
389 Err(QuorumError::Diverging(_))
390 );
391
392 assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
398 assert_matches!(
399 quorum(&[*b2, *c2, *c2], 3, &repo),
400 Err(QuorumError::NoCandidates(_))
401 );
402 assert_matches!(
403 quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
404 Err(QuorumError::NoCandidates(_))
405 );
406 assert_matches!(
407 quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
408 Err(QuorumError::NoCandidates(_))
409 );
410
411 assert_matches!(
417 quorum(&[*c2, *b2, *a1], 1, &repo),
418 Err(QuorumError::Diverging(_))
419 );
420 assert_matches!(
421 quorum(&[*c2, *b2, *a1], 2, &repo),
422 Err(QuorumError::NoCandidates(_))
423 );
424 assert_matches!(
425 quorum(&[*c2, *b2, *a1], 3, &repo),
426 Err(QuorumError::NoCandidates(_))
427 );
428 assert_matches!(
429 quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
430 Err(QuorumError::NoCandidates(_))
431 );
432 assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 2, &repo).unwrap(), c1,);
433 assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 3, &repo).unwrap(), c1,);
434 assert_eq!(quorum(&[*c0, *c2, *b2, *a1], 3, &repo).unwrap(), c0);
435 assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
436 assert_matches!(
437 quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
438 Err(QuorumError::Diverging(_))
439 );
440 assert_matches!(
441 quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
442 Err(QuorumError::Diverging(_))
443 );
444 assert_matches!(
445 quorum(&[*a1, *a1, *c2], 1, &repo),
446 Err(QuorumError::Diverging(_))
447 );
448 assert_matches!(
449 quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
450 Err(QuorumError::Diverging(_))
451 );
452 assert_matches!(
453 quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
454 Err(QuorumError::Diverging(_))
455 );
456
457 assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
465 assert_matches!(
466 quorum(&[*m1, *m2], 1, &repo),
467 Err(QuorumError::Diverging(_))
468 );
469 assert_matches!(
470 quorum(&[*m2, *m1], 1, &repo),
471 Err(QuorumError::Diverging(_))
472 );
473 assert_matches!(
474 quorum(&[*m1, *m2], 2, &repo),
475 Err(QuorumError::NoCandidates(_))
476 );
477 assert_matches!(
478 quorum(&[*m1, *m2, *c2], 1, &repo),
479 Err(QuorumError::Diverging(_))
480 );
481 assert_matches!(
482 quorum(&[*m1, *a1], 1, &repo),
483 Err(QuorumError::Diverging(_))
484 );
485 assert_matches!(
486 quorum(&[*m1, *a1], 2, &repo),
487 Err(QuorumError::NoCandidates(_))
488 );
489 assert_eq!(quorum(&[*m1, *m2, *b2, *c1], 4, &repo).unwrap(), c1);
490 assert_eq!(quorum(&[*m1, *m1, *b2], 2, &repo).unwrap(), m1);
491 assert_eq!(quorum(&[*m1, *m1, *c2], 2, &repo).unwrap(), m1);
492 assert_eq!(quorum(&[*m2, *m2, *b2], 2, &repo).unwrap(), m2);
493 assert_eq!(quorum(&[*m2, *m2, *a1], 2, &repo).unwrap(), m2);
494 assert_eq!(quorum(&[*m1, *m1, *b2, *b2], 2, &repo).unwrap(), m1);
495 assert_eq!(quorum(&[*m1, *m1, *c2, *c2], 2, &repo).unwrap(), m1);
496 assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 3, &repo).unwrap(), c1);
497 assert_eq!(quorum(&[*m1, *b2, *c1, *c0], 4, &repo).unwrap(), c0);
498 }
499
500 #[test]
501 fn test_quorum_merges() {
502 let tmp = tempfile::tempdir().unwrap();
503 let (repo, c0) = fixtures::repository(tmp.path());
504 let c0: git::Oid = c0.into();
505 let c1 = fixtures::commit("C1", &[*c0], &repo);
506 let c2 = fixtures::commit("C2", &[*c0], &repo);
507 let c3 = fixtures::commit("C3", &[*c0], &repo);
508
509 let m1 = fixtures::commit("M1", &[*c1, *c2], &repo);
510 let m2 = fixtures::commit("M2", &[*c2, *c3], &repo);
511
512 eprintln!("C0: {c0}");
513 eprintln!("C1: {c1}");
514 eprintln!("C2: {c2}");
515 eprintln!("C3: {c3}");
516 eprintln!("M1: {m1}");
517 eprintln!("M2: {m2}");
518
519 assert_matches!(
525 quorum(&[*m1, *m2], 1, &repo),
526 Err(QuorumError::Diverging(_))
527 );
528 assert_matches!(
529 quorum(&[*m1, *m2], 2, &repo),
530 Err(QuorumError::NoCandidates(_))
531 );
532
533 let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
534
535 assert_matches!(
541 quorum(&[*m1, *m3], 1, &repo),
542 Err(QuorumError::Diverging(_))
543 );
544 assert_matches!(
545 quorum(&[*m1, *m3], 2, &repo),
546 Err(QuorumError::NoCandidates(_))
547 );
548 assert_matches!(
549 quorum(&[*m3, *m1], 1, &repo),
550 Err(QuorumError::Diverging(_))
551 );
552 assert_matches!(
553 quorum(&[*m3, *m1], 2, &repo),
554 Err(QuorumError::NoCandidates(_))
555 );
556 assert_matches!(
557 quorum(&[*m3, *m2], 1, &repo),
558 Err(QuorumError::Diverging(_))
559 );
560 assert_matches!(
561 quorum(&[*m3, *m2], 2, &repo),
562 Err(QuorumError::NoCandidates(_))
563 );
564 }
565}