@@ -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 {
@@ -25,8 +29,9 @@ type gvnode struct {
25
29
26
30
func (g graphviz ) New () * graphviz {
27
31
ga := & graphviz {
28
- ps : []* gvnode {},
29
- h : make (map [string ]uint32 ),
32
+ ps : []* gvnode {},
33
+ h : make (map [string ]uint32 ),
34
+ clusters : make (map [string ]* gvsubgraph ),
30
35
}
31
36
return ga
32
37
}
@@ -108,3 +113,186 @@ func isPathPrefix(path, pre string) bool {
108
113
109
114
return prflen == pathlen || strings .Index (path [prflen :], "/" ) == 0
110
115
}
116
+
117
+ // gvsubgraph is a graphviz subgraph with at least one node(package) in it.
118
+ type gvsubgraph struct {
119
+ project string // Project root name of a project.
120
+ packages []string // List of subpackages in the project.
121
+ index int // Index of the subgraph cluster. This is used to refer the subgraph in the dot file.
122
+ children []string // Dependencies of the project root package.
123
+ }
124
+
125
+ func (sg gvsubgraph ) hash () uint32 {
126
+ h := fnv .New32a ()
127
+ h .Write ([]byte (sg .project ))
128
+ return h .Sum32 ()
129
+ }
130
+
131
+ // createSubgraph creates a graphviz subgraph with nodes in it. This should only
132
+ // be created when a project has more than one package. A single package project
133
+ // should be just a single node.
134
+ // First nodes are created using the provided packages and their imports. Then
135
+ // a subgraph is created with all the nodes in it.
136
+ func (g * graphviz ) createSubgraph (project string , packages map [string ][]string ) {
137
+ // If there's only a single package and that's the project root, do not
138
+ // create a subgraph. Just create a node.
139
+ if children , ok := packages [project ]; ok && len (packages ) == 1 {
140
+ g .createNode (project , "" , children )
141
+ return
142
+ }
143
+
144
+ // Sort and use the packages for consistent output.
145
+ pkgs := []pkg {}
146
+
147
+ for name , children := range packages {
148
+ pkgs = append (pkgs , pkg {name , children })
149
+ }
150
+
151
+ sort .Sort (byPkg (pkgs ))
152
+
153
+ subgraphPkgs := []string {}
154
+ rootChildren := []string {}
155
+ for _ , p := range pkgs {
156
+ if p .name == project {
157
+ // Do not create a separate node for the root package.
158
+ rootChildren = append (rootChildren , p .children ... )
159
+ continue
160
+ }
161
+ g .createNode (p .name , "" , p .children )
162
+ subgraphPkgs = append (subgraphPkgs , p .name )
163
+ }
164
+
165
+ sg := & gvsubgraph {
166
+ project : project ,
167
+ packages : subgraphPkgs ,
168
+ index : len (g .clusters ),
169
+ children : rootChildren ,
170
+ }
171
+
172
+ g .h [project ] = sg .hash ()
173
+ g .clusters [project ] = sg
174
+ }
175
+
176
+ // outputProjectRelationship takes a project name and generates dot output
177
+ // showing relationship of other project with the given target project.
178
+ func (g graphviz ) outputProjectRelationship (project string ) bytes.Buffer {
179
+ g .b .WriteString ("digraph {\n \t node [shape=box];\n \t compound=true;\n \t edge [minlen=2];" )
180
+
181
+ // Declare all the nodes with labels.
182
+ for _ , gvp := range g .ps {
183
+ g .b .WriteString (fmt .Sprintf ("\n \t %d [label=\" %s\" ];" , gvp .hash (), gvp .label ()))
184
+ }
185
+
186
+ // Sort the clusters for a consistent output.
187
+ clusters := sortClusters (g .clusters )
188
+
189
+ // Declare all the subgraphs with labels.
190
+ for _ , gsg := range clusters {
191
+ g .b .WriteString (fmt .Sprintf ("\n \t subgraph cluster_%d {" , gsg .index ))
192
+ g .b .WriteString (fmt .Sprintf ("\n \t \t label = \" %s\" ;" , gsg .project ))
193
+
194
+ nodes := ""
195
+ for i , pkg := range gsg .packages {
196
+ nodes += fmt .Sprint (g .h [pkg ])
197
+ if i != len (gsg .packages )- 1 {
198
+ // Space between the node names.
199
+ nodes += " "
200
+ }
201
+ }
202
+ g .b .WriteString (fmt .Sprintf ("\n \t \t %s;" , nodes ))
203
+ g .b .WriteString ("\n \t }" )
204
+ }
205
+
206
+ // Create relations from nodes.
207
+ for _ , node := range g .ps {
208
+ for _ , child := range node .children {
209
+ // Only if it points to the target project, proceed further.
210
+ if isPathPrefix (child , project ) {
211
+ meta := []string {}
212
+ from := g .h [node .project ]
213
+ to := g .h [child ]
214
+
215
+ if child == project {
216
+ // Check if it's a cluster.
217
+ target , ok := g .clusters [project ]
218
+ if ok {
219
+ // It's a cluster. Point to the Project Root. Use lhead.
220
+ meta = append (meta , fmt .Sprintf ("lhead=cluster_%d" , target .index ))
221
+ // When the head points to a cluster root, use the first
222
+ // node in the cluster as to.
223
+ to = g .h [target .packages [0 ]]
224
+ }
225
+ // Else, use the node.
226
+ }
227
+
228
+ if len (meta ) > 0 {
229
+ g .b .WriteString (fmt .Sprintf ("\n \t %d -> %d [%s];" , from , to , strings .Join (meta , " " )))
230
+ } else {
231
+ g .b .WriteString (fmt .Sprintf ("\n \t %d -> %d;" , from , to ))
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ // Create relations from clusters.
238
+ for _ , cluster := range clusters {
239
+ for _ , child := range cluster .children {
240
+ if isPathPrefix (child , project ) {
241
+ meta := []string {}
242
+ // When the tail is from a cluster, use the first node in the
243
+ // cluster as from.
244
+ from := g .h [cluster .packages [0 ]]
245
+ to := g .h [child ]
246
+ meta = append (meta , fmt .Sprintf ("ltail=cluster_%d" , cluster .index ))
247
+
248
+ if child == project {
249
+ target , ok := g .clusters [project ]
250
+ if ok {
251
+ // It's a cluster. Point to the Project Root. Use lhead.
252
+ meta = append (meta , fmt .Sprintf ("lhead=cluster_%d" , target .index ))
253
+ // When the head points to a cluster root, use the first
254
+ // node in the cluster as to.
255
+ to = g .h [target .packages [0 ]]
256
+ }
257
+ // Else, use the node.
258
+ }
259
+
260
+ if len (meta ) > 0 {
261
+ g .b .WriteString (fmt .Sprintf ("\n \t %d -> %d [%s];" , from , to , strings .Join (meta , " " )))
262
+ } else {
263
+ g .b .WriteString (fmt .Sprintf ("\n \t %d -> %d;" , from , to ))
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ g .b .WriteString ("\n }\n " )
270
+ return g .b
271
+ }
272
+
273
+ // pkg is a simplified package type. It exists to keep graphviz independent of
274
+ // other components.
275
+ type pkg struct {
276
+ name string
277
+ children []string
278
+ }
279
+
280
+ // Sort packages.
281
+ type byPkg []pkg
282
+
283
+ func (p byPkg ) Len () int { return len (p ) }
284
+ func (p byPkg ) Swap (i , j int ) { p [i ], p [j ] = p [j ], p [i ] }
285
+ func (p byPkg ) Less (i , j int ) bool { return p [i ].name < p [j ].name }
286
+
287
+ // sortCluster takes a map of all the clusters and returns a list of cluster
288
+ // names sorted by the cluster index.
289
+ func sortClusters (clusters map [string ]* gvsubgraph ) []* gvsubgraph {
290
+ result := []* gvsubgraph {}
291
+ for _ , cluster := range clusters {
292
+ result = append (result , cluster )
293
+ }
294
+ sort .Slice (result , func (i , j int ) bool {
295
+ return result [i ].index < result [j ].index
296
+ })
297
+ return result
298
+ }
0 commit comments