github.com/golang/dep@v0.5.4/cmd/dep/graphviz.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bytes" 9 "fmt" 10 "hash/fnv" 11 "sort" 12 "strings" 13 ) 14 15 type graphviz struct { 16 ps []*gvnode 17 b bytes.Buffer 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 22 } 23 24 type gvnode struct { 25 project string 26 version string 27 children []string 28 } 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 37 func (g graphviz) New() *graphviz { 38 ga := &graphviz{ 39 ps: []*gvnode{}, 40 h: make(map[string]uint32), 41 clusters: make(map[string]*gvsubgraph), 42 } 43 return ga 44 } 45 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 } 78 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) 84 } 85 86 g.b.WriteString("\n}\n") 87 return g.b 88 } 89 90 func (g *graphviz) createProjectRelations() { 91 // Store relations to avoid duplication 92 rels := make(map[string]bool) 93 94 // Create relations 95 for _, dp := range g.ps { 96 for _, bsc := range dp.children { 97 for pr, hsh := range g.h { 98 if isPathPrefix(bsc, pr) { 99 r := fmt.Sprintf("\n\t%d -> %d", g.h[dp.project], hsh) 100 101 if _, ex := rels[r]; !ex { 102 g.b.WriteString(r + ";") 103 rels[r] = true 104 } 105 106 } 107 } 108 } 109 } 110 } 111 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 } 164 } 165 166 func (g *graphviz) createNode(project, version string, children []string) { 167 pr := &gvnode{ 168 project: project, 169 version: version, 170 children: children, 171 } 172 173 g.h[pr.project] = pr.hash() 174 g.ps = append(g.ps, pr) 175 } 176 177 func (dp gvnode) hash() uint32 { 178 h := fnv.New32a() 179 h.Write([]byte(dp.project)) 180 return h.Sum32() 181 } 182 183 func (dp gvnode) label() string { 184 label := []string{dp.project} 185 186 if dp.version != "" { 187 label = append(label, dp.version) 188 } 189 190 return strings.Join(label, "\\n") 191 } 192 193 // isPathPrefix ensures that the literal string prefix is a path tree match and 194 // guards against possibilities like this: 195 // 196 // github.com/sdboyer/foo 197 // github.com/sdboyer/foobar/baz 198 // 199 // Verify that prefix is path match and either the input is the same length as 200 // the match (in which case we know they're equal), or that the next character 201 // is a "/". (Import paths are defined to always use "/", not the OS-specific 202 // path separator.) 203 func isPathPrefix(path, pre string) bool { 204 pathlen, prflen := len(path), len(pre) 205 if pathlen < prflen || path[0:prflen] != pre { 206 return false 207 } 208 209 return prflen == pathlen || strings.Index(path[prflen:], "/") == 0 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 }