use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct CommitId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SourceState {
pub commit: CommitId,
pub dirty_digest: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SnapshotId(pub u64);
#[derive(Debug, Clone, Default)]
pub struct CommitGraph {
parents: BTreeMap<CommitId, Vec<CommitId>>,
}
impl CommitGraph {
pub fn insert(&mut self, commit: CommitId, parents: &[CommitId]) {
self.parents.insert(commit, parents.to_vec());
}
#[must_use]
pub fn parents_of(&self, commit: CommitId) -> Vec<CommitId> {
self.parents.get(&commit).cloned().unwrap_or_default()
}
#[must_use]
pub fn reachable(&self, from: CommitId, ancestor: CommitId) -> bool {
let mut frontier = vec![from];
let mut seen = Vec::new();
while let Some(c) = frontier.pop() {
if c == ancestor {
return true;
}
if seen.contains(&c) {
continue;
}
seen.push(c);
if let Some(ps) = self.parents.get(&c) {
frontier.extend(ps.iter().copied());
}
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NearestSnapshot {
pub snapshot: SnapshotId,
pub state: SourceState,
pub distance: u32,
}
#[derive(Debug, Clone)]
pub struct AncestryIndex {
entries: Vec<(SourceState, SnapshotId)>,
capacity: usize,
}
impl AncestryIndex {
#[must_use]
pub const fn new(capacity: usize) -> Self {
Self {
entries: Vec::new(),
capacity,
}
}
pub fn record(&mut self, state: SourceState, snapshot: SnapshotId) {
self.entries.retain(|(s, _)| *s != state); if self.entries.len() >= self.capacity {
self.entries.remove(0);
}
self.entries.push((state, snapshot));
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn at_commit(&self, commit: CommitId, dirty: Option<u64>) -> Option<(SourceState, SnapshotId)> {
if let Some(hit) = self
.entries
.iter()
.rev()
.find(|(s, _)| s.commit == commit && s.dirty_digest == dirty)
{
return Some(*hit);
}
self.entries
.iter()
.rev()
.find(|(s, _)| s.commit == commit && s.dirty_digest.is_none())
.copied()
}
#[must_use]
pub fn nearest(&self, target: SourceState, graph: &CommitGraph) -> Option<NearestSnapshot> {
let mut frontier = vec![target.commit];
let mut seen: Vec<CommitId> = Vec::new();
let mut distance = 0_u32;
while !frontier.is_empty() {
let dirty = if distance == 0 {
target.dirty_digest
} else {
None
};
for &commit in &frontier {
if let Some((state, snapshot)) = self.at_commit(commit, dirty) {
return Some(NearestSnapshot {
snapshot,
state,
distance,
});
}
}
let mut next = Vec::new();
for &commit in &frontier {
if seen.contains(&commit) {
continue;
}
seen.push(commit);
if let Some(parents) = graph.parents.get(&commit) {
next.extend(parents.iter().copied());
}
}
frontier = next;
distance += 1;
}
None
}
#[must_use]
pub fn orphans(&self, graph: &CommitGraph, live_tips: &[CommitId]) -> Vec<SourceState> {
self.entries
.iter()
.filter(|(state, _)| {
!live_tips
.iter()
.any(|&tip| graph.reachable(tip, state.commit))
})
.map(|(state, _)| *state)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(n: u64) -> CommitId {
CommitId(n)
}
fn clean(n: u64) -> SourceState {
SourceState {
commit: c(n),
dirty_digest: None,
}
}
fn dirty(n: u64, digest: u64) -> SourceState {
SourceState {
commit: c(n),
dirty_digest: Some(digest),
}
}
fn fixture() -> (CommitGraph, AncestryIndex) {
let mut graph = CommitGraph::default();
graph.insert(c(1), &[]);
graph.insert(c(10), &[c(1)]);
graph.insert(c(11), &[c(10)]);
graph.insert(c(20), &[c(1)]);
let mut index = AncestryIndex::new(16);
index.record(clean(1), SnapshotId(100));
index.record(clean(11), SnapshotId(111));
index.record(clean(20), SnapshotId(120));
(graph, index)
}
#[test]
fn ping_pong_between_branches_hits_exactly() {
let (graph, index) = fixture();
assert_eq!(
index.nearest(clean(11), &graph),
Some(NearestSnapshot {
snapshot: SnapshotId(111),
state: clean(11),
distance: 0,
})
);
assert_eq!(
index.nearest(clean(20), &graph).expect("hit").snapshot,
SnapshotId(120)
);
let mut graph2 = fixture().0;
graph2.insert(c(30), &[c(1)]);
let hit = index.nearest(clean(30), &graph2).expect("shared ancestor");
assert_eq!(hit.snapshot, SnapshotId(100));
assert_eq!(hit.distance, 1);
let mut graph3 = fixture().0;
graph3.insert(c(12), &[c(11)]);
let hit = index.nearest(clean(12), &graph3).expect("parent hit");
assert_eq!(hit.snapshot, SnapshotId(111));
assert_eq!(hit.distance, 1);
}
#[test]
fn dirty_trees_are_their_own_states() {
let (graph, mut index) = fixture();
let dirty_state = dirty(11, 0xD1);
let warm = index.nearest(dirty_state, &graph).expect("clean base");
assert_eq!(warm.snapshot, SnapshotId(111));
assert_eq!(warm.state, clean(11), "clean snapshot serves dirty tree");
index.record(dirty_state, SnapshotId(211));
let exact = index.nearest(dirty_state, &graph).expect("exact");
assert_eq!(exact.snapshot, SnapshotId(211));
assert_eq!(exact.state, dirty_state);
let other = index.nearest(dirty(11, 0xD2), &graph).expect("clean base");
assert_eq!(other.snapshot, SnapshotId(111));
}
#[test]
fn rebase_orphans_are_unreachable_and_named_for_eviction() {
let (mut graph, index) = fixture();
graph.insert(c(31), &[c(1)]); let live_tips = [c(31), c(20)];
let hit = index.nearest(clean(31), &graph).expect("main base");
assert_eq!(hit.snapshot, SnapshotId(100));
assert_eq!(index.orphans(&graph, &live_tips), vec![clean(11)]);
}
#[test]
fn growth_is_bounded_with_oldest_evicted() {
let mut index = AncestryIndex::new(4);
for n in 0..12 {
index.record(clean(n), SnapshotId(n));
assert!(index.len() <= 4, "the bound is an invariant");
}
assert_eq!(index.len(), 4);
let graph = CommitGraph::default();
assert!(index.nearest(clean(0), &graph).is_none());
assert_eq!(
index.nearest(clean(11), &graph).expect("newest").snapshot,
SnapshotId(11)
);
index.record(clean(11), SnapshotId(99));
assert_eq!(index.len(), 4);
assert_eq!(
index.nearest(clean(11), &graph).expect("moved").snapshot,
SnapshotId(99)
);
}
}