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

Commit

Permalink
graphviz: project-package relation graph
Browse files Browse the repository at this point in the history
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 output(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
  • Loading branch information
darkowlzz committed Feb 10, 2018
1 parent d81b4d0 commit f88b8fd
Show file tree
Hide file tree
Showing 10 changed files with 541 additions and 16 deletions.
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 f88b8fd

Please sign in to comment.