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

Commit b259bf3

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 outputProjectRelationship(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 1284487 commit b259bf3

File tree

6 files changed

+543
-2
lines changed

6 files changed

+543
-2
lines changed

cmd/dep/graphviz.go

+190-2
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 {
@@ -25,8 +29,9 @@ type gvnode struct {
2529

2630
func (g graphviz) New() *graphviz {
2731
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),
3035
}
3136
return ga
3237
}
@@ -108,3 +113,186 @@ func isPathPrefix(path, pre string) bool {
108113

109114
return prflen == pathlen || strings.Index(path[prflen:], "/") == 0
110115
}
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\tnode [shape=box];\n\tcompound=true;\n\tedge [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\tsubgraph cluster_%d {", gsg.index))
192+
g.b.WriteString(fmt.Sprintf("\n\t\tlabel = \"%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

Comments
 (0)