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

Commit e4f1f3e

Browse files
authored
Merge pull request #271 from Rhymond/dot_output
Dep status tree visualisation dot output
2 parents 864d348 + 7fa0203 commit e4f1f3e

File tree

6 files changed

+252
-0
lines changed

6 files changed

+252
-0
lines changed

cmd/dep/graphviz.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
"strings"
12+
)
13+
14+
type graphviz struct {
15+
ps []*gvnode
16+
b bytes.Buffer
17+
h map[string]uint32
18+
}
19+
20+
type gvnode struct {
21+
project string
22+
version string
23+
children []string
24+
}
25+
26+
func (g graphviz) New() *graphviz {
27+
ga := &graphviz{
28+
ps: []*gvnode{},
29+
h: make(map[string]uint32),
30+
}
31+
return ga
32+
}
33+
34+
func (g graphviz) output() bytes.Buffer {
35+
g.b.WriteString("digraph {\n\tnode [shape=box];")
36+
37+
for _, gvp := range g.ps {
38+
// Create node string
39+
g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label()))
40+
}
41+
42+
// Store relations to avoid duplication
43+
rels := make(map[string]bool)
44+
45+
// Create relations
46+
for _, dp := range g.ps {
47+
for _, bsc := range dp.children {
48+
for pr, hsh := range g.h {
49+
if isPathPrefix(bsc, pr) {
50+
r := fmt.Sprintf("\n\t%d -> %d", g.h[dp.project], hsh)
51+
52+
if _, ex := rels[r]; !ex {
53+
g.b.WriteString(r + ";")
54+
rels[r] = true
55+
}
56+
57+
}
58+
}
59+
}
60+
}
61+
62+
g.b.WriteString("\n}")
63+
return g.b
64+
}
65+
66+
func (g *graphviz) createNode(project, version string, children []string) {
67+
pr := &gvnode{
68+
project: project,
69+
version: version,
70+
children: children,
71+
}
72+
73+
g.h[pr.project] = pr.hash()
74+
g.ps = append(g.ps, pr)
75+
}
76+
77+
func (dp gvnode) hash() uint32 {
78+
h := fnv.New32a()
79+
h.Write([]byte(dp.project))
80+
return h.Sum32()
81+
}
82+
83+
func (dp gvnode) label() string {
84+
label := []string{dp.project}
85+
86+
if dp.version != "" {
87+
label = append(label, dp.version)
88+
}
89+
90+
return strings.Join(label, "\\n")
91+
}
92+
93+
// isPathPrefix ensures that the literal string prefix is a path tree match and
94+
// guards against possibilities like this:
95+
//
96+
// github.com/sdboyer/foo
97+
// github.com/sdboyer/foobar/baz
98+
//
99+
// Verify that prefix is path match and either the input is the same length as
100+
// the match (in which case we know they're equal), or that the next character
101+
// is a "/". (Import paths are defined to always use "/", not the OS-specific
102+
// path separator.)
103+
func isPathPrefix(path, pre string) bool {
104+
pathlen, prflen := len(path), len(pre)
105+
if pathlen < prflen || path[0:prflen] != pre {
106+
return false
107+
}
108+
109+
return prflen == pathlen || strings.Index(path[prflen:], "/") == 0
110+
}

cmd/dep/graphviz_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
"testing"
9+
10+
"github.com/golang/dep/test"
11+
)
12+
13+
func TestEmptyProject(t *testing.T) {
14+
g := new(graphviz).New()
15+
h := test.NewHelper(t)
16+
defer h.Cleanup()
17+
18+
b := g.output()
19+
want := h.GetTestFileString("graphviz/empty.dot")
20+
21+
if b.String() != want {
22+
t.Fatalf("expected '%v', got '%v'", want, b.String())
23+
}
24+
}
25+
26+
func TestSimpleProject(t *testing.T) {
27+
g := new(graphviz).New()
28+
h := test.NewHelper(t)
29+
defer h.Cleanup()
30+
31+
g.createNode("project", "", []string{"foo", "bar"})
32+
g.createNode("foo", "master", []string{"bar"})
33+
g.createNode("bar", "dev", []string{})
34+
35+
b := g.output()
36+
want := h.GetTestFileString("graphviz/case1.dot")
37+
if b.String() != want {
38+
t.Fatalf("expected '%v', got '%v'", want, b.String())
39+
}
40+
}
41+
42+
func TestNoLinks(t *testing.T) {
43+
g := new(graphviz).New()
44+
h := test.NewHelper(t)
45+
defer h.Cleanup()
46+
47+
g.createNode("project", "", []string{})
48+
49+
b := g.output()
50+
want := h.GetTestFileString("graphviz/case2.dot")
51+
if b.String() != want {
52+
t.Fatalf("expected '%v', got '%v'", want, b.String())
53+
}
54+
}
55+
56+
func TestIsPathPrefix(t *testing.T) {
57+
tcs := []struct {
58+
path string
59+
pre string
60+
want bool
61+
}{
62+
{"github.com/sdboyer/foo/bar", "github.com/sdboyer/foo", true},
63+
{"github.com/sdboyer/foobar", "github.com/sdboyer/foo", false},
64+
{"github.com/sdboyer/bar/foo", "github.com/sdboyer/foo", false},
65+
{"golang.org/sdboyer/bar/foo", "github.com/sdboyer/foo", false},
66+
{"golang.org/sdboyer/FOO", "github.com/sdboyer/foo", false},
67+
}
68+
69+
for _, tc := range tcs {
70+
r := isPathPrefix(tc.path, tc.pre)
71+
if tc.want != r {
72+
t.Fatalf("expected '%v', got '%v'", tc.want, r)
73+
}
74+
}
75+
}

