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  }