use crate::GraphViews;
use gantz_ca::{CommitAddr, registry::MergeResult};
use gantz_core::node::{self, graph::Graph};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
pub const FILE_EXTENSION: &str = "gantz";
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Export<G> {
pub registry: gantz_ca::Registry<G>,
#[serde(default)]
pub views: HashMap<CommitAddr, GraphViews>,
}
pub fn export_with_views<G>(
registry: gantz_ca::Registry<G>,
all_views: &HashMap<CommitAddr, GraphViews>,
) -> Export<G>
where
G: Clone,
{
let views = all_views
.iter()
.filter(|(ca, _)| registry.commits().contains_key(ca))
.map(|(&ca, v)| (ca, v.clone()))
.collect();
Export { registry, views }
}
pub fn merge_with_views<G>(
registry: &mut gantz_ca::Registry<G>,
views: &mut HashMap<CommitAddr, GraphViews>,
export: Export<G>,
) -> MergeResult {
let result = registry.merge(export.registry);
for (ca, v) in export.views {
views.entry(ca).or_insert(v);
}
result
}
pub fn default_filename(head: &gantz_ca::Head) -> String {
match head {
gantz_ca::Head::Branch(name) => format!("{name}.{FILE_EXTENSION}"),
gantz_ca::Head::Commit(ca) => format!("{}.{FILE_EXTENSION}", ca.display_short()),
}
}
pub fn is_gantz_path(path: &std::path::Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.eq_ignore_ascii_case(FILE_EXTENSION))
.unwrap_or(false)
}
pub fn is_maybe_gantz(path: Option<&std::path::Path>) -> bool {
path.map(is_gantz_path).unwrap_or(true)
}
pub fn read_dropped_file(file: &egui::DroppedFile) -> Option<Vec<u8>> {
if let Some(ref bytes) = file.bytes {
return Some(bytes.to_vec());
}
if let Some(ref path) = file.path {
return std::fs::read(path).ok();
}
None
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Copied<N> {
pub export: Export<Graph<N>>,
pub graph: Graph<N>,
pub positions: egui_graph::Layout,
}
pub fn copy<N>(
registry: &gantz_ca::Registry<Graph<N>>,
all_views: &HashMap<CommitAddr, GraphViews>,
graph: &Graph<N>,
selected: &HashSet<node::graph::NodeIx>,
layout: &egui_graph::Layout,
) -> Copied<N>
where
N: Clone + gantz_core::Node,
{
let subgraph = gantz_core::graph::extract_subgraph(graph, selected);
let mut positions = egui_graph::Layout::default();
let sorted: std::collections::BTreeSet<_> = selected.iter().copied().collect();
for (old_ix, new_ix) in sorted.iter().zip(subgraph.node_indices()) {
let old_id = egui_graph::NodeId(old_ix.index() as u64);
let new_id = egui_graph::NodeId(new_ix.index() as u64);
if let Some(&pos) = layout.get(&old_id) {
positions.insert(new_id, pos);
}
}
let mut required_commits = HashSet::new();
for n in subgraph.node_indices() {
for ca in subgraph[n].required_addrs() {
let commit_ca = CommitAddr::from(ca);
if registry.commits().contains_key(&commit_ca) {
required_commits.insert(commit_ca);
}
}
}
let export_registry = registry.export(&required_commits);
let export = export_with_views(export_registry, all_views);
Copied {
export,
graph: subgraph,
positions,
}
}
pub fn paste<N>(
registry: &mut gantz_ca::Registry<Graph<N>>,
views: &mut HashMap<CommitAddr, GraphViews>,
target_graph: &mut Graph<N>,
target_layout: &mut egui_graph::Layout,
copied: &Copied<N>,
offset: egui::Vec2,
) -> Vec<node::graph::NodeIx>
where
N: Clone,
{
merge_with_views(registry, views, copied.export.clone());
let new_indices = gantz_core::graph::add_subgraph(target_graph, &copied.graph);
for (sub_ix, &target_ix) in copied.graph.node_indices().zip(new_indices.iter()) {
let sub_id = egui_graph::NodeId(sub_ix.index() as u64);
let target_id = egui_graph::NodeId(target_ix.index() as u64);
if let Some(&pos) = copied.positions.get(&sub_id) {
target_layout.insert(target_id, pos + offset);
}
}
new_indices
}
#[cfg(test)]
mod tests {
use super::*;
use gantz_ca::{Commit, ContentAddr};
use std::{collections::BTreeMap, time::Duration};
fn graph_addr(n: u8) -> gantz_ca::GraphAddr {
gantz_ca::GraphAddr::from(ContentAddr::from([n; 32]))
}
fn commit_addr_raw(n: u8) -> CommitAddr {
CommitAddr::from(ContentAddr::from([n; 32]))
}
fn test_export() -> Export<String> {
let ga = graph_addr(1);
let ca = commit_addr_raw(10);
let commit = Commit::new(Duration::from_secs(1), None, ga);
let registry = gantz_ca::Registry::new(
HashMap::from([(ga, "graph_a".to_string())]),
HashMap::from([(ca, commit)]),
BTreeMap::from([("alpha".to_string(), ca)]),
);
Export {
registry,
views: HashMap::new(),
}
}
#[test]
fn round_trip_serde() {
let export = test_export();
let s = ron::to_string(&export).expect("serialize");
let recovered: Export<String> = ron::from_str(&s).expect("deserialize");
assert_eq!(
export.registry.commits().len(),
recovered.registry.commits().len()
);
assert_eq!(
export.registry.graphs().len(),
recovered.registry.graphs().len()
);
assert_eq!(
export.registry.names().len(),
recovered.registry.names().len()
);
let ca = commit_addr_raw(10);
assert!(recovered.registry.commits().contains_key(&ca));
assert_eq!(recovered.registry.names().get("alpha"), Some(&ca));
}
#[test]
fn round_trip_export_merge_recovers_data() {
let export = test_export();
let s = ron::to_string(&export).expect("serialize");
let recovered: Export<String> = ron::from_str(&s).expect("deserialize");
let mut target = gantz_ca::Registry::<String>::default();
let mut views = HashMap::new();
let result = merge_with_views(&mut target, &mut views, recovered);
assert_eq!(result.names_added, vec!["alpha".to_string()]);
assert!(result.names_replaced.is_empty());
let ca = commit_addr_raw(10);
assert!(target.commits().contains_key(&ca));
assert_eq!(target.names().get("alpha"), Some(&ca));
}
#[test]
fn export_with_views_filters_views() {
let ga = graph_addr(1);
let ca = commit_addr_raw(10);
let cb = commit_addr_raw(20);
let commit = Commit::new(Duration::from_secs(1), None, ga);
let registry = gantz_ca::Registry::new(
HashMap::from([(ga, "g".to_string())]),
HashMap::from([(ca, commit)]),
BTreeMap::new(),
);
let mut all_views = HashMap::new();
all_views.insert(ca, GraphViews::new());
all_views.insert(cb, GraphViews::new()); let export = export_with_views(registry, &all_views);
assert!(export.views.contains_key(&ca));
assert!(!export.views.contains_key(&cb));
}
#[test]
fn copied_round_trip_serde() {
use gantz_core::Edge;
let mut graph: Graph<String> = Graph::default();
let a = graph.add_node("A".to_string());
let b = graph.add_node("B".to_string());
graph.add_edge(a, b, Edge::new(0.into(), 0.into()));
let mut positions = egui_graph::Layout::default();
positions.insert(egui_graph::NodeId(0), egui::pos2(10.0, 20.0));
positions.insert(egui_graph::NodeId(1), egui::pos2(30.0, 40.0));
let copied = Copied {
export: Export::default(),
graph,
positions,
};
let s = ron::to_string(&copied).expect("serialize");
let recovered: Copied<String> = ron::from_str(&s).expect("deserialize");
assert_eq!(recovered.graph.node_count(), 2);
assert_eq!(recovered.graph.edge_count(), 1);
assert_eq!(recovered.positions.len(), 2);
assert_eq!(
recovered.positions[&egui_graph::NodeId(0)],
egui::pos2(10.0, 20.0),
);
assert_eq!(
recovered.positions[&egui_graph::NodeId(1)],
egui::pos2(30.0, 40.0),
);
}
#[test]
fn merge_with_views_keeps_existing_views() {
let ga = graph_addr(1);
let ca = commit_addr_raw(10);
let commit = Commit::new(Duration::from_secs(1), None, ga);
let mut registry = gantz_ca::Registry::new(
HashMap::from([(ga, "g".to_string())]),
HashMap::from([(ca, commit.clone())]),
BTreeMap::new(),
);
let mut existing_view = GraphViews::new();
existing_view.insert(vec![0], egui_graph::View::default());
let mut views = HashMap::from([(ca, existing_view)]);
let export = Export {
registry: gantz_ca::Registry::new(
HashMap::from([(ga, "g".to_string())]),
HashMap::from([(ca, commit)]),
BTreeMap::new(),
),
views: HashMap::from([(ca, GraphViews::new())]),
};
merge_with_views(&mut registry, &mut views, export);
assert_eq!(views[&ca].len(), 1);
}
#[test]
fn is_gantz_path_matches_extension() {
use std::path::Path;
assert!(is_gantz_path(Path::new("foo.gantz")));
assert!(is_gantz_path(Path::new("/tmp/bar.gantz")));
assert!(is_gantz_path(Path::new("x.GANTZ")));
assert!(!is_gantz_path(Path::new("foo.txt")));
assert!(!is_gantz_path(Path::new("foo")));
assert!(!is_gantz_path(Path::new("gantz")));
}
}