@@ -8,13 +8,17 @@ import (
8
8
"bytes"
9
9
"fmt"
10
10
"hash/fnv"
11
+ "sort"
11
12
"strings"
12
13
)
13
14
14
15
type graphviz struct {
15
16
ps []* gvnode
16
17
b bytes.Buffer
17
18
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
18
22
}
19
23
20
24
type gvnode struct {
@@ -23,22 +27,67 @@ type gvnode struct {
23
27
children []string
24
28
}
25
29
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
+
26
37
func (g graphviz ) New () * graphviz {
27
38
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 ),
30
42
}
31
43
return ga
32
44
}
33
45
34
- func (g graphviz ) output () bytes.Buffer {
35
- g .b .WriteString ("digraph {\n \t node [shape=box];" )
46
+ func (g * graphviz ) output (project string ) bytes.Buffer {
47
+ if project == "" {
48
+ // Project relations graph.
49
+ g .b .WriteString ("digraph {\n \t node [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 \t node [shape=box];\n \t compound=true;\n \t edge [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 \t subgraph cluster_%d {" , gsg .index ))
72
+ g .b .WriteString (fmt .Sprintf ("\n \t \t label = \" %s\" ;" , gsg .project ))
73
+
74
+ nhashes := []string {}
75
+ for _ , pkg := range gsg .packages {
76
+ nhashes = append (nhashes , fmt .Sprint (g .h [pkg ]))
77
+ }
36
78
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 )
40
84
}
41
85
86
+ g .b .WriteString ("\n }\n " )
87
+ return g .b
88
+ }
89
+
90
+ func (g * graphviz ) createProjectRelations () {
42
91
// Store relations to avoid duplication
43
92
rels := make (map [string ]bool )
44
93
@@ -58,9 +107,60 @@ func (g graphviz) output() bytes.Buffer {
58
107
}
59
108
}
60
109
}
110
+ }
61
111
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
+ }
64
164
}
65
165
66
166
func (g * graphviz ) createNode (project , version string , children []string ) {
@@ -108,3 +208,75 @@ func isPathPrefix(path, pre string) bool {
108
208
109
209
return prflen == pathlen || strings .Index (path [prflen :], "/" ) == 0
110
210
}
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