cmd/dep/status.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type statusCommand struct {
6262
detailed bool
6363
json bool
6464
template string
65+
output string
6566
dot bool
6667
old bool
6768
missing bool
@@ -152,6 +153,35 @@ func (out *jsonOutput) MissingFooter() {
152153
json.NewEncoder(out.w).Encode(out.missing)
153154
}
154155

156+
type dotOutput struct {
157+
w io.Writer
158+
o string
159+
g *graphviz
160+
p *dep.Project
161+
}
162+
163+
func (out *dotOutput) BasicHeader() {
164+
out.g = new(graphviz).New()
165+
166+
ptree, _ := pkgtree.ListPackages(out.p.AbsRoot, string(out.p.ImportRoot))
167+
prm, _ := ptree.ToReachMap(true, false, false, nil)
168+
169+
out.g.createNode(string(out.p.ImportRoot), "", prm.Flatten(false))
170+
}
171+
172+
func (out *dotOutput) BasicFooter() {
173+
gvo := out.g.output()
174+
fmt.Fprintf(out.w, gvo.String())
175+
}
176+
177+
func (out *dotOutput) BasicLine(bs *BasicStatus) {
178+
out.g.createNode(bs.ProjectRoot, bs.Version.String(), bs.Children)
179+
}
180+
181+
func (out *dotOutput) MissingHeader() {}
182+
func (out *dotOutput) MissingLine(ms *MissingStatus) {}
183+
func (out *dotOutput) MissingFooter() {}
184+
155185
func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
156186
p, err := ctx.LoadProject("")
157187
if err != nil {
@@ -173,6 +203,12 @@ func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
173203
out = &jsonOutput{
174204
w: os.Stdout,
175205
}
206+
case cmd.dot:
207+
out = &dotOutput{
208+
p: p,
209+
o: cmd.output,
210+
w: os.Stdout,
211+
}
176212
default:
177213
out = &tableOutput{
178214
w: tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0),
@@ -185,6 +221,7 @@ func (cmd *statusCommand) Run(ctx *dep.Ctx, args []string) error {
185221
// in the summary/list status output mode.
186222
type BasicStatus struct {
187223
ProjectRoot string
224+
Children []string
188225
Constraint gps.Constraint
189226
Version gps.UnpairedVersion
190227
Revision gps.Revision
@@ -248,6 +285,20 @@ func runStatusAll(out outputter, p *dep.Project, sm *gps.SourceMgr) error {
248285
PackageCount: len(proj.Packages()),
249286
}
250287

288+
// Get children only for specific outputers
289+
// in order to avoid slower status process
290+
switch out.(type) {
291+
case *dotOutput:
292+
ptr, err := sm.ListPackages(proj.Ident(), proj.Version())
293+
294+
if err != nil {
295+
return fmt.Errorf("analysis of %s package failed: %v", proj.Ident().ProjectRoot, err)
296+
}
297+
298+
prm, _ := ptr.ToReachMap(true, false, false, nil)
299+
bs.Children = prm.Flatten(false)
300+
}
301+
251302
// Split apart the version from the lock into its constituent parts
252303
switch tv := proj.Version().(type) {
253304
case gps.UnpairedVersion:

cmd/dep/testdata/graphviz/case1.dot

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
digraph {
2+
node [shape=box];
3+
4106060478 [label="project"];
4+
2851307223 [label="foo\nmaster"];
5+
1991736602 [label="bar\ndev"];
6+
4106060478 -> 2851307223;
7+
4106060478 -> 1991736602;
8+
2851307223 -> 1991736602;
9+
}

cmd/dep/testdata/graphviz/case2.dot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
digraph {
2+
node [shape=box];
3+
4106060478 [label="project"];
4+
}

cmd/dep/testdata/graphviz/empty.dot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
digraph {
2+
node [shape=box];
3+
}

0 commit comments

Comments
 (0)