From 0c7f4faf0be219453123b82cc5c361f6a219f141 Mon Sep 17 00:00:00 2001 From: Sunny Date: Thu, 25 Jan 2018 23:59:39 +0530 Subject: [PATCH 1/2] graphviz: project-package relation graph This change introduces "clusters", which are projects with multiple subpackages. Clusters are created using "subgraph" in dot syntax. To create a project-package relationship graph, nodes and subgraphs are created first. Nodes are created when a project has a single package and that's the root package. A subgraph/cluster is created when a project has multiple subpackages. createSubgraph(project, packages) takes a project name and its packages and creates nodes or subgraphs/clusters based on the packages. Once all the nodes and subgraphs are created, a target project can be passed to output(project) to generate a dot output with all the nodes and subgraphs related to the target project. Following relation scenarios have been covered: 1. edge from a node within a cluster to a target cluster 2. edge from a node within a cluster to a single node 3. edge from a cluster to a target cluster 4. edge from a cluster to a target single node 5. edge from a cluster to a node within a cluster --- cmd/dep/graphviz.go | 190 ++++++++++++++++- cmd/dep/graphviz_test.go | 269 +++++++++++++++++++++++- cmd/dep/status.go | 2 +- cmd/dep/testdata/graphviz/case1.dot | 2 +- cmd/dep/testdata/graphviz/case2.dot | 2 +- cmd/dep/testdata/graphviz/empty.dot | 2 +- cmd/dep/testdata/graphviz/subgraph1.dot | 27 +++ cmd/dep/testdata/graphviz/subgraph2.dot | 23 ++ cmd/dep/testdata/graphviz/subgraph3.dot | 25 +++ cmd/dep/testdata/graphviz/subgraph4.dot | 15 ++ 10 files changed, 541 insertions(+), 16 deletions(-) create mode 100644 cmd/dep/testdata/graphviz/subgraph1.dot create mode 100644 cmd/dep/testdata/graphviz/subgraph2.dot create mode 100644 cmd/dep/testdata/graphviz/subgraph3.dot create mode 100644 cmd/dep/testdata/graphviz/subgraph4.dot diff --git a/cmd/dep/graphviz.go b/cmd/dep/graphviz.go index b422ddde7e..8ffda94700 100644 --- a/cmd/dep/graphviz.go +++ b/cmd/dep/graphviz.go @@ -8,6 +8,7 @@ import ( "bytes" "fmt" "hash/fnv" + "sort" "strings" ) @@ -15,6 +16,9 @@ type graphviz struct { ps []*gvnode b bytes.Buffer h map[string]uint32 + // clusters is a map of project name and subgraph object. This can be used + // to refer the subgraph by project name. + clusters map[string]*gvsubgraph } type gvnode struct { @@ -23,22 +27,67 @@ type gvnode struct { children []string } +// Sort gvnode(s). +type byGvnode []gvnode + +func (n byGvnode) Len() int { return len(n) } +func (n byGvnode) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n byGvnode) Less(i, j int) bool { return n[i].project < n[j].project } + func (g graphviz) New() *graphviz { ga := &graphviz{ - ps: []*gvnode{}, - h: make(map[string]uint32), + ps: []*gvnode{}, + h: make(map[string]uint32), + clusters: make(map[string]*gvsubgraph), } return ga } -func (g graphviz) output() bytes.Buffer { - g.b.WriteString("digraph {\n\tnode [shape=box];") +func (g *graphviz) output(project string) bytes.Buffer { + if project == "" { + // Project relations graph. + 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())) + } + + g.createProjectRelations() + } else { + // Project-Package relations graph. + g.b.WriteString("digraph {\n\tnode [shape=box];\n\tcompound=true;\n\tedge [minlen=2];") + + // Declare all the nodes with labels. + for _, gvp := range g.ps { + g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label())) + } + + // Sort the clusters for a consistent output. + clusters := sortClusters(g.clusters) + + // Declare all the subgraphs with labels. + for _, gsg := range clusters { + g.b.WriteString(fmt.Sprintf("\n\tsubgraph cluster_%d {", gsg.index)) + g.b.WriteString(fmt.Sprintf("\n\t\tlabel = \"%s\";", gsg.project)) + + nhashes := []string{} + for _, pkg := range gsg.packages { + nhashes = append(nhashes, fmt.Sprint(g.h[pkg])) + } - for _, gvp := range g.ps { - // Create node string - g.b.WriteString(fmt.Sprintf("\n\t%d [label=\"%s\"];", gvp.hash(), gvp.label())) + g.b.WriteString(fmt.Sprintf("\n\t\t%s;", strings.Join(nhashes, " "))) + g.b.WriteString("\n\t}") + } + + g.createProjectPackageRelations(project, clusters) } + g.b.WriteString("\n}\n") + return g.b +} + +func (g *graphviz) createProjectRelations() { // Store relations to avoid duplication rels := make(map[string]bool) @@ -58,9 +107,60 @@ func (g graphviz) output() bytes.Buffer { } } } +} - g.b.WriteString("\n}") - return g.b +func (g *graphviz) createProjectPackageRelations(project string, clusters []*gvsubgraph) { + // This function takes a child package/project, target project, subgraph meta, from + // and to of the edge and write a relation. + linkRelation := func(child, project string, meta []string, from, to uint32) { + if child == project { + // Check if it's a cluster. + target, ok := g.clusters[project] + if ok { + // It's a cluster. Point to the Project Root. Use lhead. + meta = append(meta, fmt.Sprintf("lhead=cluster_%d", target.index)) + // When the head points to a cluster root, use the first + // node in the cluster as to. + to = g.h[target.packages[0]] + } + } + + if len(meta) > 0 { + g.b.WriteString(fmt.Sprintf("\n\t%d -> %d [%s];", from, to, strings.Join(meta, " "))) + } else { + g.b.WriteString(fmt.Sprintf("\n\t%d -> %d;", from, to)) + } + } + + // Create relations from nodes. + for _, node := range g.ps { + for _, child := range node.children { + // Only if it points to the target project, proceed further. + if isPathPrefix(child, project) { + meta := []string{} + from := g.h[node.project] + to := g.h[child] + + linkRelation(child, project, meta, from, to) + } + } + } + + // Create relations from clusters. + for _, cluster := range clusters { + for _, child := range cluster.children { + // Only if it points to the target project, proceed further. + if isPathPrefix(child, project) { + meta := []string{fmt.Sprintf("ltail=cluster_%d", cluster.index)} + // When the tail is from a cluster, use the first node in the + // cluster as from. + from := g.h[cluster.packages[0]] + to := g.h[child] + + linkRelation(child, project, meta, from, to) + } + } + } } func (g *graphviz) createNode(project, version string, children []string) { @@ -108,3 +208,75 @@ func isPathPrefix(path, pre string) bool { return prflen == pathlen || strings.Index(path[prflen:], "/") == 0 } + +// gvsubgraph is a graphviz subgraph with at least one node(package) in it. +type gvsubgraph struct { + project string // Project root name of a project. + packages []string // List of subpackages in the project. + index int // Index of the subgraph cluster. This is used to refer the subgraph in the dot file. + children []string // Dependencies of the project root package. +} + +func (sg gvsubgraph) hash() uint32 { + h := fnv.New32a() + h.Write([]byte(sg.project)) + return h.Sum32() +} + +// createSubgraph creates a graphviz subgraph with nodes in it. This should only +// be created when a project has more than one package. A single package project +// should be just a single node. +// First nodes are created using the provided packages and their imports. Then +// a subgraph is created with all the nodes in it. +func (g *graphviz) createSubgraph(project string, packages map[string][]string) { + // If there's only a single package and that's the project root, do not + // create a subgraph. Just create a node. + if children, ok := packages[project]; ok && len(packages) == 1 { + g.createNode(project, "", children) + return + } + + // Sort and use the packages for consistent output. + pkgs := []gvnode{} + + for name, children := range packages { + pkgs = append(pkgs, gvnode{project: name, children: children}) + } + + sort.Sort(byGvnode(pkgs)) + + subgraphPkgs := []string{} + rootChildren := []string{} + for _, p := range pkgs { + if p.project == project { + // Do not create a separate node for the root package. + rootChildren = append(rootChildren, p.children...) + continue + } + g.createNode(p.project, "", p.children) + subgraphPkgs = append(subgraphPkgs, p.project) + } + + sg := &gvsubgraph{ + project: project, + packages: subgraphPkgs, + index: len(g.clusters), + children: rootChildren, + } + + g.h[project] = sg.hash() + g.clusters[project] = sg +} + +// sortCluster takes a map of all the clusters and returns a list of cluster +// names sorted by the cluster index. +func sortClusters(clusters map[string]*gvsubgraph) []*gvsubgraph { + result := []*gvsubgraph{} + for _, cluster := range clusters { + result = append(result, cluster) + } + sort.Slice(result, func(i, j int) bool { + return result[i].index < result[j].index + }) + return result +} diff --git a/cmd/dep/graphviz_test.go b/cmd/dep/graphviz_test.go index a7537f0074..d34e13b740 100644 --- a/cmd/dep/graphviz_test.go +++ b/cmd/dep/graphviz_test.go @@ -5,6 +5,7 @@ package main import ( + "reflect" "testing" "github.com/golang/dep/internal/test" @@ -17,7 +18,7 @@ func TestEmptyProject(t *testing.T) { g := new(graphviz).New() - b := g.output() + b := g.output("") want := h.GetTestFileString("graphviz/empty.dot") if b.String() != want { @@ -36,7 +37,7 @@ func TestSimpleProject(t *testing.T) { g.createNode("foo", "master", []string{"bar"}) g.createNode("bar", "dev", []string{}) - b := g.output() + b := g.output("") want := h.GetTestFileString("graphviz/case1.dot") if b.String() != want { t.Fatalf("expected '%v', got '%v'", want, b.String()) @@ -52,7 +53,7 @@ func TestNoLinks(t *testing.T) { g.createNode("project", "", []string{}) - b := g.output() + b := g.output("") want := h.GetTestFileString("graphviz/case2.dot") if b.String() != want { t.Fatalf("expected '%v', got '%v'", want, b.String()) @@ -81,3 +82,265 @@ func TestIsPathPrefix(t *testing.T) { } } } + +func TestSimpleSubgraphs(t *testing.T) { + type testProject struct { + name string + packages map[string][]string + } + + testCases := []struct { + name string + projects []testProject + targetProject string + outputfile string + }{ + { + name: "simple graph", + projects: []testProject{ + { + name: "ProjectA", + packages: map[string][]string{ + "ProjectA/pkgX": []string{"ProjectC/pkgZ", "ProjectB/pkgX"}, + "ProjectA/pkgY": []string{"ProjectC/pkgX"}, + }, + }, + { + name: "ProjectB", + packages: map[string][]string{ + "ProjectB/pkgX": []string{}, + "ProjectB/pkgY": []string{"ProjectA/pkgY", "ProjectC/pkgZ"}, + }, + }, + { + name: "ProjectC", + packages: map[string][]string{ + "ProjectC/pkgX": []string{}, + "ProjectC/pkgY": []string{}, + "ProjectC/pkgZ": []string{}, + }, + }, + }, + targetProject: "ProjectC", + outputfile: "graphviz/subgraph1.dot", + }, + { + name: "edges from and to root projects", + projects: []testProject{ + { + name: "ProjectB", + packages: map[string][]string{ + "ProjectB": []string{"ProjectC/pkgX", "ProjectC"}, + "ProjectB/pkgX": []string{}, + "ProjectB/pkgY": []string{"ProjectA/pkgY", "ProjectC/pkgZ"}, + "ProjectB/pkgZ": []string{"ProjectC"}, + }, + }, + { + name: "ProjectC", + packages: map[string][]string{ + "ProjectC/pkgX": []string{}, + "ProjectC/pkgY": []string{}, + "ProjectC/pkgZ": []string{}, + }, + }, + }, + targetProject: "ProjectC", + outputfile: "graphviz/subgraph2.dot", + }, + { + name: "multi and single package projects", + projects: []testProject{ + { + name: "ProjectA", + packages: map[string][]string{ + "ProjectA": []string{"ProjectC/pkgX"}, + }, + }, + { + name: "ProjectB", + packages: map[string][]string{ + "ProjectB": []string{"ProjectC/pkgX", "ProjectC"}, + "ProjectB/pkgX": []string{}, + "ProjectB/pkgY": []string{"ProjectA/pkgY", "ProjectC/pkgZ"}, + "ProjectB/pkgZ": []string{"ProjectC"}, + }, + }, + { + name: "ProjectC", + packages: map[string][]string{ + "ProjectC/pkgX": []string{}, + "ProjectC/pkgY": []string{}, + "ProjectC/pkgZ": []string{}, + }, + }, + }, + targetProject: "ProjectC", + outputfile: "graphviz/subgraph3.dot", + }, + { + name: "relation from a cluster to a node", + projects: []testProject{ + { + name: "ProjectB", + packages: map[string][]string{ + "ProjectB": []string{"ProjectC/pkgX", "ProjectA"}, + "ProjectB/pkgX": []string{}, + "ProjectB/pkgY": []string{"ProjectA", "ProjectC/pkgZ"}, + "ProjectB/pkgZ": []string{"ProjectC"}, + }, + }, + { + name: "ProjectA", + packages: map[string][]string{ + "ProjectA": []string{"ProjectC/pkgX"}, + }, + }, + }, + targetProject: "ProjectA", + outputfile: "graphviz/subgraph4.dot", + }, + } + + h := test.NewHelper(t) + h.Parallel() + defer h.Cleanup() + + for _, tc := range testCases { + g := new(graphviz).New() + + for _, project := range tc.projects { + g.createSubgraph(project.name, project.packages) + } + + output := g.output(tc.targetProject) + want := h.GetTestFileString(tc.outputfile) + if output.String() != want { + t.Fatalf("expected '%v', got '%v'", want, output.String()) + } + } +} + +func TestCreateSubgraph(t *testing.T) { + testCases := []struct { + name string + project string + pkgs map[string][]string + wantNodes []*gvnode + wantClusters map[string]*gvsubgraph + }{ + { + name: "Project with subpackages", + project: "ProjectA", + pkgs: map[string][]string{ + "ProjectA/pkgX": []string{"ProjectC/pkgZ", "ProjectB/pkgX"}, + "ProjectA/pkgY": []string{"ProjectC/pkgX"}, + }, + wantNodes: []*gvnode{ + &gvnode{ + project: "ProjectA/pkgX", + children: []string{"ProjectC/pkgZ", "ProjectB/pkgX"}, + }, + &gvnode{ + project: "ProjectA/pkgY", + children: []string{"ProjectC/pkgX"}, + }, + }, + wantClusters: map[string]*gvsubgraph{ + "ProjectA": &gvsubgraph{ + project: "ProjectA", + packages: []string{"ProjectA/pkgX", "ProjectA/pkgY"}, + index: 0, + children: []string{}, + }, + }, + }, + { + name: "Project with single subpackage at root", + project: "ProjectA", + pkgs: map[string][]string{ + "ProjectA": []string{"ProjectC/pkgZ", "ProjectB/pkgX"}, + }, + wantNodes: []*gvnode{ + &gvnode{ + project: "ProjectA", + children: []string{"ProjectC/pkgZ", "ProjectB/pkgX"}, + }, + }, + wantClusters: map[string]*gvsubgraph{}, + }, + { + name: "Project with subpackages and no children", + project: "ProjectX", + pkgs: map[string][]string{ + "ProjectX/pkgA": []string{}, + }, + wantNodes: []*gvnode{ + &gvnode{ + project: "ProjectX/pkgA", + children: []string{}, + }, + }, + wantClusters: map[string]*gvsubgraph{ + "ProjectX": &gvsubgraph{ + project: "ProjectX", + packages: []string{"ProjectX/pkgA"}, + index: 0, + children: []string{}, + }, + }, + }, + { + name: "Project with subpackage and root package with children", + project: "ProjectA", + pkgs: map[string][]string{ + "ProjectA": []string{"ProjectC/pkgZ", "ProjectB/pkgX"}, + "ProjectA/pkgX": []string{"ProjectC/pkgA"}, + }, + wantNodes: []*gvnode{ + &gvnode{ + project: "ProjectA/pkgX", + children: []string{"ProjectC/pkgA"}, + }, + }, + wantClusters: map[string]*gvsubgraph{ + "ProjectA": &gvsubgraph{ + project: "ProjectA", + packages: []string{"ProjectA/pkgX"}, + index: 0, + children: []string{"ProjectC/pkgZ", "ProjectB/pkgX"}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := new(graphviz).New() + + g.createSubgraph(tc.project, tc.pkgs) + + // Check the number of created nodes. + if len(g.ps) != len(tc.wantNodes) { + t.Errorf("unexpected number of nodes: \n\t(GOT) %v\n\t(WNT) %v", len(g.ps), len(tc.wantNodes)) + } + + // Check if the expected nodes are created. + for i, v := range tc.wantNodes { + if v.project != g.ps[i].project { + t.Errorf("found unexpected node: \n\t(GOT) %v\n\t(WNT) %v", g.ps[i].project, v.project) + } + } + + // Check the number of created clusters. + if len(g.clusters) != len(tc.wantClusters) { + t.Errorf("unexpected number of clusters: \n\t(GOT) %v\n\t(WNT) %v", len(g.clusters), len(tc.wantClusters)) + } + + // Check if the expected clusters are created. + if !reflect.DeepEqual(g.clusters, tc.wantClusters) { + t.Errorf("unexpected clusters: \n\t(GOT) %v\n\t(WNT) %v", g.clusters, tc.wantClusters) + } + }) + } +} diff --git a/cmd/dep/status.go b/cmd/dep/status.go index f87c78679e..56897dc9db 100644 --- a/cmd/dep/status.go +++ b/cmd/dep/status.go @@ -338,7 +338,7 @@ func (out *dotOutput) BasicHeader() error { } func (out *dotOutput) BasicFooter() error { - gvo := out.g.output() + gvo := out.g.output("") _, err := fmt.Fprintf(out.w, gvo.String()) return err } diff --git a/cmd/dep/testdata/graphviz/case1.dot b/cmd/dep/testdata/graphviz/case1.dot index 3de927271d..b4ee9af371 100644 --- a/cmd/dep/testdata/graphviz/case1.dot +++ b/cmd/dep/testdata/graphviz/case1.dot @@ -6,4 +6,4 @@ digraph { 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 index df2d6a4792..dd5dad15ec 100644 --- a/cmd/dep/testdata/graphviz/case2.dot +++ b/cmd/dep/testdata/graphviz/case2.dot @@ -1,4 +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 index 3eabc972dc..3eb9e55348 100644 --- a/cmd/dep/testdata/graphviz/empty.dot +++ b/cmd/dep/testdata/graphviz/empty.dot @@ -1,3 +1,3 @@ digraph { node [shape=box]; -} \ No newline at end of file +} diff --git a/cmd/dep/testdata/graphviz/subgraph1.dot b/cmd/dep/testdata/graphviz/subgraph1.dot new file mode 100644 index 0000000000..1e6b5bc98c --- /dev/null +++ b/cmd/dep/testdata/graphviz/subgraph1.dot @@ -0,0 +1,27 @@ +digraph { + node [shape=box]; + compound=true; + edge [minlen=2]; + 552838292 [label="ProjectA/pkgX"]; + 569615911 [label="ProjectA/pkgY"]; + 2062426895 [label="ProjectB/pkgX"]; + 2045649276 [label="ProjectB/pkgY"]; + 990902230 [label="ProjectC/pkgX"]; + 1007679849 [label="ProjectC/pkgY"]; + 957346992 [label="ProjectC/pkgZ"]; + subgraph cluster_0 { + label = "ProjectA"; + 552838292 569615911; + } + subgraph cluster_1 { + label = "ProjectB"; + 2062426895 2045649276; + } + subgraph cluster_2 { + label = "ProjectC"; + 990902230 1007679849 957346992; + } + 552838292 -> 957346992; + 569615911 -> 990902230; + 2045649276 -> 957346992; +} diff --git a/cmd/dep/testdata/graphviz/subgraph2.dot b/cmd/dep/testdata/graphviz/subgraph2.dot new file mode 100644 index 0000000000..b63d32b453 --- /dev/null +++ b/cmd/dep/testdata/graphviz/subgraph2.dot @@ -0,0 +1,23 @@ +digraph { + node [shape=box]; + compound=true; + edge [minlen=2]; + 2062426895 [label="ProjectB/pkgX"]; + 2045649276 [label="ProjectB/pkgY"]; + 2095982133 [label="ProjectB/pkgZ"]; + 990902230 [label="ProjectC/pkgX"]; + 1007679849 [label="ProjectC/pkgY"]; + 957346992 [label="ProjectC/pkgZ"]; + subgraph cluster_0 { + label = "ProjectB"; + 2062426895 2045649276 2095982133; + } + subgraph cluster_1 { + label = "ProjectC"; + 990902230 1007679849 957346992; + } + 2045649276 -> 957346992; + 2095982133 -> 990902230 [lhead=cluster_1]; + 2062426895 -> 990902230 [ltail=cluster_0]; + 2062426895 -> 990902230 [ltail=cluster_0 lhead=cluster_1]; +} diff --git a/cmd/dep/testdata/graphviz/subgraph3.dot b/cmd/dep/testdata/graphviz/subgraph3.dot new file mode 100644 index 0000000000..3f9b871d9c --- /dev/null +++ b/cmd/dep/testdata/graphviz/subgraph3.dot @@ -0,0 +1,25 @@ +digraph { + node [shape=box]; + compound=true; + edge [minlen=2]; + 1459457741 [label="ProjectA"]; + 2062426895 [label="ProjectB/pkgX"]; + 2045649276 [label="ProjectB/pkgY"]; + 2095982133 [label="ProjectB/pkgZ"]; + 990902230 [label="ProjectC/pkgX"]; + 1007679849 [label="ProjectC/pkgY"]; + 957346992 [label="ProjectC/pkgZ"]; + subgraph cluster_0 { + label = "ProjectB"; + 2062426895 2045649276 2095982133; + } + subgraph cluster_1 { + label = "ProjectC"; + 990902230 1007679849 957346992; + } + 1459457741 -> 990902230; + 2045649276 -> 957346992; + 2095982133 -> 990902230 [lhead=cluster_1]; + 2062426895 -> 990902230 [ltail=cluster_0]; + 2062426895 -> 990902230 [ltail=cluster_0 lhead=cluster_1]; +} diff --git a/cmd/dep/testdata/graphviz/subgraph4.dot b/cmd/dep/testdata/graphviz/subgraph4.dot new file mode 100644 index 0000000000..293292b7aa --- /dev/null +++ b/cmd/dep/testdata/graphviz/subgraph4.dot @@ -0,0 +1,15 @@ +digraph { + node [shape=box]; + compound=true; + edge [minlen=2]; + 2062426895 [label="ProjectB/pkgX"]; + 2045649276 [label="ProjectB/pkgY"]; + 2095982133 [label="ProjectB/pkgZ"]; + 1459457741 [label="ProjectA"]; + subgraph cluster_0 { + label = "ProjectB"; + 2062426895 2045649276 2095982133; + } + 2045649276 -> 1459457741; + 2062426895 -> 1459457741 [ltail=cluster_0]; +} From b2fba4a8bb4ce0714cb044356835f7bcdc9bd50d Mon Sep 17 00:00:00 2001 From: Sunny Date: Tue, 17 Jul 2018 16:41:26 +0530 Subject: [PATCH 2/2] Add to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52e6fbb97c..5a04c39e4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ IMPROVEMENTS: * Update our dependency on Masterminds/semver to follow upstream again now that [Masterminds/semver#67](https://github.com/Masterminds/semver/pull/67) is merged([#1792](https://github.com/golang/dep/pull/1792)). * `inputs-digest` was removed from `Gopkg.lock` ([#1912](https://github.com/golang/dep/pull/1912)). * Don't exclude `Godeps` folder ([#1822](https://github.com/golang/dep/issues/1822)). +* Add project-package relationship graph support in graphviz ([#1588](https://github.com/golang/dep/pull/1588)). WIP: * Enable importing external configuration from dependencies during init (#1277). This is feature flagged and disabled by default.