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

Dep status tree visualisation dot output #271

Merged
merged 14 commits into from
Apr 13, 2017
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions cmd/dep/graphviz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
"bytes"
"fmt"
"hash/fnv"
"strings"
)

type graphviz struct {
ps []*gvnode
b bytes.Buffer
h map[string]uint32
}

type gvnode struct {
project string
version string
children []string
}

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

func (g graphviz) output() bytes.Buffer {
g.b.WriteString("digraph { node [shape=box]; ")

for _, gvp := range g.ps {
g.h[gvp.project] = gvp.hash()

// Create node string
g.b.WriteString(fmt.Sprintf("%d [label=\"%s\"];", gvp.hash(), gvp.label()))
}

// Store relations to avoid duplication
rels := make(map[string]bool)

// Create relations
for _, dp := range g.ps {
for _, bsc := range dp.children {
for pr, hsh := range g.h {
if strings.HasPrefix(bsc, pr) && isPathPrefixOrEqual(pr, bsc) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was imagining this as a standalone implementation - move the strings.HasPrefix() call inside isPathPrefixOrEqual, rather than requiring two calls out here.

r := fmt.Sprintf("%d -> %d", g.h[dp.project], hsh)

if _, ex := rels[r]; !ex {
g.b.WriteString(r + "; ")
rels[r] = true
}

}
}
}
}

g.b.WriteString("}")

return g.b
}

func (g *graphviz) createNode(p, v string, c []string) {
pr := &gvnode{
project: p,
version: v,
children: c,
}

g.ps = append(g.ps, pr)
}

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

func (dp gvnode) label() string {
label := []string{dp.project}

if dp.version != "" {
label = append(label, dp.version)
}

return strings.Join(label, "\n")
}

// Ensure that the literal string prefix is a path tree match and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this isn't exported, it should follow the Go standard of having the comment block begin with the func name.

// guard against possibilities like this:
//
// github.com/sdboyer/foo
// github.com/sdboyer/foobar/baz
//
// Verify that either the input is the same length as the match (in which
// case we know they're equal), or that the next character is a "/". (Import
// paths are defined to always use "/", not the OS-specific path separator.)
func isPathPrefixOrEqual(pre, path string) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I'm looking at this here, I realize the change in parameter order is terrible. Let's flip them so that it's the same as strings.HasPrefix() - string first, prefix second.

prflen, pathlen := len(pre), len(path)
if pathlen == prflen+1 {
// this can never be the case
return false
}

// we assume something else (a trie) has done equality check up to the point
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should go away when strings.HasPrefix() moves into the func.

// of the prefix, so we just check len
return prflen == pathlen || strings.Index(path[prflen:], "/") == 0
}
51 changes: 51 additions & 0 deletions cmd/dep/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type statusCommand struct {
detailed bool
json bool
template string
output string
dot bool
old bool
missing bool
Expand Down Expand Up @@ -150,6 +151,35 @@ func (out *jsonOutput) MissingFooter() {
json.NewEncoder(out.w).Encode(out.missing)
}

type dotOutput struct {
w io.Writer
o string
g *graphviz
p *dep.Project
}

func (out *dotOutput) BasicHeader() {
out.g = new(graphviz).New()

ptree, _ := gps.ListPackages(out.p.AbsRoot, string(out.p.ImportRoot))
prm, _ := ptree.ToReachMap(true, false, false, nil)

out.g.createNode(string(out.p.ImportRoot), "", prm.Flatten(false))
}

func (out *dotOutput) BasicFooter() {
gvo := out.g.output()
fmt.Fprintf(out.w, gvo.String())
}

func (out *dotOutput) BasicLine(bs *BasicStatus) {
out.g.createNode(bs.ProjectRoot, bs.Version.String(), bs.Children)
}

func (out *dotOutput) MissingHeader() {}
func (out *dotOutput) MissingLine(ms *MissingStatus) {}
func (out *dotOutput) MissingFooter() {}

func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
p, err := ctx.LoadProject("")
if err != nil {
Expand All @@ -171,6 +201,12 @@ func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
out = &jsonOutput{
w: os.Stdout,
}
case cmd.dot:
out = &dotOutput{
p: p,
o: cmd.output,
w: os.Stdout,
}
default:
out = &tableOutput{
w: tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0),
Expand All @@ -183,6 +219,7 @@ func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
// in the summary/list status output mode.
type BasicStatus struct {
ProjectRoot string
Children []string
Constraint gps.Constraint
Version gps.UnpairedVersion
Revision gps.Revision
Expand Down Expand Up @@ -246,6 +283,20 @@ func runStatusAll(out outputter, p *dep.Project, sm *gps.SourceMgr) error {
PackageCount: len(proj.Packages()),
}

// Get children only for specific outputers
// in order to avoid slower status process
switch out.(type) {
case *dotOutput:
ptr, err := sm.ListPackages(proj.Ident(), proj.Version())

if err != nil {
return fmt.Errorf("analysis of %s package failed: %v", proj.Ident().ProjectRoot, err)
}

prm, _ := ptr.ToReachMap(true, false, false, nil)
bs.Children = prm.Flatten(false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some filtering that needs to be done here. Simply calling Flatten will get you the imports of ALL the packages in a ptree. We need only the subset of imports that come from the packages we're actually importing from a project.

The lock should contain the imported package list for each project, though, so this shouldn't be that bad to do.

Copy link
Contributor Author

@Rhymond Rhymond Mar 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdboyer we are doing some sort of "filtering".
https://github.com/golang/dep/pull/271/files#diff-0d223b461ecb49bf384a2c75a3893558R51
When we are generating Graphviz nodes we are only working with packages which are presented in the lock in this way we're excluding other packages.

}

// Split apart the version from the lock into its constituent parts
switch tv := proj.Version().(type) {
case gps.UnpairedVersion:
Expand Down