modgraphviz: colour nodes based on whether they were picked by MVS or not
Green nodes are those picked by MVS, whilst gray nodes are those not picked. Example output on the tools repo: https://user-images.githubusercontent.com/3584893/61019393-45338f80-a357-11e9-8d28-b06c41357c0f.png This CL includes a refactor of the input into a graph: this is one part for organization, and another part in preparation for cycle detection. Change-Id: I2ff717642480270659640f098cdf509d479d3ca3 Reviewed-on: https://go-review.googlesource.com/c/exp/+/185657 Run-TryBot: Jay Conrod <jayconrod@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
Родитель
fd42eb6b33
Коммит
cfdd5522f6
|
@ -14,6 +14,10 @@
|
|||
// generated by “go mod graph” on standard input and writes DOT language
|
||||
// on standard output.
|
||||
//
|
||||
// For each module, the node representing the greatest version (i.e., the
|
||||
// version chosen by Go's minimal version selection algorithm) is colored green.
|
||||
// Other nodes, which aren't in the final build list, are colored grey.
|
||||
//
|
||||
// See http://www.graphviz.org/doc/info/lang.html for details of the DOT language
|
||||
// and http://www.graphviz.org/about/ for Graphviz itself.
|
||||
//
|
||||
|
@ -29,11 +33,19 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: go mod graph | modgraphviz | dot -Tpng -o graph.png\n")
|
||||
fmt.Fprintf(os.Stderr, `Usage: go mod graph | modgraphviz | dot -Tpng -o graph.png
|
||||
|
||||
For each module, the node representing the greatest version (i.e., the
|
||||
version chosen by Go's minimal version selection algorithm) is colored green.
|
||||
Other nodes, which aren't in the final build list, are colored grey.
|
||||
`)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
|
@ -47,33 +59,108 @@ func main() {
|
|||
usage()
|
||||
}
|
||||
|
||||
graph, err := convert(os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stdout.Write(graph); err != nil {
|
||||
if err := modgraphviz(os.Stdin, os.Stdout); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// convert reads “go mod graph” output from r
|
||||
// and returns the equivalent DOT digraph.
|
||||
func convert(r io.Reader) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "digraph gomodgraph {\n")
|
||||
func modgraphviz(in io.Reader, out io.Writer) error {
|
||||
graph, err := convert(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "digraph gomodgraph {\n")
|
||||
out.Write(graph.edgesAsDOT())
|
||||
for _, n := range graph.mvsPicked {
|
||||
fmt.Fprintf(out, "\t%q [style = filled, fillcolor = green]\n", n)
|
||||
}
|
||||
for _, n := range graph.mvsUnpicked {
|
||||
fmt.Fprintf(out, "\t%q [style = filled, fillcolor = gray]\n", n)
|
||||
}
|
||||
fmt.Fprintf(out, "}\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type edge struct{ from, to string }
|
||||
type graph struct {
|
||||
edges []edge
|
||||
mvsPicked []string
|
||||
mvsUnpicked []string
|
||||
}
|
||||
|
||||
// convert reads “go mod graph” output from r and returns a graph, recording
|
||||
// MVS picked and unpicked nodes along the way.
|
||||
func convert(r io.Reader) (*graph, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
var g graph
|
||||
seen := map[string]bool{}
|
||||
mvsPicked := map[string]string{} // module name -> module version
|
||||
|
||||
for scanner.Scan() {
|
||||
parts := strings.Fields(scanner.Text())
|
||||
if len(parts) != 2 {
|
||||
l := scanner.Text()
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&buf, "\t%q -> %q\n", parts[0], parts[1])
|
||||
parts := strings.Fields(l)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("expected 2 words in line, but got %d: %s", len(parts), l)
|
||||
}
|
||||
from := parts[0]
|
||||
to := parts[1]
|
||||
g.edges = append(g.edges, edge{from: from, to: to})
|
||||
|
||||
for _, node := range []string{from, to} {
|
||||
if _, ok := seen[node]; ok {
|
||||
// Skip over nodes we've already seen.
|
||||
continue
|
||||
}
|
||||
seen[node] = true
|
||||
|
||||
var m, v string
|
||||
if i := strings.IndexByte(node, '@'); i >= 0 {
|
||||
m, v = node[:i], node[i+1:]
|
||||
} else {
|
||||
// Root node doesn't have a version.
|
||||
continue
|
||||
}
|
||||
|
||||
if maxV, ok := mvsPicked[m]; ok {
|
||||
if semver.Compare(maxV, v) < 0 {
|
||||
// This version is higher - replace it and consign the old
|
||||
// max to the unpicked list.
|
||||
g.mvsUnpicked = append(g.mvsUnpicked, m+"@"+maxV)
|
||||
mvsPicked[m] = v
|
||||
} else {
|
||||
// Other version is higher - stick this version in the
|
||||
// unpicked list.
|
||||
g.mvsUnpicked = append(g.mvsUnpicked, node)
|
||||
}
|
||||
} else {
|
||||
mvsPicked[m] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Fprintf(&buf, "}\n")
|
||||
return buf.Bytes(), nil
|
||||
for m, v := range mvsPicked {
|
||||
g.mvsPicked = append(g.mvsPicked, m+"@"+v)
|
||||
}
|
||||
|
||||
// Make this function deterministic.
|
||||
sort.Strings(g.mvsPicked)
|
||||
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
// edgesAsDOT returns the edges in DOT notation.
|
||||
func (g *graph) edgesAsDOT() []byte {
|
||||
var buf bytes.Buffer
|
||||
for _, e := range g.edges {
|
||||
fmt.Fprintf(&buf, "\t%q -> %q\n", e.from, e.to)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
|
|
@ -6,25 +6,110 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
in := bytes.NewBuffer([]byte(`
|
||||
test.com/A test.com/B@v1.2.3
|
||||
test.com/B test.com/C@v4.5.6
|
||||
test.com/A@v1.0.0 test.com/B@v1.2.3
|
||||
test.com/B@v1.0.0 test.com/C@v4.5.6
|
||||
`))
|
||||
graph, err := convert(in)
|
||||
if err != nil {
|
||||
if err := modgraphviz(in, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := `digraph gomodgraph {
|
||||
"test.com/A" -> "test.com/B@v1.2.3"
|
||||
"test.com/B" -> "test.com/C@v4.5.6"
|
||||
gotGraph := string(out.Bytes())
|
||||
wantGraph := `digraph gomodgraph {
|
||||
"test.com/A@v1.0.0" -> "test.com/B@v1.2.3"
|
||||
"test.com/B@v1.0.0" -> "test.com/C@v4.5.6"
|
||||
"test.com/A@v1.0.0" [style = filled, fillcolor = green]
|
||||
"test.com/B@v1.2.3" [style = filled, fillcolor = green]
|
||||
"test.com/C@v4.5.6" [style = filled, fillcolor = green]
|
||||
"test.com/B@v1.0.0" [style = filled, fillcolor = gray]
|
||||
}
|
||||
`
|
||||
if string(graph) != want {
|
||||
t.Fatalf("\ngot: %s\nwant: %s", string(graph), want)
|
||||
if gotGraph != wantGraph {
|
||||
t.Fatalf("\ngot: %s\nwant: %s", gotGraph, wantGraph)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMVSPicking(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []string
|
||||
wantPicked []string
|
||||
wantUnpicked []string
|
||||
}{
|
||||
{
|
||||
name: "single node",
|
||||
in: []string{"foo@v0.0.1"},
|
||||
wantPicked: []string{"foo@v0.0.1"},
|
||||
wantUnpicked: nil,
|
||||
},
|
||||
{
|
||||
name: "duplicate same node",
|
||||
in: []string{"foo@v0.0.1", "foo@v0.0.1"},
|
||||
wantPicked: []string{"foo@v0.0.1"},
|
||||
wantUnpicked: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple semver - same major",
|
||||
in: []string{"foo@v1.0.0", "foo@v1.3.7", "foo@v1.2.0", "foo@v1.0.1"},
|
||||
wantPicked: []string{"foo@v1.3.7"},
|
||||
wantUnpicked: []string{"foo@v1.0.0", "foo@v1.2.0", "foo@v1.0.1"},
|
||||
},
|
||||
{
|
||||
name: "multiple semver - multiple major",
|
||||
in: []string{"foo@v1.0.0", "foo@v1.3.7", "foo/v2@v2.2.0", "foo/v2@v2.0.1", "foo@v1.1.1"},
|
||||
wantPicked: []string{"foo/v2@v2.2.0", "foo@v1.3.7"},
|
||||
wantUnpicked: []string{"foo@v1.0.0", "foo/v2@v2.0.1", "foo@v1.1.1"},
|
||||
},
|
||||
{
|
||||
name: "semver and pseudo version",
|
||||
in: []string{"foo@v1.0.0", "foo@v1.3.7", "foo/v2@v2.2.0", "foo/v2@v2.0.1", "foo@v1.1.1", "foo@v0.0.0-20190311183353-d8887717615a"},
|
||||
wantPicked: []string{"foo/v2@v2.2.0", "foo@v1.3.7"},
|
||||
wantUnpicked: []string{"foo@v1.0.0", "foo/v2@v2.0.1", "foo@v1.1.1", "foo@v0.0.0-20190311183353-d8887717615a"},
|
||||
},
|
||||
{
|
||||
name: "multiple pseudo version",
|
||||
in: []string{
|
||||
"foo@v0.0.0-20190311183353-d8887717615a",
|
||||
"foo@v0.0.0-20190227222117-0694c2d4d067",
|
||||
"foo@v0.0.0-20190312151545-0bb0c0a6e846",
|
||||
},
|
||||
wantPicked: []string{"foo@v0.0.0-20190312151545-0bb0c0a6e846"},
|
||||
wantUnpicked: []string{
|
||||
"foo@v0.0.0-20190227222117-0694c2d4d067",
|
||||
"foo@v0.0.0-20190311183353-d8887717615a",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "semver and suffix",
|
||||
in: []string{"foo@v1.0.0", "foo@v1.3.8-rc1", "foo@v1.3.7"},
|
||||
wantPicked: []string{"foo@v1.3.8-rc1"},
|
||||
wantUnpicked: []string{"foo@v1.0.0", "foo@v1.3.7"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
for _, node := range tc.in {
|
||||
fmt.Fprintf(&buf, "A %s\n", node)
|
||||
}
|
||||
|
||||
g, err := convert(&buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(g.mvsPicked, tc.wantPicked) {
|
||||
t.Fatalf("picked: got %v, want %v", g.mvsPicked, tc.wantPicked)
|
||||
}
|
||||
if !reflect.DeepEqual(g.mvsUnpicked, tc.wantUnpicked) {
|
||||
t.Fatalf("unpicked: got %v, want %v", g.mvsUnpicked, tc.wantUnpicked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
5
go.mod
5
go.mod
|
@ -1,11 +1,12 @@
|
|||
module golang.org/x/exp
|
||||
|
||||
go 1.11
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313
|
||||
golang.org/x/mod v0.1.0
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -1,14 +1,18 @@
|
|||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6 h1:Tus/Y4w3V77xDsGwKUC8a/QrV7jScpU557J77lFffNs=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846 h1:0oJP+9s5Z3MT6dym56c4f7nVeujVpL1QyD2Vp/bTql0=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
|
|
Загрузка…
Ссылка в новой задаче