|
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