fix(graph): traverse all dependency types, not just parent-child

The graph command now shows issues connected via any dependency type
(blocks, parent-child, etc.), not just parent-child relationships.

This makes bd graph useful for epics where children are connected via
blocks dependencies rather than hierarchical parent-child.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-18 13:51:26 -08:00
parent a89de1ac8b
commit 7f9ee3d1c4

View File

@@ -108,9 +108,73 @@ func init() {
}
// loadGraphSubgraph loads an issue and its subgraph for visualization
// Reuses template subgraph loading logic
// Unlike template loading, this includes ALL dependency types (not just parent-child)
func loadGraphSubgraph(ctx context.Context, s storage.Storage, issueID string) (*TemplateSubgraph, error) {
return loadTemplateSubgraph(ctx, s, issueID)
if s == nil {
return nil, fmt.Errorf("no database connection")
}
// Get the root issue
root, err := s.GetIssue(ctx, issueID)
if err != nil {
return nil, fmt.Errorf("failed to get issue: %w", err)
}
if root == nil {
return nil, fmt.Errorf("issue %s not found", issueID)
}
subgraph := &TemplateSubgraph{
Root: root,
Issues: []*types.Issue{root},
IssueMap: map[string]*types.Issue{root.ID: root},
}
// BFS to find all connected issues (via any dependency type)
// We traverse both directions: dependents and dependencies
queue := []string{root.ID}
visited := map[string]bool{root.ID: true}
for len(queue) > 0 {
currentID := queue[0]
queue = queue[1:]
// Get issues that depend on this one (dependents)
dependents, err := s.GetDependents(ctx, currentID)
if err != nil {
continue
}
for _, dep := range dependents {
if !visited[dep.ID] {
visited[dep.ID] = true
subgraph.Issues = append(subgraph.Issues, dep)
subgraph.IssueMap[dep.ID] = dep
queue = append(queue, dep.ID)
}
}
// Get issues this one depends on (dependencies) - but only for non-root
// to avoid pulling in unrelated upstream issues
if currentID == root.ID {
// For root, we might want to include direct dependencies too
// Skip for now to keep graph focused on "what this issue encompasses"
}
}
// Load all dependencies within the subgraph
for _, issue := range subgraph.Issues {
deps, err := s.GetDependencyRecords(ctx, issue.ID)
if err != nil {
continue
}
for _, dep := range deps {
// Only include dependencies where both ends are in the subgraph
if _, ok := subgraph.IssueMap[dep.DependsOnID]; ok {
subgraph.Dependencies = append(subgraph.Dependencies, dep)
}
}
}
return subgraph, nil
}
// computeLayout assigns layers to nodes using topological sort