use crate::sync::AsNamedRef;
use crate::widget::gantz::OpenHeadState;
use crate::widget::graph_scene::NodeIndex;
use crate::{CreateNode, InspectEdge, PastePos, export, node::NamedRef};
use gantz_ca::{CaHash, CommitAddr};
use gantz_core::node::{self, GetNode, graph::Graph};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::collections::{HashMap, HashSet};
use steel::steel_vm::engine::Engine;
pub fn branch_node<N>(
registry: &mut gantz_ca::Registry<Graph<N>>,
timestamp: std::time::Duration,
graph: &mut Graph<N>,
new_name: String,
ca: gantz_ca::ContentAddr,
path: &[node::Id],
) where
N: From<NamedRef> + AsNamedRef,
{
let commit_ca = CommitAddr::from(ca);
let Some(commit) = registry.commits().get(&commit_ca) else {
log::error!("BranchNode: commit not found for {commit_ca:?}");
return;
};
let graph_addr = commit.graph;
let new_commit_ca = registry.commit_graph(timestamp, Some(commit_ca), graph_addr, || {
unreachable!("graph already exists in registry")
});
registry.insert_name(new_name.clone(), new_commit_ca);
let Some(&node_ix) = path.last() else {
log::error!("BranchNode: empty node path");
return;
};
let node_id = node::graph::NodeIx::new(node_ix);
let new_ref = match graph.node_weight(node_id).and_then(N::as_named_ref) {
Some(old) => old.ref_().retarget(new_commit_ca.into()),
None => node::Ref::new(new_commit_ca.into()),
};
let named_ref = NamedRef::new(new_name, new_ref);
if let Some(node) = graph.node_weight_mut(node_id) {
*node = N::from(named_ref);
} else {
log::error!("BranchNode: node not found at index {node_ix}");
}
}
pub fn copy_nodes<N>(
registry: &gantz_ca::Registry<Graph<N>>,
all_views: &HashMap<CommitAddr, crate::SceneView>,
graph: &Graph<N>,
head_view: &crate::SceneView,
selection: &HashSet<NodeIndex>,
) -> Option<String>
where
N: gantz_core::Node
+ Clone
+ Serialize
+ DeserializeOwned
+ CaHash
+ gantz_format::NodeSugar
+ 'static,
{
if selection.is_empty() {
return None;
}
let copied = export::copy(registry, all_views, graph, selection, &head_view.layout);
match export::copied_to_string(&copied) {
Ok(text) => Some(text),
Err(e) => {
log::error!("CopyNodes: failed to serialize: {e}");
None
}
}
}
pub fn create_node<N>(
registry: &gantz_ca::Registry<Graph<N>>,
editing: Option<&str>,
get_node: GetNode,
new_node: impl FnOnce(&str) -> Option<N>,
graph: &mut Graph<N>,
view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
vm: &mut Engine,
cmd: CreateNode,
) -> Option<NodeIndex>
where
N: gantz_core::Node + crate::sync::AsNamedRef,
{
let CreateNode { node_type, pos } = cmd;
if editing.is_some_and(|editing| crate::cycle::would_cycle(registry, &node_type, editing)) {
log::warn!("CreateNode: '{node_type}' would create a reference cycle; skipping");
return None;
}
let Some(node) = new_node(&node_type) else {
log::error!("CreateNode: unknown node type: {node_type}");
return None;
};
let node_ix = graph.add_node(node);
let node_path = [node_ix.index()];
let reg_ctx = node::RegCtx::new(get_node, &node_path, vm);
graph[node_ix].register(reg_ctx);
let pos = pos.unwrap_or_else(|| view.camera.center);
let egui_id = egui_graph::NodeId::from_u64(node_ix.index() as u64);
view.layout.insert(egui_id, pos);
let sel = &mut head_state.scene.interaction.selection;
sel.nodes.clear();
sel.edges.clear();
sel.nodes.insert(node_ix);
Some(node_ix)
}
pub fn create_nested_graph<N>(
registry: &mut gantz_ca::Registry<Graph<N>>,
timestamp: std::time::Duration,
graph: &mut Graph<N>,
view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
pos: Option<egui::Pos2>,
parent: &str,
) -> Option<NodeIndex>
where
N: gantz_core::Node + From<NamedRef> + CaHash,
{
let sep = crate::node::NESTED_SEP;
let mut n = 1u32;
let name = loop {
let candidate = format!("{parent}{sep}{n}");
if !registry.names().contains_key(&candidate) {
break candidate;
}
n += 1;
};
let nested_graph = Graph::<N>::default();
let graph_ca = gantz_ca::graph_addr(&nested_graph);
let commit_ca = registry.commit_graph_to_name(timestamp, graph_ca, || nested_graph, &name);
let named_ref = NamedRef::with_sync(name, node::Ref::new(commit_ca.into()));
let node_ix = graph.add_node(N::from(named_ref));
let pos = pos.unwrap_or_else(|| view.camera.center);
let egui_id = egui_graph::NodeId::from_u64(node_ix.index() as u64);
view.layout.insert(egui_id, pos);
let sel = &mut head_state.scene.interaction.selection;
sel.nodes.clear();
sel.edges.clear();
sel.nodes.insert(node_ix);
Some(node_ix)
}
#[derive(Clone, Copy, Debug)]
pub struct RemoveOp {
pub removed: usize,
pub moved_from: Option<usize>,
}
#[derive(Clone, Debug, Default)]
pub struct Reindex(pub Vec<RemoveOp>);
impl Reindex {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn apply_to_index(&self, mut ix: usize) -> Option<usize> {
for op in &self.0 {
if ix == op.removed {
return None;
}
if op.moved_from == Some(ix) {
ix = op.removed;
}
}
Some(ix)
}
}
pub fn remove_nodes<N>(
graph: &mut Graph<N>,
vm: &mut Engine,
layout: &mut egui_graph::Layout,
selection: &mut crate::widget::graph_scene::Selection,
nodes: impl IntoIterator<Item = NodeIndex>,
) -> Reindex {
let node_id = |ix: usize| egui_graph::NodeId::from_u64(ix as u64);
let mut targets: Vec<NodeIndex> = nodes.into_iter().collect();
targets.sort_unstable_by_key(|n| std::cmp::Reverse(n.index()));
targets.dedup();
let mut ops = Vec::new();
for t in targets {
if graph.node_weight(t).is_none() {
continue;
}
let last = graph.node_count() - 1;
let _ = node::state::remove_value(vm, &[t.index()]);
layout.remove(&node_id(t.index()));
selection.nodes.remove(&t);
graph.remove_node(t);
let moved_from = (t.index() != last).then_some(last);
if let Some(last) = moved_from {
let _ = node::state::move_value(vm, &[last], &[t.index()]);
if let Some(pos) = layout.remove(&node_id(last)) {
layout.insert(node_id(t.index()), pos);
}
if selection.nodes.remove(&NodeIndex::new(last)) {
selection.nodes.insert(t);
}
}
ops.push(RemoveOp {
removed: t.index(),
moved_from,
});
}
if !ops.is_empty() {
selection.edges.clear();
}
Reindex(ops)
}
pub fn cut_nodes<N>(
registry: &gantz_ca::Registry<Graph<N>>,
all_views: &HashMap<CommitAddr, crate::SceneView>,
graph: &mut Graph<N>,
vm: &mut Engine,
head_view: &mut crate::SceneView,
selection: &mut crate::widget::graph_scene::Selection,
nodes: &HashSet<NodeIndex>,
) -> Option<String>
where
N: gantz_core::Node
+ Clone
+ Serialize
+ DeserializeOwned
+ CaHash
+ gantz_format::NodeSugar
+ 'static,
{
let text = copy_nodes(registry, all_views, graph, head_view, nodes)?;
remove_nodes(
graph,
vm,
&mut head_view.layout,
selection,
nodes.iter().copied(),
);
Some(text)
}
pub fn inspect_edge<N>(
get_node: GetNode,
new_inspect: impl FnOnce() -> Option<N>,
graph: &mut Graph<N>,
view: &mut crate::SceneView,
vm: &mut Engine,
cmd: InspectEdge,
) where
N: gantz_core::Node,
{
let InspectEdge { edge, pos } = cmd;
let Some((src_node, dst_node)) = graph.edge_endpoints(edge) else {
log::error!("InspectEdge: edge not found");
return;
};
let edge_weight = *graph.edge_weight(edge).unwrap();
graph.remove_edge(edge);
let Some(inspect_node) = new_inspect() else {
log::error!("InspectEdge: could not create inspect node");
return;
};
let inspect_id = graph.add_node(inspect_node);
let node_path = [inspect_id.index()];
let reg_ctx = node::RegCtx::new(get_node, &node_path, vm);
graph[inspect_id].register(reg_ctx);
graph.add_edge(
src_node,
inspect_id,
gantz_core::Edge::new(edge_weight.output, node::Input(0)),
);
graph.add_edge(
inspect_id,
dst_node,
gantz_core::Edge::new(node::Output(0), edge_weight.input),
);
let node_id = egui_graph::NodeId::from_u64(inspect_id.index() as u64);
view.layout.insert(node_id, pos);
}
pub fn paste<N>(
registry: &mut gantz_ca::Registry<Graph<N>>,
editing: Option<&str>,
all_views: &mut HashMap<CommitAddr, crate::SceneView>,
all_demos: &mut HashMap<String, String>,
graph: &mut Graph<N>,
head_view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
text: &str,
pos: &PastePos,
) -> bool
where
N: Clone
+ Serialize
+ DeserializeOwned
+ CaHash
+ AsNamedRef
+ gantz_format::NodeSugar
+ 'static,
{
let copied: export::Copied<N> = match export::copied_from_str(text) {
Ok(c) => c,
Err(e) => {
log::debug!("Clipboard does not contain a valid gantz payload: {e}");
return false;
}
};
if let Some(editing) = editing {
if let Some(named) = copied
.graph
.node_weights()
.filter_map(|n| n.as_named_ref())
.find(|nr| crate::cycle::would_cycle(registry, nr.name(), editing))
{
log::warn!(
"Paste: '{}' would create a reference cycle in '{editing}'; skipping paste",
named.name()
);
return false;
}
}
let offset = crate::resolve_paste_offset(pos, &copied.positions);
let new_indices = export::paste(
registry,
all_views,
all_demos,
graph,
&mut head_view.layout,
&copied,
offset,
);
head_state.scene.interaction.selection.nodes = new_indices.into_iter().collect();
head_state.scene.interaction.selection.edges.clear();
true
}
pub fn duplicate_nodes<N>(
registry: &mut gantz_ca::Registry<Graph<N>>,
editing: Option<&str>,
all_views: &mut HashMap<CommitAddr, crate::SceneView>,
all_demos: &mut HashMap<String, String>,
graph: &mut Graph<N>,
head_view: &mut crate::SceneView,
head_state: &mut OpenHeadState,
nodes: &HashSet<NodeIndex>,
) -> bool
where
N: gantz_core::Node
+ Clone
+ Serialize
+ DeserializeOwned
+ CaHash
+ AsNamedRef
+ gantz_format::NodeSugar
+ 'static,
{
let Some(text) = copy_nodes(registry, all_views, graph, head_view, nodes) else {
return false;
};
paste(
registry,
editing,
all_views,
all_demos,
graph,
head_view,
head_state,
&text,
&PastePos::Offset(egui::vec2(20.0, 20.0)),
)
}
pub fn undo<G>(
registry: &gantz_ca::Registry<G>,
redo_stacks: &mut HashMap<gantz_ca::Head, Vec<CommitAddr>>,
head: &gantz_ca::Head,
) -> Option<CommitAddr> {
let commit_ca = registry.head_commit_ca(head).copied()?;
let parent = registry.commits().get(&commit_ca)?.parent?;
redo_stacks.entry(head.clone()).or_default().push(commit_ca);
Some(parent)
}
pub fn redo(
redo_stacks: &mut HashMap<gantz_ca::Head, Vec<CommitAddr>>,
head: &gantz_ca::Head,
) -> Option<CommitAddr> {
redo_stacks.get_mut(head)?.pop()
}
pub fn carry_layout(
live: &crate::SceneView,
matching: &gantz_ca::Matching,
new_node_count: usize,
) -> crate::SceneView {
let node_id = |ix: usize| egui_graph::NodeId::from_u64(ix as u64);
let mut view = crate::SceneView {
camera: live.camera,
layout: Default::default(),
};
for (&old_ix, &new_ix) in matching {
if let Some(pos) = live.layout.get(&node_id(old_ix)) {
view.layout.insert(node_id(new_ix), *pos);
}
}
if view.layout.is_empty() {
return view;
}
let unmapped: Vec<usize> = (0..new_node_count)
.filter(|&ix| !view.layout.contains_key(&node_id(ix)))
.collect();
for (i, ix) in unmapped.into_iter().enumerate() {
let pos = view.camera.center + egui::vec2(20.0, 20.0) * i as f32;
view.layout.insert(node_id(ix), pos);
}
view
}
#[derive(Debug)]
pub enum MergeHeadOutcome {
FastForward(CommitAddr),
Merged {
new_commit: CommitAddr,
mapping: gantz_ca::Matching,
},
Refused(Vec<String>),
Noop,
}
#[allow(clippy::too_many_arguments)]
pub fn merge_head<N>(
registry: &mut gantz_ca::Registry<Graph<N>>,
all_views: &HashMap<CommitAddr, crate::SceneView>,
timestamp: gantz_ca::Timestamp,
head: &mut gantz_ca::Head,
graph: &mut Graph<N>,
vm: &mut Engine,
head_view: &mut crate::SceneView,
selection: &mut crate::widget::graph_scene::Selection,
source: &str,
resolutions: gantz_ca::Resolutions,
auto_resolve: bool,
) -> MergeHeadOutcome
where
N: Clone + CaHash + AsNamedRef,
{
let node_id = |ix: usize| egui_graph::NodeId::from_u64(ix as u64);
let Some(&ours_tip) = registry.head_commit_ca(head) else {
log::error!("MergeHead: no commit for head {head}");
return MergeHeadOutcome::Noop;
};
let Some(&theirs_tip) = registry.names().get(source) else {
log::error!("MergeHead: unknown source branch '{source}'");
return MergeHeadOutcome::Noop;
};
let outcome = match gantz_ca::merge_commits(registry, ours_tip, theirs_tip, resolutions) {
Err(e) => {
log::warn!("MergeHead: cannot merge '{source}': {e}");
return MergeHeadOutcome::Noop;
}
Ok(gantz_ca::MergeResolution::AlreadyUpToDate) => return MergeHeadOutcome::Noop,
Ok(gantz_ca::MergeResolution::FastForward) => {
return MergeHeadOutcome::FastForward(theirs_tip);
}
Ok(gantz_ca::MergeResolution::Diverged { outcome, .. }) => outcome,
};
let blockers = crate::merge::merge_blockers(registry, head, &outcome.graph);
if !blockers.is_empty() {
return MergeHeadOutcome::Refused(blockers);
}
if !outcome.conflicts.is_empty() && !auto_resolve {
return MergeHeadOutcome::Refused(crate::merge::conflict_strings(&outcome.conflicts));
}
let mut ours_map = gantz_ca::Matching::new();
let mut theirs_only = Vec::new();
for (m, src) in outcome.node_srcs.iter().enumerate() {
match (src.ours, src.theirs) {
(Some(o), _) => {
ours_map.insert(o, m);
}
(None, Some(t)) => theirs_only.push((m, t)),
(None, None) => unreachable!("a merged node comes from somewhere"),
}
}
if let Err(e) = node::state::remap_root(vm, &ours_map) {
log::error!("MergeHead: failed to remap node state: {e}");
}
let old_layout = std::mem::take(&mut head_view.layout);
for (&o, &m) in &ours_map {
if let Some(pos) = old_layout.get(&node_id(o)) {
head_view.layout.insert(node_id(m), *pos);
}
}
selection.nodes = selection
.nodes
.iter()
.filter_map(|n| ours_map.get(&n.index()).map(|&m| NodeIndex::new(m)))
.collect();
selection.edges.clear();
let theirs_view = all_views.get(&theirs_tip);
for (i, &(m, t)) in theirs_only.iter().enumerate() {
let pos = theirs_view
.and_then(|v| v.layout.get(&node_id(t)).copied())
.unwrap_or_else(|| head_view.camera.center + egui::vec2(20.0, 20.0) * i as f32);
head_view.layout.insert(node_id(m), pos);
}
*graph = outcome.graph;
let new_commit = registry.commit_merge_to_head(
timestamp,
gantz_ca::graph_addr(&*graph),
|| graph.clone(),
theirs_tip,
head,
);
MergeHeadOutcome::Merged {
new_commit,
mapping: ours_map,
}
}
pub fn commit_layout<G>(
registry: &mut gantz_ca::Registry<G>,
views: &HashMap<CommitAddr, crate::SceneView>,
timestamp: gantz_ca::Timestamp,
head: &mut gantz_ca::Head,
live: &crate::SceneView,
) -> Option<CommitAddr> {
let head_commit_ca = *registry.head_commit_ca(head)?;
let baseline = views.get(&head_commit_ca)?;
if baseline.layout == live.layout {
return None;
}
let graph_addr = registry.commits().get(&head_commit_ca)?.graph;
Some(registry.commit_graph_to_head(
timestamp,
graph_addr,
|| unreachable!("layout commit reuses an existing graph"),
head,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::graph_scene::Selection;
use gantz_core::ROOT_STATE;
use gantz_core::node::graph::NodeIx;
use steel::SteelVal;
#[test]
fn remove_nodes_migrates_layout_and_selection() {
let node_id = |i: usize| egui_graph::NodeId::from_u64(i as u64);
let mut graph: Graph<u32> = Graph::default();
for w in 10u32..15 {
graph.add_node(w);
}
let mut layout = egui_graph::Layout::default();
for i in 0..5 {
layout.insert(node_id(i), egui::pos2(i as f32, 0.0));
}
let mut selection = Selection::default();
selection.nodes.insert(NodeIx::new(4));
let mut vm = Engine::new_base();
let reindex = remove_nodes(
&mut graph,
&mut vm,
&mut layout,
&mut selection,
[NodeIx::new(1)],
);
assert!(!reindex.is_empty());
assert_eq!(reindex.apply_to_index(4), Some(1));
assert_eq!(reindex.apply_to_index(1), None);
assert_eq!(graph.node_count(), 4);
assert_eq!(graph[NodeIx::new(1)], 14);
assert_eq!(layout.len(), 4);
assert_eq!(layout.get(&node_id(1)).copied(), Some(egui::pos2(4.0, 0.0)));
assert!(!layout.contains_key(&node_id(4)));
assert_eq!(
selection.nodes.iter().copied().collect::<Vec<_>>(),
vec![NodeIx::new(1)],
);
}
#[test]
fn carry_layout_remaps_and_places_new_nodes() {
let mut live = crate::SceneView::default();
live.camera.center = egui::pos2(100.0, 50.0);
live.layout.insert(node_id(0), egui::pos2(1.0, 1.0));
live.layout.insert(node_id(1), egui::pos2(2.0, 2.0));
live.layout.insert(node_id(2), egui::pos2(3.0, 3.0));
let matching: gantz_ca::Matching = [(0, 1), (2, 0)].into_iter().collect();
let view = carry_layout(&live, &matching, 3);
assert_eq!(view.camera, live.camera);
assert_eq!(view.layout.len(), 3);
assert_eq!(
view.layout.get(&node_id(1)).copied(),
Some(egui::pos2(1.0, 1.0))
);
assert_eq!(
view.layout.get(&node_id(0)).copied(),
Some(egui::pos2(3.0, 3.0))
);
assert_eq!(
view.layout.get(&node_id(2)).copied(),
Some(egui::pos2(100.0, 50.0))
);
}
#[test]
fn carry_layout_empty_live_yields_empty() {
let live = crate::SceneView::default();
let matching: gantz_ca::Matching = [(0, 0)].into_iter().collect();
let view = carry_layout(&live, &matching, 4);
assert!(view.layout.is_empty());
}
#[test]
fn carry_layout_no_carried_positions_yields_empty() {
let mut live = crate::SceneView::default();
live.layout.insert(node_id(5), egui::pos2(1.0, 1.0));
let matching = gantz_ca::Matching::new();
let view = carry_layout(&live, &matching, 3);
assert!(view.layout.is_empty());
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct TestNode(u32);
impl CaHash for TestNode {
fn hash(&self, hasher: &mut gantz_ca::Hasher) {
CaHash::hash(&self.0, hasher);
}
}
impl AsNamedRef for TestNode {
fn as_named_ref(&self) -> Option<&NamedRef> {
None
}
}
fn test_graph(nodes: &[u32]) -> Graph<TestNode> {
let mut g = Graph::default();
for &n in nodes {
g.add_node(TestNode(n));
}
g
}
fn node_id(ix: usize) -> egui_graph::NodeId {
egui_graph::NodeId::from_u64(ix as u64)
}
fn diverged_registry(
base: &[u32],
ours: &[u32],
theirs: &[u32],
) -> (gantz_ca::Registry<Graph<TestNode>>, gantz_ca::Head) {
let secs = |s| std::time::Duration::from_secs(s);
let mut reg = gantz_ca::Registry::default();
let g = test_graph(base);
let base_ca = reg.commit_graph(secs(1), None, gantz_ca::graph_addr(&g), || g);
let g = test_graph(ours);
let ours_ca = reg.commit_graph(secs(2), Some(base_ca), gantz_ca::graph_addr(&g), || g);
let g = test_graph(theirs);
let theirs_ca = reg.commit_graph(secs(3), Some(base_ca), gantz_ca::graph_addr(&g), || g);
reg.insert_name("alpha".to_string(), ours_ca);
reg.insert_name("beta".to_string(), theirs_ca);
(reg, gantz_ca::Head::Branch("alpha".to_string()))
}
#[allow(clippy::type_complexity)]
fn run_merge(
reg: &mut gantz_ca::Registry<Graph<TestNode>>,
head: &mut gantz_ca::Head,
graph: &mut Graph<TestNode>,
vm: &mut Engine,
view: &mut crate::SceneView,
selection: &mut Selection,
auto_resolve: bool,
) -> MergeHeadOutcome {
merge_head(
reg,
&HashMap::new(),
std::time::Duration::from_secs(9),
head,
graph,
vm,
view,
selection,
"beta",
gantz_ca::Resolutions::default(),
auto_resolve,
)
}
#[test]
fn merge_head_applies_theirs_and_commits_two_parents() {
let (mut reg, mut head) = diverged_registry(&[1, 2], &[1, 20], &[1, 2, 3]);
let ours_tip = *reg.head_commit_ca(&head).unwrap();
let theirs_tip = reg.names()["beta"];
let mut graph = test_graph(&[1, 20]);
let mut vm = Engine::new_base();
let mut view = crate::SceneView::default();
view.layout.insert(node_id(0), egui::pos2(0.0, 0.0));
view.layout.insert(node_id(1), egui::pos2(1.0, 0.0));
let mut selection = Selection::default();
selection.nodes.insert(NodeIx::new(1));
let outcome = run_merge(
&mut reg,
&mut head,
&mut graph,
&mut vm,
&mut view,
&mut selection,
false,
);
let MergeHeadOutcome::Merged { new_commit, .. } = outcome else {
panic!("expected Merged, got {outcome:?}");
};
let weights: Vec<u32> = graph.node_weights().map(|n| n.0).collect();
assert_eq!(weights, vec![1, 20, 3]);
assert_eq!(view.layout.get(&node_id(1)), Some(&egui::pos2(1.0, 0.0)));
assert!(view.layout.contains_key(&node_id(2)));
assert!(selection.nodes.contains(&NodeIx::new(1)));
let commit = ®.commits()[&new_commit];
assert_eq!(commit.parent, Some(ours_tip));
assert_eq!(commit.merge_parents, vec![theirs_tip]);
assert_eq!(reg.head_commit_ca(&head), Some(&new_commit));
}
#[test]
fn merge_head_migrates_state_layout_selection_on_removal() {
let (mut reg, mut head) = diverged_registry(&[1, 2], &[1, 2], &[2]);
let mut graph = test_graph(&[1, 2]);
let mut vm = Engine::new_base();
vm.register_value(ROOT_STATE, SteelVal::empty_hashmap());
node::state::update_value(&mut vm, &[1], SteelVal::IntV(42)).unwrap();
let mut view = crate::SceneView::default();
view.layout.insert(node_id(0), egui::pos2(0.0, 0.0));
view.layout.insert(node_id(1), egui::pos2(1.0, 0.0));
let mut selection = Selection::default();
selection.nodes.insert(NodeIx::new(1));
let outcome = run_merge(
&mut reg,
&mut head,
&mut graph,
&mut vm,
&mut view,
&mut selection,
false,
);
let MergeHeadOutcome::Merged { mapping, .. } = outcome else {
panic!("expected Merged, got {outcome:?}");
};
assert_eq!(mapping, gantz_ca::Matching::from([(1, 0)]));
let weights: Vec<u32> = graph.node_weights().map(|n| n.0).collect();
assert_eq!(weights, vec![2]);
let state = node::state::extract_value(&vm, &[0]).unwrap();
assert_eq!(state, Some(SteelVal::IntV(42)));
assert_eq!(view.layout.len(), 1);
assert_eq!(view.layout.get(&node_id(0)), Some(&egui::pos2(1.0, 0.0)));
assert_eq!(
selection.nodes.iter().copied().collect::<Vec<_>>(),
vec![NodeIx::new(0)],
);
}
#[test]
fn merge_head_refuses_conflicts_unless_auto_resolve() {
let (mut reg, mut head) = diverged_registry(&[1, 2], &[1, 20], &[1, 30]);
let ours_tip = *reg.head_commit_ca(&head).unwrap();
let mut graph = test_graph(&[1, 20]);
let mut vm = Engine::new_base();
let mut view = crate::SceneView::default();
let mut selection = Selection::default();
let outcome = run_merge(
&mut reg,
&mut head,
&mut graph,
&mut vm,
&mut view,
&mut selection,
false,
);
let MergeHeadOutcome::Refused(reasons) = outcome else {
panic!("expected Refused, got {outcome:?}");
};
assert!(!reasons.is_empty());
assert_eq!(reg.head_commit_ca(&head), Some(&ours_tip));
assert_eq!(
graph.node_weights().map(|n| n.0).collect::<Vec<_>>(),
[1, 20]
);
let outcome = run_merge(
&mut reg,
&mut head,
&mut graph,
&mut vm,
&mut view,
&mut selection,
true,
);
assert!(matches!(outcome, MergeHeadOutcome::Merged { .. }));
assert_eq!(
graph.node_weights().map(|n| n.0).collect::<Vec<_>>(),
[1, 20]
);
assert_ne!(reg.head_commit_ca(&head), Some(&ours_tip));
}
#[test]
fn merge_head_fast_forwards() {
let secs = |s| std::time::Duration::from_secs(s);
let mut reg = gantz_ca::Registry::default();
let g = test_graph(&[1]);
let base_ca = reg.commit_graph(secs(1), None, gantz_ca::graph_addr(&g), || g);
let g = test_graph(&[1, 2]);
let theirs_ca = reg.commit_graph(secs(2), Some(base_ca), gantz_ca::graph_addr(&g), || g);
reg.insert_name("alpha".to_string(), base_ca);
reg.insert_name("beta".to_string(), theirs_ca);
let mut head = gantz_ca::Head::Branch("alpha".to_string());
let mut graph = test_graph(&[1]);
let mut vm = Engine::new_base();
let mut view = crate::SceneView::default();
let mut selection = Selection::default();
let outcome = run_merge(
&mut reg,
&mut head,
&mut graph,
&mut vm,
&mut view,
&mut selection,
false,
);
let MergeHeadOutcome::FastForward(target) = outcome else {
panic!("expected FastForward, got {outcome:?}");
};
assert_eq!(target, theirs_ca);
assert_eq!(reg.names()["alpha"], base_ca);
assert_eq!(graph.node_count(), 1);
}
}