Format the provenance decision graph for display. When `--verbose` is passed via the top-level `-p` flag the display includes the structured `detail` field from each provenance node so the user can see the actual commands, file paths, and diffs.
(&self, change_hash: &Hash, repo: &Repository)
| 599 | /// includes the structured `detail` field from each provenance node so |
| 600 | /// the user can see the actual commands, file paths, and diffs. |
| 601 | fn format_provenance_graph(&self, change_hash: &Hash, repo: &Repository) -> String { |
| 602 | let mut output = String::new(); |
| 603 | |
| 604 | let graphs = match repo.find_provenance_for_change(change_hash) { |
| 605 | Ok(g) if !g.is_empty() => g, |
| 606 | Ok(_) => { |
| 607 | output.push_str(&format!( |
| 608 | "{}\n", |
| 609 | hint("No provenance graph found for this change.") |
| 610 | )); |
| 611 | return output; |
| 612 | } |
| 613 | Err(e) => { |
| 614 | output.push_str(&format!( |
| 615 | "{}\n", |
| 616 | hint(&format!("Failed to load provenance: {}", e)) |
| 617 | )); |
| 618 | return output; |
| 619 | } |
| 620 | }; |
| 621 | |
| 622 | for (_graph_hash, graph) in &graphs { |
| 623 | output.push_str(&format!("{}\n", emphasis("Provenance Graph:"))); |
| 624 | output.push_str(&format!(" Session: {}\n", info(&graph.session_id))); |
| 625 | output.push_str(&format!( |
| 626 | " Agent: {} ({})\n", |
| 627 | info(&graph.agent_display_name), |
| 628 | hint(&graph.agent_vendor) |
| 629 | )); |
| 630 | output.push_str(&format!( |
| 631 | " Nodes: {} Edges: {} Changes: {}\n", |
| 632 | graph.node_count(), |
| 633 | graph.edge_count(), |
| 634 | graph.change_count() |
| 635 | )); |
| 636 | output.push('\n'); |
| 637 | |
| 638 | // Display nodes |
| 639 | for node in &graph.nodes { |
| 640 | let kind_str = format!("{}", node.kind); |
| 641 | let kind_styled = format!("{}", style(&kind_str).bold().cyan()); |
| 642 | let duration = node |
| 643 | .duration_ms |
| 644 | .map(|ms| format!(" ({}ms)", ms)) |
| 645 | .unwrap_or_default(); |
| 646 | let tool = node.tool_name.as_deref().unwrap_or(""); |
| 647 | let tool_str = if tool.is_empty() { |
| 648 | String::new() |
| 649 | } else { |
| 650 | format!(" {}", hint(&format!("[{}]", tool))) |
| 651 | }; |
| 652 | |
| 653 | // Extract a one-line detail string from the structured detail JSON |
| 654 | let detail_str = node |
| 655 | .detail |
| 656 | .as_ref() |
| 657 | .and_then(|d| format_node_detail(d)) |
| 658 | .map(|s| format!("\n {}", hint(&s))) |
no test coverage detected