use crate::node::NamedRef;
use gantz_ca::{CommitAddr, DataGraph, GraphAddr, Name, NodeData, Registry};
use gantz_nodetag::NodeTag;
use std::collections::{BTreeSet, HashMap, VecDeque};
use std::time::Duration;
#[derive(Clone, Debug)]
pub struct Moved {
pub name: Name,
pub old_commit: CommitAddr,
pub new_commit: CommitAddr,
}
pub(crate) fn with_named_ref_mut(
weight: &mut NodeData,
f: impl FnOnce(&mut NamedRef) -> bool,
) -> bool {
if weight.tag != <NamedRef as NodeTag>::TAG {
return false;
}
let mut named_ref = match gantz_core::data::reify_node_concrete::<NamedRef>(weight) {
Ok(named_ref) => named_ref,
Err(e) => {
log::error!("failed to decode a stored `NamedRef`: {e}");
return false;
}
};
if !f(&mut named_ref) {
return false;
}
match gantz_core::data::erase_node_typed(&named_ref) {
Ok(node_data) => {
*weight = node_data;
true
}
Err(e) => {
log::error!("failed to erase a rewritten `NamedRef`: {e}");
false
}
}
}
fn named_ref_of(weight: &NodeData) -> Option<NamedRef> {
if weight.tag != <NamedRef as NodeTag>::TAG {
return None;
}
match gantz_core::data::reify_node_concrete::<NamedRef>(weight) {
Ok(named_ref) => Some(named_ref),
Err(e) => {
log::error!("failed to decode a stored `NamedRef`: {e}");
None
}
}
}
fn rewrite_refs(graph: &mut DataGraph, mut mutate: impl FnMut(&mut NamedRef) -> bool) -> bool {
let mut changed = false;
for weight in graph.node_weights_mut() {
changed |= with_named_ref_mut(weight, &mut mutate);
}
changed
}
fn commit_data_graph(
registry: &mut Registry,
timestamp: Duration,
name: &Name,
graph: DataGraph,
) -> CommitAddr {
let graph_ca = gantz_ca::graph_addr(&graph);
registry.commit_graph_to_name(timestamp, graph_ca, || graph, name)
}
fn commit_rewritten(
registry: &mut Registry,
timestamp: Duration,
name: &Name,
source_commit: CommitAddr,
mutate: impl FnMut(&mut NamedRef) -> bool,
) -> Option<Moved> {
let mut g = registry.commit_graph_ref(&source_commit)?.clone();
if !rewrite_refs(&mut g, mutate) {
return None;
}
let new_commit = commit_data_graph(registry, timestamp, name, g);
Some(Moved {
name: name.clone(),
old_commit: source_commit,
new_commit,
})
}
fn remap_ref(named_ref: &mut NamedRef, remap: &HashMap<Name, (Name, GraphAddr)>) -> bool {
match remap.get(named_ref.name()) {
Some((new_name, new_graph)) => {
named_ref.rename(new_name.clone(), (*new_graph).into());
true
}
None => false,
}
}
fn renamed(descendant: &Name, old: &Name, new: &Name) -> Name {
let segments: Vec<String> = new
.segments()
.iter()
.chain(&descendant.segments()[old.segments().len()..])
.cloned()
.collect();
Name::from(segments)
}
pub fn fork_nested(
registry: &mut Registry,
timestamp: Duration,
old: &Name,
new: &Name,
) -> Vec<Moved> {
let mut descendants: Vec<Name> = registry
.heads()
.filter(|(n, _)| n.starts_with(old) && *n != old)
.map(|(n, _)| n.clone())
.collect();
descendants.sort_by(|a, b| b.depth().cmp(&a.depth()).then_with(|| a.cmp(b)));
let mut remap: HashMap<Name, (Name, GraphAddr)> = HashMap::new();
let mut moves = Vec::new();
for d in &descendants {
let d_new = renamed(d, old, new);
let Some(commit) = registry.head(d) else {
continue;
};
let Some(mut g) = registry.commit_graph_ref(&commit).cloned() else {
continue;
};
rewrite_refs(&mut g, |nr| remap_ref(nr, &remap));
let new_commit = commit_data_graph(registry, timestamp, &d_new, g);
let graph_ca = registry
.commits()
.get(&new_commit)
.expect("freshly committed")
.graph;
remap.insert(d.clone(), (d_new.clone(), graph_ca));
moves.push(Moved {
name: d_new,
old_commit: commit,
new_commit,
});
}
if let Some(root_commit) = registry.head(new) {
moves.extend(commit_rewritten(
registry,
timestamp,
new,
root_commit,
|nr| remap_ref(nr, &remap),
));
}
moves
}
pub fn resync(registry: &mut Registry, timestamp: Duration) -> Vec<Moved> {
let mut order: Vec<Name> = registry.heads().map(|(n, _)| n.clone()).collect();
order.sort_by(|a, b| b.depth().cmp(&a.depth()).then_with(|| a.cmp(b)));
let mut current: HashMap<Name, (CommitAddr, GraphAddr)> = registry
.heads()
.filter_map(|(n, ca)| {
let graph = registry.commits().get(&ca)?.graph;
Some((n.clone(), (ca, graph)))
})
.collect();
let mut moves = Vec::new();
let max_passes = order.len() + 1;
for _ in 0..max_passes {
let mut changed_any = false;
for name in &order {
let Some(&(commit_ca, _)) = current.get(name) else {
continue;
};
let resolve = |m: &Name| {
current
.get(m)
.map(|&(_, graph)| gantz_ca::ContentAddr::from(graph))
};
if let Some(moved) = commit_rewritten(registry, timestamp, name, commit_ca, |nr| {
nr.resync(&resolve)
}) {
let graph = registry
.commits()
.get(&moved.new_commit)
.expect("freshly committed")
.graph;
current.insert(name.clone(), (moved.new_commit, graph));
moves.push(moved);
changed_any = true;
}
}
if !changed_any {
break;
}
}
moves
}
pub fn session_scope(registry: &Registry, root: &Name) -> BTreeSet<Name> {
let mut scope = BTreeSet::new();
let mut queue: VecDeque<Name> = VecDeque::from([root.clone()]);
while let Some(name) = queue.pop_front() {
if !scope.insert(name.clone()) {
continue;
}
let graph = registry
.head(&name)
.and_then(|ca| registry.commit_graph_ref(&ca));
let Some(graph) = graph else {
continue;
};
for weight in graph.node_weights() {
if let Some(named_ref) = named_ref_of(weight) {
if named_ref.sync && !scope.contains(named_ref.name()) {
queue.push_back(named_ref.name().clone());
}
}
}
}
scope
}
pub fn promote_nested(
registry: &mut Registry,
timestamp: Duration,
old_nested: &Name,
new_name: &Name,
) -> Vec<Moved> {
let Some(parent) = old_nested.parent() else {
return Vec::new();
};
let (Some(new_graph), Some(parent_commit)) = (
registry.named_commit(new_name).map(|c| c.graph),
registry.head(&parent),
) else {
return Vec::new();
};
let mut moves = Vec::new();
moves.extend(commit_rewritten(
registry,
timestamp,
&parent,
parent_commit,
|nr| {
if nr.name() == old_nested {
nr.rename(new_name.clone(), new_graph.into());
true
} else {
false
}
},
));
let orphans: Vec<Name> = registry
.heads()
.filter(|(n, _)| n.starts_with(old_nested))
.map(|(n, _)| n.clone())
.collect();
for orphan in orphans {
registry.remove_head(&orphan);
}
moves
}
#[cfg(test)]
mod tests {
use super::*;
use gantz_ca::{ContentAddr, Datum};
use std::time::Duration;
fn ref_node(name: &str, sync: bool) -> NodeData {
let ref_ = gantz_core::node::Ref::new(ContentAddr::from([0; 32]));
let name: Name = name.parse().unwrap();
let named_ref = if sync {
NamedRef::with_sync(name, ref_)
} else {
NamedRef::new(name, ref_)
};
gantz_core::data::erase_node_typed(&named_ref).unwrap()
}
fn add_named(registry: &mut Registry, name: &str, n: u8, graph: DataGraph) {
let graph_ca = gantz_ca::GraphAddr::from(ContentAddr::from([n; 32]));
let name: Name = name.parse().unwrap();
registry.commit_graph_to_name(Duration::from_secs(n as u64), graph_ca, || graph, &name);
}
fn scope_names(registry: &Registry, root: &str) -> BTreeSet<Name> {
session_scope(registry, &root.parse().unwrap())
}
fn names(names: impl IntoIterator<Item = &'static str>) -> BTreeSet<Name> {
names.into_iter().map(|n| n.parse().unwrap()).collect()
}
#[test]
fn session_scope_follows_sync_refs_transitively_and_skips_pinned() {
let mut registry = Registry::default();
let mut root = DataGraph::default();
root.add_node(NodeData::new("test", Datum::Map(vec![])));
root.add_node(ref_node("dep", true));
root.add_node(ref_node("pin", false));
add_named(&mut registry, "root", 1, root);
let mut dep = DataGraph::default();
dep.add_node(ref_node("dep:1", true));
dep.add_node(ref_node("elsewhere", true));
add_named(&mut registry, "dep", 2, dep);
add_named(&mut registry, "dep:1", 3, DataGraph::default());
add_named(&mut registry, "pin", 4, DataGraph::default());
let scope = scope_names(®istry, "root");
assert_eq!(scope, names(["root", "dep", "dep:1", "elsewhere"]));
}
#[test]
fn session_scope_handles_reference_cycles() {
let mut registry = Registry::default();
let mut a = DataGraph::default();
a.add_node(ref_node("b", true));
add_named(&mut registry, "a", 1, a);
let mut b = DataGraph::default();
b.add_node(ref_node("a", true));
add_named(&mut registry, "b", 2, b);
let scope = scope_names(®istry, "a");
assert_eq!(scope, names(["a", "b"]));
}
}