1use crate::{Graph, Snapshot};
2use serde::Serialize;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct DiffCounts {
7 pub added: usize,
8 pub removed: usize,
9 pub affected: usize,
10 pub unchanged: usize,
11}
12
13#[derive(Debug, Clone, Serialize)]
14pub struct LevelDiff {
15 pub nodes: DiffCounts,
16 pub edges: DiffCounts,
17 pub cycle_nodes_before: usize,
19 pub cycle_nodes_after: usize,
20 pub sccs_before: usize,
22 pub sccs_after: usize,
23}
24
25#[derive(Debug, Clone, Serialize)]
26pub struct SnapMeta {
27 pub target: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub branch: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub commit: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct CompareSummary {
36 pub schema_version: String,
37 pub before: SnapMeta,
38 pub after: SnapMeta,
39 pub identical: bool,
40 pub modules: LevelDiff,
41 pub files: LevelDiff,
42 pub functions: LevelDiff,
43}
44
45pub fn compare_snapshots(before: &Snapshot, after: &Snapshot) -> CompareSummary {
46 let modules = diff_graph(&before.graphs.modules, &after.graphs.modules);
47 let files = diff_graph(&before.graphs.files, &after.graphs.files);
48 let functions = diff_graph(&before.graphs.functions, &after.graphs.functions);
49
50 let identical = [&modules, &files, &functions].iter().all(|d| {
51 d.nodes.added == 0
52 && d.nodes.removed == 0
53 && d.nodes.affected == 0
54 && d.edges.added == 0
55 && d.edges.removed == 0
56 && d.edges.affected == 0
57 });
58
59 CompareSummary {
60 schema_version: "1".to_string(),
61 before: snap_meta(before),
62 after: snap_meta(after),
63 identical,
64 modules,
65 files,
66 functions,
67 }
68}
69
70fn snap_meta(snap: &Snapshot) -> SnapMeta {
71 let commit_short = snap
72 .git
73 .as_ref()
74 .map(|g| g.commit[..8.min(g.commit.len())].to_string());
75 SnapMeta {
76 target: snap
77 .target
78 .split('/')
79 .next_back()
80 .unwrap_or(&snap.target)
81 .to_string(),
82 branch: snap.git.as_ref().map(|g| g.branch.clone()),
83 commit: commit_short,
84 }
85}
86
87fn diff_graph(before: &Graph, after: &Graph) -> LevelDiff {
89 let bg: HashMap<String, ()> = before
91 .nodes
92 .iter()
93 .filter(|n| !n.external.unwrap_or(false))
94 .map(|n| (n.id.clone(), ()))
95 .collect();
96 let ag: HashMap<String, ()> = after
97 .nodes
98 .iter()
99 .filter(|n| !n.external.unwrap_or(false))
100 .map(|n| (n.id.clone(), ()))
101 .collect();
102
103 let mut node_status: HashMap<String, u8> = HashMap::new();
105 for id in ag.keys() {
106 node_status.insert(id.clone(), if bg.contains_key(id) { 0 } else { 1 });
107 }
108 for id in bg.keys() {
109 if !ag.contains_key(id) {
110 node_status.insert(id.clone(), 2);
111 }
112 }
113
114 let ekey = |e: &crate::Edge| format!("{}\x00{}\x00{:?}", e.from, e.to, e.kind);
116
117 let local_edges = |edges: &[crate::Edge]| -> HashMap<String, (String, String)> {
119 edges
120 .iter()
121 .filter(|e| node_status.contains_key(&e.from) && node_status.contains_key(&e.to))
122 .map(|e| (ekey(e), (e.from.clone(), e.to.clone())))
123 .collect()
124 };
125
126 let bg_edges = local_edges(&before.edges);
127 let ag_edges = local_edges(&after.edges);
128
129 let mut edge_list: Vec<(String, String, u8)> = Vec::new();
131 for (key, (from, to)) in &ag_edges {
132 edge_list.push((
133 from.clone(),
134 to.clone(),
135 if bg_edges.contains_key(key) { 0 } else { 1 },
136 ));
137 }
138 for (key, (from, to)) in &bg_edges {
139 if !ag_edges.contains_key(key) {
140 edge_list.push((from.clone(), to.clone(), 2));
141 }
142 }
143
144 for (from, to, status) in &edge_list {
146 if *status != 0 {
147 if node_status.get(from.as_str()) == Some(&0) {
148 node_status.insert(from.clone(), 3);
149 }
150 if node_status.get(to.as_str()) == Some(&0) {
151 node_status.insert(to.clone(), 3);
152 }
153 }
154 }
155
156 let mut nodes = DiffCounts {
158 added: 0,
159 removed: 0,
160 affected: 0,
161 unchanged: 0,
162 };
163 for &s in node_status.values() {
164 match s {
165 1 => nodes.added += 1,
166 2 => nodes.removed += 1,
167 3 => nodes.affected += 1,
168 _ => nodes.unchanged += 1,
169 }
170 }
171
172 let mut edges = DiffCounts {
174 added: 0,
175 removed: 0,
176 affected: 0,
177 unchanged: 0,
178 };
179 for (from, to, status) in &edge_list {
180 let s = if *status == 0
181 && (node_status.get(from.as_str()) != Some(&0)
182 || node_status.get(to.as_str()) != Some(&0))
183 {
184 3u8
185 } else {
186 *status
187 };
188 match s {
189 1 => edges.added += 1,
190 2 => edges.removed += 1,
191 3 => edges.affected += 1,
192 _ => edges.unchanged += 1,
193 }
194 }
195
196 LevelDiff {
197 nodes,
198 edges,
199 cycle_nodes_before: before.cycles.iter().map(|c| c.nodes.len()).sum(),
200 cycle_nodes_after: after.cycles.iter().map(|c| c.nodes.len()).sum(),
201 sccs_before: before.cycles.len(),
202 sccs_after: after.cycles.len(),
203 }
204}