-
Notifications
You must be signed in to change notification settings - Fork 1k
Dep status tree visualisation dot output #271
Changes from 10 commits
a7c6fa8
1aa5000
a70afb5
80d8fc2
08e23ec
408c3aa
3ff1f50
5d3174f
20fcfef
5427779
70d8108
1a64b30
b519dfa
7fa0203
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment should go away when |
||
// of the prefix, so we just check len | ||
return prflen == pathlen || strings.Index(path[prflen:], "/") == 0 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,6 +60,7 @@ type statusCommand struct { | |
detailed bool | ||
json bool | ||
template string | ||
output string | ||
dot bool | ||
old bool | ||
missing bool | ||
|
@@ -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 { | ||
|
@@ -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), | ||
|
@@ -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 | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's some filtering that needs to be done here. Simply calling The lock should contain the imported package list for each project, though, so this shouldn't be that bad to do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sdboyer we are doing some sort of "filtering". |
||
} | ||
|
||
// Split apart the version from the lock into its constituent parts | ||
switch tv := proj.Version().(type) { | ||
case gps.UnpairedVersion: | ||
|
There was a problem hiding this comment.
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 insideisPathPrefixOrEqual
, rather than requiring two calls out here.