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

Commit

Permalink
Merge pull request #1588 from darkowlzz/proj-pkg-rel
Browse files Browse the repository at this point in the history
graphviz: project-package relationship graph
  • Loading branch information
sdboyer authored Jul 20, 2018
2 parents 2354cd9 + b2fba4a commit 22dc1dd
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ IMPROVEMENTS:
* Update our dependency on Masterminds/semver to follow upstream again now that [Masterminds/semver#67](https://github.com/Masterminds/semver/pull/67) is merged([#1792](https://github.com/golang/dep/pull/1792)).
* `inputs-digest` was removed from `Gopkg.lock` ([#1912](https://github.com/golang/dep/pull/1912)).
* Don't exclude `Godeps` folder ([#1822](https://github.com/golang/dep/issues/1822)).
* Add project-package relationship graph support in graphviz ([#1588](https://github.com/golang/dep/pull/1588)).

WIP:
* Enable importing external configuration from dependencies during init (#1277). This is feature flagged and disabled by default.
Expand Down
190 changes: 181 additions & 9 deletions cmd/dep/graphviz.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import (
"bytes"
"fmt"
"hash/fnv"
"sort"
"strings"
)

type graphviz struct {
ps []*gvnode
b bytes.Buffer
h map[string]uint32
// clusters is a map of project name and subgraph object. This can be used
// to refer the subgraph by project name.
clusters map[string]*gvsubgraph
}

type gvnode struct {
Expand All @@ -23,22 +27,67 @@ type gvnode struct {
children []string
}

// Sort gvnode(s).
type byGvnode []gvnode

func (n byGvnode) Len() int { return len(n) }
func (n byGvnode) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byGvnode) Less(i, j int) bool { return n[i].project < n[j].project }

func (g graphviz) New() *graphviz {
ga := &graphviz{
ps: []*gvnode{},
h: make(map[string]uint32),
ps: []*gvnode{},
h: make(map[string]uint32),
clusters: make(map[string]*gvsubgraph),
}
return ga
}

func (g graphviz) output() bytes.Buffer {
g.b.WriteString("digraph {\n\tnode [shape=box];")
func (g *graphviz) output(project string) bytes.Buffer {
if project == "" {
// Project relations graph.
g.b.WriteString("digraph {\n\tnode [shape=box];")

for _, gvp := range g.ps {
// Create node string
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
}

g.createProjectRelations()
} else {
// Project-Package relations graph.
g.b.WriteString("digraph {\n\tnode [shape=box];\n\tcompound=true;\n\tedge [minlen=2];")

// Declare all the nodes with labels.
for _, gvp := range g.ps {
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
}

// Sort the clusters for a consistent output.
clusters := sortClusters(g.clusters)

// Declare all the subgraphs with labels.
for _, gsg := range clusters {
g.b.WriteString(fmt.Sprintf("\n\tsubgraph cluster_%d {", gsg.index))
g.b.WriteString(fmt.Sprintf("\n\t\tlabel = \"%s\";", gsg.project))

nhashes := []string{}
for _, pkg := range gsg.packages {
nhashes = append(nhashes, fmt.Sprint(g.h[pkg]))
}

for _, gvp := range g.ps {
// Create node string
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
g.b.WriteString(fmt.Sprintf("\n\t\t%s;", strings.Join(nhashes, " ")))
g.b.WriteString("\n\t}")
}

g.createProjectPackageRelations(project, clusters)
}

g.b.WriteString("\n}\n")
return g.b
}

func (g *graphviz) createProjectRelations() {
// Store relations to avoid duplication
rels := make(map[string]bool)

Expand All @@ -58,9 +107,60 @@ func (g graphviz) output() bytes.Buffer {
}
}
}
}

g.b.WriteString("\n}")
return g.b
func (g *graphviz) createProjectPackageRelations(project string, clusters []*gvsubgraph) {
// This function takes a child package/project, target project, subgraph meta, from
// and to of the edge and write a relation.
linkRelation := func(child, project string, meta []string, from, to uint32) {
if child == project {
// Check if it's a cluster.
target, ok := g.clusters[project]
if ok {
// It's a cluster. Point to the Project Root. Use lhead.
meta = append(meta, fmt.Sprintf("lhead=cluster_%d", target.index))
// When the head points to a cluster root, use the first
// node in the cluster as to.
to = g.h[target.packages[0]]
}
}

if len(meta) > 0 {
g.b.WriteString(fmt.Sprintf("\n\t%d -> %d [%s];", from, to, strings.Join(meta, " ")))
} else {
g.b.WriteString(fmt.Sprintf("\n\t%d -> %d;", from, to))
}
}

// Create relations from nodes.
for _, node := range g.ps {
for _, child := range node.children {
// Only if it points to the target project, proceed further.
if isPathPrefix(child, project) {
meta := []string{}
from := g.h[node.project]
to := g.h[child]

linkRelation(child, project, meta, from, to)
}
}
}

// Create relations from clusters.
for _, cluster := range clusters {
for _, child := range cluster.children {
// Only if it points to the target project, proceed further.
if isPathPrefix(child, project) {
meta := []string{fmt.Sprintf("ltail=cluster_%d", cluster.index)}
// When the tail is from a cluster, use the first node in the
// cluster as from.
from := g.h[cluster.packages[0]]
to := g.h[child]

linkRelation(child, project, meta, from, to)
}
}
}
}

func (g *graphviz) createNode(project, version string, children []string) {
Expand Down Expand Up @@ -108,3 +208,75 @@ func isPathPrefix(path, pre string) bool {

return prflen == pathlen || strings.Index(path[prflen:], "/") == 0
}

// gvsubgraph is a graphviz subgraph with at least one node(package) in it.
type gvsubgraph struct {
project string // Project root name of a project.
packages []string // List of subpackages in the project.
index int // Index of the subgraph cluster. This is used to refer the subgraph in the dot file.
children []string // Dependencies of the project root package.
}

func (sg gvsubgraph) hash() uint32 {
h := fnv.New32a()
h.Write([]byte(sg.project))
return h.Sum32()
}

// createSubgraph creates a graphviz subgraph with nodes in it. This should only
// be created when a project has more than one package. A single package project
// should be just a single node.
// First nodes are created using the provided packages and their imports. Then
// a subgraph is created with all the nodes in it.
func (g *graphviz) createSubgraph(project string, packages map[string][]string) {
// If there's only a single package and that's the project root, do not
// create a subgraph. Just create a node.
if children, ok := packages[project]; ok && len(packages) == 1 {
g.createNode(project, "", children)
return
}

// Sort and use the packages for consistent output.
pkgs := []gvnode{}

for name, children := range packages {
pkgs = append(pkgs, gvnode{project: name, children: children})
}

sort.Sort(byGvnode(pkgs))

subgraphPkgs := []string{}
rootChildren := []string{}
for _, p := range pkgs {
if p.project == project {
// Do not create a separate node for the root package.
rootChildren = append(rootChildren, p.children...)
continue
}
g.createNode(p.project, "", p.children)
subgraphPkgs = append(subgraphPkgs, p.project)
}

sg := &gvsubgraph{
project: project,
packages: subgraphPkgs,
index: len(g.clusters),
children: rootChildren,
}

g.h[project] = sg.hash()
g.clusters[project] = sg
}

// sortCluster takes a map of all the clusters and returns a list of cluster
// names sorted by the cluster index.
func sortClusters(clusters map[string]*gvsubgraph) []*gvsubgraph {
result := []*gvsubgraph{}
for _, cluster := range clusters {
result = append(result, cluster)
}
sort.Slice(result, func(i, j int) bool {
return result[i].index < result[j].index
})
return result
}
Loading

0 comments on commit 22dc1dd

Please sign in to comment.