use std::collections::{BTreeMap, BTreeSet};
use crate::state::{ReviewEntry, ReviewKey};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stack {
pub tip: ReviewKey,
pub members: Vec<ReviewKey>,
}
pub fn group<'a>(entries: impl IntoIterator<Item = &'a ReviewEntry>) -> Vec<Stack> {
let live: BTreeMap<&ReviewKey, &ReviewEntry> = entries
.into_iter()
.filter(|e| !e.resolved)
.map(|e| (&e.key, e))
.collect();
let ancestors_of_something: BTreeSet<&ReviewKey> =
live.values().flat_map(|e| e.ancestors.iter()).collect();
let mut assigned: BTreeSet<&ReviewKey> = BTreeSet::new();
let mut stacks = Vec::new();
for (key, entry) in &live {
if ancestors_of_something.contains(key) {
continue;
}
let members: Vec<ReviewKey> = entry
.ancestors
.iter()
.chain(std::iter::once(*key))
.filter(|k| live.contains_key(k) && assigned.insert(k))
.cloned()
.collect();
stacks.push(Stack {
tip: (*key).clone(),
members,
});
}
for key in live.keys() {
if assigned.insert(key) {
stacks.push(Stack {
tip: (*key).clone(),
members: vec![(*key).clone()],
});
}
}
stacks
}
pub fn stack_containing<'a>(stacks: &'a [Stack], key: &ReviewKey) -> Option<&'a Stack> {
stacks.iter().find(|s| s.members.contains(key))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::{RepoRef, ReviewKind};
fn k(id: &str) -> ReviewKey {
ReviewKey::new("moz", id)
}
fn entry(id: &str, ancestors: &[&str]) -> ReviewEntry {
ReviewEntry {
key: k(id),
title: String::new(),
author: String::new(),
url: String::new(),
repo: RepoRef {
urls: vec![],
display_name: String::new(),
},
kind: ReviewKind::Direct,
version: "1".into(),
in_queue: true,
resolved: false,
last_synced: chrono::Utc::now(),
stack_id: None,
ancestors: ancestors.iter().map(|a| k(a)).collect(),
diff_stat: None,
description: None,
}
}
#[test]
fn lone_reviews_are_stacks_of_one() {
let entries = [entry("D1", &[]), entry("D2", &[])];
let stacks = group(&entries);
assert_eq!(stacks.len(), 2);
assert_eq!(stacks[0].members, vec![k("D1")]);
}
#[test]
fn chain_groups_bottom_to_top() {
let entries = [
entry("D3", &["D1", "D2"]),
entry("D1", &[]),
entry("D2", &["D1"]),
];
let stacks = group(&entries);
assert_eq!(stacks.len(), 1);
assert_eq!(stacks[0].tip, k("D3"));
assert_eq!(stacks[0].members, vec![k("D1"), k("D2"), k("D3")]);
}
#[test]
fn untracked_ancestors_are_skipped_but_still_link_members() {
let entries = [entry("D1", &[]), entry("D3", &["D1", "D9"])];
let stacks = group(&entries);
assert_eq!(stacks.len(), 1);
assert_eq!(stacks[0].members, vec![k("D1"), k("D3")]);
}
#[test]
fn a_fork_gives_each_leaf_its_own_stack_and_the_shared_parent_to_the_first() {
let entries = [entry("D1", &[]), entry("D2", &["D1"]), entry("D3", &["D1"])];
let stacks = group(&entries);
assert_eq!(stacks.len(), 2);
assert_eq!(stacks[0].members, vec![k("D1"), k("D2")]);
assert_eq!(stacks[1].members, vec![k("D3")]);
}
#[test]
fn resolved_entries_are_ignored() {
let mut d1 = entry("D1", &[]);
d1.resolved = true;
let entries = [d1, entry("D2", &[])];
let stacks = group(&entries);
assert_eq!(stacks.len(), 1);
assert_eq!(stacks[0].members, vec![k("D2")]);
}
}