|
1 | 1 | //! Tests for visualizing the graph data structure.
|
| 2 | +
|
2 | 3 | use but_core::ref_metadata;
|
3 |
| -use but_graph::{CommitFlags, Graph, Segment, SegmentIndex, SegmentMetadata}; |
| 4 | +use but_graph::{Commit, CommitFlags, Graph, Segment, SegmentIndex, SegmentMetadata}; |
| 5 | +use but_testsupport::graph_tree; |
| 6 | +use gix::ObjectId; |
| 7 | +use std::str::FromStr; |
4 | 8 |
|
5 | 9 | /// Simulate a graph data structure after the first pass, i.e., right after the walk.
|
6 | 10 | /// There is no pruning of 'empty' branches, just a perfect representation of the graph as is,
|
@@ -101,241 +105,34 @@ fn detached_head() {
|
101 | 105 | ");
|
102 | 106 | }
|
103 | 107 |
|
104 |
| -#[test] |
105 |
| -fn unborn_head() { |
106 |
| - insta::assert_snapshot!(graph_tree(&Graph::default()), @"<UNBORN>"); |
107 |
| -} |
108 |
| - |
109 |
| -pub(crate) mod utils { |
110 |
| - use but_graph::{Commit, CommitFlags, SegmentMetadata}; |
111 |
| - use but_graph::{EntryPoint, Graph, SegmentIndex}; |
112 |
| - |
113 |
| - use gix::ObjectId; |
114 |
| - use termtree::Tree; |
115 |
| - |
116 |
| - use but_graph::projection::StackCommitDebugFlags; |
117 |
| - use std::collections::{BTreeMap, BTreeSet}; |
118 |
| - use std::str::FromStr; |
119 |
| - |
120 |
| - pub fn commit( |
121 |
| - id: ObjectId, |
122 |
| - parent_ids: impl IntoIterator<Item = ObjectId>, |
123 |
| - flags: CommitFlags, |
124 |
| - ) -> Commit { |
125 |
| - Commit { |
126 |
| - id, |
127 |
| - parent_ids: parent_ids.into_iter().collect(), |
128 |
| - refs: Vec::new(), |
129 |
| - flags, |
130 |
| - } |
131 |
| - } |
132 |
| - |
133 |
| - pub fn id(hex: &str) -> ObjectId { |
134 |
| - let hash_len = gix::hash::Kind::Sha1.len_in_hex(); |
135 |
| - if hex.len() != hash_len { |
136 |
| - ObjectId::from_str( |
137 |
| - &std::iter::repeat_n(hex, hash_len / hex.len()) |
138 |
| - .collect::<Vec<_>>() |
139 |
| - .join(""), |
140 |
| - ) |
141 |
| - } else { |
142 |
| - ObjectId::from_str(hex) |
143 |
| - } |
144 |
| - .unwrap() |
145 |
| - } |
146 |
| - |
147 |
| - type StringTree = Tree<String>; |
148 |
| - |
149 |
| - /// Visualize `graph` as a tree. |
150 |
| - pub fn graph_workspace(workspace: &but_graph::projection::Workspace) -> StringTree { |
151 |
| - let commit_flags = workspace |
152 |
| - .graph |
153 |
| - .hard_limit_hit() |
154 |
| - .then_some(StackCommitDebugFlags::HardLimitReached) |
155 |
| - .unwrap_or_default(); |
156 |
| - let mut root = Tree::new(workspace.debug_string()); |
157 |
| - for stack in &workspace.stacks { |
158 |
| - root.push(tree_for_stack(stack, commit_flags)); |
159 |
| - } |
160 |
| - root |
161 |
| - } |
162 |
| - |
163 |
| - fn tree_for_stack( |
164 |
| - stack: &but_graph::projection::Stack, |
165 |
| - commit_flags: StackCommitDebugFlags, |
166 |
| - ) -> StringTree { |
167 |
| - let mut root = Tree::new(stack.debug_string()); |
168 |
| - for segment in &stack.segments { |
169 |
| - root.push(tree_for_stack_segment(segment, commit_flags)); |
170 |
| - } |
171 |
| - root |
172 |
| - } |
173 |
| - |
174 |
| - fn tree_for_stack_segment( |
175 |
| - segment: &but_graph::projection::StackSegment, |
176 |
| - commit_flags: StackCommitDebugFlags, |
177 |
| - ) -> StringTree { |
178 |
| - let mut root = Tree::new(segment.debug_string()); |
179 |
| - if let Some(outside) = &segment.commits_outside { |
180 |
| - for commit in outside { |
181 |
| - root.push(format!("{}*", commit.debug_string(commit_flags))); |
182 |
| - } |
183 |
| - } |
184 |
| - for commit in &segment.commits_on_remote { |
185 |
| - root.push(commit.debug_string(commit_flags | StackCommitDebugFlags::RemoteOnly)); |
186 |
| - } |
187 |
| - for commit in &segment.commits { |
188 |
| - root.push(commit.debug_string(commit_flags)); |
189 |
| - } |
190 |
| - root |
191 |
| - } |
192 |
| - |
193 |
| - /// Visualize `graph` as a tree. |
194 |
| - pub fn graph_tree(graph: &Graph) -> StringTree { |
195 |
| - let mut root = Tree::new("".to_string()); |
196 |
| - let mut seen = Default::default(); |
197 |
| - for sidx in graph.tip_segments() { |
198 |
| - root.push(recurse_segment(graph, sidx, &mut seen)); |
199 |
| - } |
200 |
| - let missing = graph.num_segments() - seen.len(); |
201 |
| - if missing > 0 { |
202 |
| - let mut missing = Tree::new(format!( |
203 |
| - "ERROR: disconnected {missing} nodes unreachable through base" |
204 |
| - )); |
205 |
| - let mut newly_seen = Default::default(); |
206 |
| - for sidx in graph.segments().filter(|sidx| !seen.contains(sidx)) { |
207 |
| - missing.push(recurse_segment(graph, sidx, &mut newly_seen)); |
208 |
| - } |
209 |
| - root.push(missing); |
210 |
| - seen.extend(newly_seen); |
211 |
| - } |
212 |
| - |
213 |
| - if seen.is_empty() { |
214 |
| - "<UNBORN>".to_string().into() |
215 |
| - } else { |
216 |
| - root |
217 |
| - } |
218 |
| - } |
219 |
| - |
220 |
| - fn no_first_commit_on_named_segments(mut ep: EntryPoint) -> EntryPoint { |
221 |
| - if ep.segment.ref_name.is_some() && ep.commit_index == Some(0) { |
222 |
| - ep.commit_index = None; |
223 |
| - } |
224 |
| - ep |
| 108 | +fn id(hex: &str) -> ObjectId { |
| 109 | + let hash_len = gix::hash::Kind::Sha1.len_in_hex(); |
| 110 | + if hex.len() != hash_len { |
| 111 | + ObjectId::from_str( |
| 112 | + &std::iter::repeat_n(hex, hash_len / hex.len()) |
| 113 | + .collect::<Vec<_>>() |
| 114 | + .join(""), |
| 115 | + ) |
| 116 | + } else { |
| 117 | + ObjectId::from_str(hex) |
225 | 118 | }
|
| 119 | + .unwrap() |
| 120 | +} |
226 | 121 |
|
227 |
| - fn tree_for_commit( |
228 |
| - commit: &but_graph::Commit, |
229 |
| - is_entrypoint: bool, |
230 |
| - is_early_end: bool, |
231 |
| - hard_limit_hit: bool, |
232 |
| - ) -> StringTree { |
233 |
| - Graph::commit_debug_string(commit, is_entrypoint, is_early_end, hard_limit_hit).into() |
| 122 | +fn commit( |
| 123 | + id: ObjectId, |
| 124 | + parent_ids: impl IntoIterator<Item = ObjectId>, |
| 125 | + flags: CommitFlags, |
| 126 | +) -> Commit { |
| 127 | + Commit { |
| 128 | + id, |
| 129 | + parent_ids: parent_ids.into_iter().collect(), |
| 130 | + refs: Vec::new(), |
| 131 | + flags, |
234 | 132 | }
|
235 |
| - fn recurse_segment( |
236 |
| - graph: &but_graph::Graph, |
237 |
| - sidx: SegmentIndex, |
238 |
| - seen: &mut BTreeSet<SegmentIndex>, |
239 |
| - ) -> StringTree { |
240 |
| - let segment = &graph[sidx]; |
241 |
| - if seen.contains(&sidx) { |
242 |
| - return format!( |
243 |
| - "→:{sidx}:{name}", |
244 |
| - sidx = sidx.index(), |
245 |
| - name = graph[sidx] |
246 |
| - .ref_name |
247 |
| - .as_ref() |
248 |
| - .map(|n| format!( |
249 |
| - " ({}{maybe_sibling})", |
250 |
| - Graph::ref_debug_string(n), |
251 |
| - maybe_sibling = segment |
252 |
| - .sibling_segment_id |
253 |
| - .map_or_else(String::new, |sid| format!(" →:{}:", sid.index())) |
254 |
| - )) |
255 |
| - .unwrap_or_default() |
256 |
| - ) |
257 |
| - .into(); |
258 |
| - } |
259 |
| - seen.insert(sidx); |
260 |
| - let ep = no_first_commit_on_named_segments(graph.lookup_entrypoint().unwrap()); |
261 |
| - let segment_is_entrypoint = ep.segment_index == sidx; |
262 |
| - let mut show_segment_entrypoint = segment_is_entrypoint; |
263 |
| - if segment_is_entrypoint { |
264 |
| - // Reduce noise by preferring ref-based entry-points. |
265 |
| - if segment.ref_name.is_none() && ep.commit_index.is_some() { |
266 |
| - show_segment_entrypoint = false; |
267 |
| - } |
268 |
| - } |
269 |
| - let connected_segments = { |
270 |
| - let mut m = BTreeMap::<_, Vec<_>>::new(); |
271 |
| - let below = graph.segments_below_in_order(sidx).collect::<Vec<_>>(); |
272 |
| - for (cidx, sidx) in below { |
273 |
| - m.entry(cidx).or_default().push(sidx); |
274 |
| - } |
275 |
| - m |
276 |
| - }; |
277 |
| - |
278 |
| - let mut root = Tree::new(format!( |
279 |
| - "{entrypoint}{meta}{arrow}:{id}[{generation}]:{ref_name_and_remote}", |
280 |
| - meta = match segment.metadata { |
281 |
| - None => { |
282 |
| - "" |
283 |
| - } |
284 |
| - Some(SegmentMetadata::Workspace(_)) => { |
285 |
| - "📕" |
286 |
| - } |
287 |
| - Some(SegmentMetadata::Branch(_)) => { |
288 |
| - "📙" |
289 |
| - } |
290 |
| - }, |
291 |
| - id = segment.id.index(), |
292 |
| - generation = segment.generation, |
293 |
| - arrow = if segment.workspace_metadata().is_some() { |
294 |
| - "►►►" |
295 |
| - } else { |
296 |
| - "►" |
297 |
| - }, |
298 |
| - entrypoint = if show_segment_entrypoint { |
299 |
| - if ep.commit.is_none() && ep.commit_index.is_some() { |
300 |
| - "🫱" |
301 |
| - } else { |
302 |
| - "👉" |
303 |
| - } |
304 |
| - } else { |
305 |
| - "" |
306 |
| - }, |
307 |
| - ref_name_and_remote = Graph::ref_and_remote_debug_string( |
308 |
| - segment.ref_name.as_ref(), |
309 |
| - segment.remote_tracking_ref_name.as_ref(), |
310 |
| - segment.sibling_segment_id |
311 |
| - ), |
312 |
| - )); |
313 |
| - for (cidx, commit) in segment.commits.iter().enumerate() { |
314 |
| - let mut commit_tree = tree_for_commit( |
315 |
| - commit, |
316 |
| - segment_is_entrypoint && Some(cidx) == ep.commit_index, |
317 |
| - if cidx + 1 != segment.commits.len() { |
318 |
| - false |
319 |
| - } else { |
320 |
| - graph.is_early_end_of_traversal(sidx) |
321 |
| - }, |
322 |
| - graph.hard_limit_hit(), |
323 |
| - ); |
324 |
| - if let Some(segment_indices) = connected_segments.get(&Some(cidx)) { |
325 |
| - for sidx in segment_indices { |
326 |
| - commit_tree.push(recurse_segment(graph, *sidx, seen)); |
327 |
| - } |
328 |
| - } |
329 |
| - root.push(commit_tree); |
330 |
| - } |
331 |
| - // Get the segments that are directly connected. |
332 |
| - if let Some(segment_indices) = connected_segments.get(&None) { |
333 |
| - for sidx in segment_indices { |
334 |
| - root.push(recurse_segment(graph, *sidx, seen)); |
335 |
| - } |
336 |
| - } |
| 133 | +} |
337 | 134 |
|
338 |
| - root |
339 |
| - } |
| 135 | +#[test] |
| 136 | +fn unborn_head() { |
| 137 | + insta::assert_snapshot!(graph_tree(&Graph::default()), @"<UNBORN>"); |
340 | 138 | }
|
341 |
| -use utils::{commit, graph_tree, id}; |
0 commit comments