Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit f88b8fd

Browse files
committed
graphviz: project-package relation graph
This change introduces "clusters", which are projects with multiple subpackages. Clusters are created using "subgraph" in dot syntax. To create a project-package relationship graph, nodes and subgraphs are created first. Nodes are created when a project has a single package and that's the root package. A subgraph/cluster is created when a project has multiple subpackages. createSubgraph(project, packages) takes a project name and its packages and creates nodes or subgraphs/clusters based on the packages. Once all the nodes and subgraphs are created, a target project can be passed to output(project) to generate a dot output with all the nodes and subgraphs related to the target project. Following relation scenarios have been covered: 1. edge from a node within a cluster to a target cluster 2. edge from a node within a cluster to a single node 3. edge from a cluster to a target cluster 4. edge from a cluster to a target single node 5. edge from a cluster to a node within a cluster
1 parent d81b4d0 commit f88b8fd

File tree

10 files changed

+541
-16
lines changed

10 files changed

+541
-16
lines changed

cmd/dep/graphviz.go

Lines changed: 181 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ import (
88
"bytes"
99
"fmt"
1010
"hash/fnv"
11+
"sort"
1112
"strings"
1213
)
1314

1415
type graphviz struct {
1516
ps []*gvnode
1617
b bytes.Buffer
1718
h map[string]uint32
19+
// clusters is a map of project name and subgraph object. This can be used
20+
// to refer the subgraph by project name.
21+
clusters map[string]*gvsubgraph
1822
}
1923

