diff --git a/cmd/dep/graphviz.go b/cmd/dep/graphviz.go new file mode 100644 index 0000000000..b422ddde7e --- /dev/null +++ b/cmd/dep/graphviz.go @@ -0,0 +1,110 @@ +// 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 {\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())) + } + + // 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 isPathPrefix(bsc, pr) { + r := fmt.Sprintf("\n\t%d -> %d", g.h[dp.project], hsh) + + if _, ex := rels[r]; !ex { + g.b.WriteString(r + ";") + rels[r] = true + } + + } + } + } + } + + g.b.WriteString("\n}") + return g.b +} + +func (g *graphviz) createNode(project, version string, children []string) { + pr := &gvnode{ + project: project, + version: version, + children: children, + } + + g.h[pr.project] = pr.hash() + 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") +} + +// isPathPrefix ensures that the literal string prefix is a path tree match and +// guards against possibilities like this: +// +// github.com/sdboyer/foo +// github.com/sdboyer/foobar/baz +// +// Verify that prefix is path match and 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 isPathPrefix(path, pre string) bool { + pathlen, prflen := len(path), len(pre) + if pathlen < prflen || path[0:prflen] != pre { + return false + } + + return prflen == pathlen || strings.Index(path[prflen:], "/") == 0 +} diff --git a/cmd/dep/graphviz_test.go b/cmd/dep/graphviz_test.go new file mode 100644 index 0000000000..9dc0edd377 --- /dev/null +++ b/cmd/dep/graphviz_test.go @@ -0,0 +1,75 @@ +// 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 ( + "testing" + + "github.com/golang/dep/test" +) + +func TestEmptyProject(t *testing.T) { + g := new(graphviz).New() + h := test.NewHelper(t) + defer h.Cleanup() + + b := g.output() + want := h.GetTestFileString("graphviz/empty.dot") + + if b.String() != want { + t.Fatalf("expected '%v', got '%v'", want, b.String()) + } +} + +func TestSimpleProject(t *testing.T) { + g := new(graphviz).New() + h := test.NewHelper(t) + defer h.Cleanup() + + g.createNode("project", "", []string{"foo", "bar"}) + g.createNode("foo", "master", []string{"bar"}) + g.createNode("bar", "dev", []string{}) + + b := g.output() + want := h.GetTestFileString("graphviz/case1.dot") + if b.String() != want { + t.Fatalf("expected '%v', got '%v'", want, b.String()) + } +} + +func TestNoLinks(t *testing.T) { + g := new(graphviz).New() + h := test.NewHelper(t) + defer h.Cleanup() + + g.createNode("project", "", []string{}) + + b := g.output() + want := h.GetTestFileString("graphviz/case2.dot") + if b.String() != want { + t.Fatalf("expected '%v', got '%v'", want, b.String()) + } +} + +func TestIsPathPrefix(t *testing.T) { + tcs := []struct { + path string + pre string + want bool + }{ + {"github.com/sdboyer/foo/bar", "github.com/sdboyer/foo", true}, + {"github.com/sdboyer/foobar", "github.com/sdboyer/foo", false}, + {"github.com/sdboyer/bar/foo", "github.com/sdboyer/foo", false}, + {"golang.org/sdboyer/bar/foo", "github.com/sdboyer/foo", false}, + {"golang.org/sdboyer/FOO", "github.com/sdboyer/foo", false}, + } + + for _, tc := range tcs { + r := isPathPrefix(tc.path, tc.pre) + if tc.want != r { + t.Fatalf("expected '%v', got '%v'", tc.want, r) + } + } +} diff --git a/cmd/dep/status.go b/cmd/dep/status.go index 84875e2888..8f3cda27b3 100644 --- a/cmd/dep/status.go +++ b/cmd/dep/status.go @@ -61,6 +61,7 @@ type statusCommand struct { detailed bool json bool template string + output string dot bool old bool missing bool @@ -151,6 +152,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, _ := pkgtree.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 { @@ -172,6 +202,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), @@ -184,6 +220,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 @@ -247,6 +284,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) + } + // Split apart the version from the lock into its constituent parts switch tv := proj.Version().(type) { case gps.UnpairedVersion: diff --git a/cmd/dep/testdata/graphviz/case1.dot b/cmd/dep/testdata/graphviz/case1.dot new file mode 100644 index 0000000000..3de927271d --- /dev/null +++ b/cmd/dep/testdata/graphviz/case1.dot @@ -0,0 +1,9 @@ +digraph { + node [shape=box]; + 4106060478 [label="project"]; + 2851307223 [label="foo\nmaster"]; + 1991736602 [label="bar\ndev"]; + 4106060478 -> 2851307223; + 4106060478 -> 1991736602; + 2851307223 -> 1991736602; +} \ No newline at end of file diff --git a/cmd/dep/testdata/graphviz/case2.dot b/cmd/dep/testdata/graphviz/case2.dot new file mode 100644 index 0000000000..df2d6a4792 --- /dev/null +++ b/cmd/dep/testdata/graphviz/case2.dot @@ -0,0 +1,4 @@ +digraph { + node [shape=box]; + 4106060478 [label="project"]; +} \ No newline at end of file diff --git a/cmd/dep/testdata/graphviz/empty.dot b/cmd/dep/testdata/graphviz/empty.dot new file mode 100644 index 0000000000..3eabc972dc --- /dev/null +++ b/cmd/dep/testdata/graphviz/empty.dot @@ -0,0 +1,3 @@ +digraph { + node [shape=box]; +} \ No newline at end of file