2024
type gvnode struct {
@@ -23,22 +27,67 @@ type gvnode struct {
2327
children []string
2428
}
2529

30+
// Sort gvnode(s).
31+
type byGvnode []gvnode
32+
33+
func (n byGvnode) Len() int { return len(n) }
34+
func (n byGvnode) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
35+
func (n byGvnode) Less(i, j int) bool { return n[i].project < n[j].project }
36+
2637
func (g graphviz) New() *graphviz {
2738
ga := &graphviz{
28-
ps: []*gvnode{},
29-
h: make(map[string]uint32),
39+
ps: []*gvnode{},
40+
h: make(map[string]uint32),
41+
clusters: make(map[string]*gvsubgraph),
3042
}
3143
return ga
3244
}
3345

34-
func (g graphviz) output() bytes.Buffer {
35-
g.b.WriteString("digraph {\n\tnode [shape=box];")
46+
func (g *graphviz) output(project string) bytes.Buffer {
47+
if project == "" {
48+
// Project relations graph.
49+
g.b.WriteString("digraph {\n\tnode [shape=box];")
50+
51+
for _, gvp := range g.ps {
52+
// Create node string
53+
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
54+
}
55+
56+
g.createProjectRelations()
57+
} else {
58+
// Project-Package relations graph.
59+
g.b.WriteString("digraph {\n\tnode [shape=box];\n\tcompound=true;\n\tedge [minlen=2];")
60+
61+
// Declare all the nodes with labels.
62+
for _, gvp := range g.ps {
63+
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
64+
}
65+
66+
// Sort the clusters for a consistent output.
67+
clusters := sortClusters(g.clusters)
68+
69+
// Declare all the subgraphs with labels.
70+
for _, gsg := range clusters {
71+
g.b.WriteString(fmt.Sprintf("\n\tsubgraph cluster_%d {", gsg.index))
72+
g.b.WriteString(fmt.Sprintf("\n\t\tlabel = \"%s\";", gsg.project))
73+
74+
nhashes := []string{}
75+
for _, pkg := range gsg.packages {
76+
nhashes = append(nhashes, fmt.Sprint(g.h[pkg]))
77+
}
3678

37-
for _, gvp := range g.ps {
38-
// Create node string
39-
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
79+
g.b.WriteString(fmt.Sprintf("\n\t\t%s;", strings.Join(nhashes, " ")))
80+
g.b.WriteString("\n\t}")
81+
}
82+
83+
g.createProjectPackageRelations(project, clusters)
4084
}
4185

86+
g.b.WriteString("\n}\n")
87+
return g.b
88+
}
89+
90+
func (g *graphviz) createProjectRelations() {
4291
// Store relations to avoid duplication
4392
rels := make(map[string]bool)
4493

@@ -58,9 +107,60 @@ func (g graphviz) output() bytes.Buffer {
58107
}
59108
}
60109
}
110+
}
61111

62-
g.b.WriteString("\n}")
63-
return g.b
112+
func (g *graphviz) createProjectPackageRelations(project string, clusters []*gvsubgraph) {
113+
// This function takes a child package/project, target project, subgraph meta, from
114+
// and to of the edge and write a relation.
115+
linkRelation := func(child, project string, meta []string, from, to uint32) {
116+
if child == project {
117+
// Check if it's a cluster.
118+
target, ok := g.clusters[project]
119+
if ok {
120+
// It's a cluster. Point to the Project Root. Use lhead.
121+
meta = append(meta, fmt.Sprintf("lhead=cluster_%d", target.index))
122+
// When the head points to a cluster root, use the first
123+
// node in the cluster as to.
124+
to = g.h[target.packages[0]]
125+
}
126+
}
127+
128+
if len(meta) > 0 {
129+
g.b.WriteString(fmt.Sprintf("\n\t%d -> %d [%s];", from, to, strings.Join(meta, " ")))
130+
} else {
131+
g.b.WriteString(fmt.Sprintf("\n\t%d -> %d;", from, to))
132+
}
133+
}
134+
135+
// Create relations from nodes.
136+
for _, node := range g.ps {
137+
for _, child := range node.children {
138+
// Only if it points to the target project, proceed further.
139+
if isPathPrefix(child, project) {
140+
meta := []string{}
141+
from := g.h[node.project]
142+
to := g.h[child]
143+
144+
linkRelation(child, project, meta, from, to)
145+
}
146+
}
147+
}
148+
149+
// Create relations from clusters.
150+
for _, cluster := range clusters {
151+
for _, child := range cluster.children {
152+
// Only if it points to the target project, proceed further.
153+
if isPathPrefix(child, project) {
154+
meta := []string{fmt.Sprintf("ltail=cluster_%d", cluster.index)}
155+
// When the tail is from a cluster, use the first node in the
156+
// cluster as from.
157+
from := g.h[cluster.packages[0]]
158+
to := g.h[child]
159+
160+
linkRelation(child, project, meta, from, to)
161+
}
162+
}
163+
}
64164
}
65165

66166
func (g *graphviz) createNode(project, version string, children []string) {
@@ -108,3 +208,75 @@ func isPathPrefix(path, pre string) bool {
108208

109209
return prflen == pathlen || strings.Index(path[prflen:], "/") == 0
110210
}
211+
212+
// gvsubgraph is a graphviz subgraph with at least one node(package) in it.
213+
type gvsubgraph struct {
214+
project string // Project root name of a project.
215+
packages []string // List of subpackages in the project.
216+
index int // Index of the subgraph cluster. This is used to refer the subgraph in the dot file.
217+
children []string // Dependencies of the project root package.
218+
}
219+
220+
func (sg gvsubgraph) hash() uint32 {
221+
h := fnv.New32a()
222+
h.Write([]byte(sg.project))
223+
return h.Sum32()
224+
}
225+
226+
// createSubgraph creates a graphviz subgraph with nodes in it. This should only
227+
// be created when a project has more than one package. A single package project
228+
// should be just a single node.
229+
// First nodes are created using the provided packages and their imports. Then
230+
// a subgraph is created with all the nodes in it.
231+
func (g *graphviz) createSubgraph(project string, packages map[string][]string) {
232+
// If there's only a single package and that's the project root, do not
233+
// create a subgraph. Just create a node.
234+
if children, ok := packages[project]; ok && len(packages) == 1 {
235+
g.createNode(project, "", children)
236+
return
237+
}
238+
239+
// Sort and use the packages for consistent output.
240+
pkgs := []gvnode{}
241+
242+
for name, children := range packages {
243+
pkgs = append(pkgs, gvnode{project: name, children: children})
244+
}
245+
246+
sort.Sort(byGvnode(pkgs))
247+
248+
subgraphPkgs := []string{}
249+
rootChildren := []string{}
250+
for _, p := range pkgs {
251+
if p.project == project {
252+
// Do not create a separate node for the root package.
253+
rootChildren = append(rootChildren, p.children...)
254+
continue
255+
}
256+
g.createNode(p.project, "", p.children)
257+
subgraphPkgs = append(subgraphPkgs, p.project)
258+
}
259+
260+
sg := &gvsubgraph{
261+
project: project,
262+
packages: subgraphPkgs,
263+
index: len(g.clusters),
264+
children: rootChildren,
265+
}
266+
267+
g.h[project] = sg.hash()
268+
g.clusters[project] = sg
269+
}
270+
271+
// sortCluster takes a map of all the clusters and returns a list of cluster
272+
// names sorted by the cluster index.
273+
func sortClusters(clusters map[string]*gvsubgraph) []*gvsubgraph {
274+
result := []*gvsubgraph{}
275+
for _, cluster := range clusters {
276+
result = append(result, cluster)
277+
}
278+
sort.Slice(result, func(i, j int) bool {
279+
return result[i].index < result[j].index
280+
})
281+
return result
282+
}

0 commit comments

Comments
 (0)