diff --git a/.gitignore b/.gitignore index a1338d6..8c2e171 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +bin/ diff --git a/.travis.yml b/.travis.yml index 60a1178..fa9f0b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,8 @@ sudo: false go: - 1.7.x - 1.8.x - -script: go test $(go list ./... | grep -v vendor/) \ No newline at end of file +before_install: + - go get github.com/mattn/goveralls +script: + - $HOME/gopath/bin/goveralls -service=travis-ci +#script: go test $(go list ./... | grep -v vendor/) diff --git a/Makefile b/Makefile index 6efd309..61ac10e 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,20 @@ -# Builds and installs the depth CLI. -install: - @go install -v github.com/KyleBanks/depth/cmd/depth - @echo "depth installed." -.PHONY: install +VERSION = 1.1.0 + +RELEASE_PKG = ./cmd/depth +INSTALL_PKG = $(RELEASE_PKG) + + +# Remote includes require 'mmake' +# github.com/tj/mmake +include github.com/KyleBanks/make/go/install +include github.com/KyleBanks/make/go/sanity +include github.com/KyleBanks/make/go/release +include github.com/KyleBanks/make/go/bench +include github.com/KyleBanks/make/git/precommit # Runs a number of depth commands as examples of what's possible. example: | install - depth github.com/KyleBanks/depth/cmd/depth strings + depth github.com/KyleBanks/depth/cmd/depth strings ./ depth -internal strings @@ -17,9 +25,8 @@ example: | install depth -test -internal strings depth -test -internal -max 3 strings -.PHONY: example -include github.com/KyleBanks/make/misc/precommit -include github.com/KyleBanks/make/go/sanity -include github.com/KyleBanks/make/go/release -include github.com/KyleBanks/make/go/bench + depth . + + depth ./cmd/depth +.PHONY: example diff --git a/README.md b/README.md index e4b2042..89c1dde 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,15 @@ [![GoDoc](https://godoc.org/github.com/KyleBanks/depth?status.svg)](https://godoc.org/github.com/KyleBanks/depth)  [![Build Status](https://travis-ci.org/KyleBanks/depth.svg?branch=master)](https://travis-ci.org/KyleBanks/depth)  -[![Go Report Card](https://goreportcard.com/badge/github.com/KyleBanks/depth)](https://goreportcard.com/report/github.com/KyleBanks/depth) +[![Go Report Card](https://goreportcard.com/badge/github.com/KyleBanks/depth)](https://goreportcard.com/report/github.com/KyleBanks/depth)  +[![Coverage Status](https://coveralls.io/repos/github/KyleBanks/depth/badge.svg?branch=master)](https://coveralls.io/github/KyleBanks/depth?branch=master) `depth` is tool to retrieve and visualize Go source code dependency trees. ## Install +Download the appropriate binary for your platform from the [Releases](https://github.com/KyleBanks/depth/releases) page, or: + ```sh go get github.com/KyleBanks/depth/cmd/depth ``` @@ -18,7 +21,7 @@ go get github.com/KyleBanks/depth/cmd/depth ### Command-Line -Simply execute `depth` with one or more package names to visualize: +Simply execute `depth` with one or more package names to visualize. You can use the fully qualified import path of the package, like so: ```sh $ depth github.com/KyleBanks/depth/cmd/depth @@ -38,6 +41,14 @@ github.com/KyleBanks/depth/cmd/depth └ strings ``` +Or you can use a relative path, for example: + +```sh +$ depth . +$ depth ./cmd/depth +$ depth ../ +``` + You can also use `depth` on the Go standard library: ```sh @@ -52,7 +63,7 @@ strings Visualizing multiple packages at a time is supported by simply naming the packages you'd like to visualize: ```sh -$ depth strings github.com/KyleBanks/depth +$ depth strings github.com/KyleBanks/depth strings ├ errors ├ io @@ -176,10 +187,25 @@ if err != nil { log.Printf("'%v' has %v dependencies.", t.Root.Name, len(t.Root.Deps)) ``` +For additional customization, simply set the appropriate flags on the `Tree` before resolving: + +```go +import "github.com/KyleBanks/depth" + +t := depth.Tree { + ResolveInternal: true, + ResolveTest: true, + MaxDepth: 10, +} + + +err := t.Resolve("strings") +``` + ## Author `depth` was developed by [Kyle Banks](https://twitter.com/kylewbanks). ## License -`depth` is available under [MIT](./LICENSE) \ No newline at end of file +`depth` is available under the [MIT](./LICENSE) license. diff --git a/benchmark/benchmark_test.go b/bench_test.go similarity index 92% rename from benchmark/benchmark_test.go rename to bench_test.go index 059d3df..4daa113 100644 --- a/benchmark/benchmark_test.go +++ b/bench_test.go @@ -7,8 +7,7 @@ import ( ) func BenchmarkTree_ResolveStrings(b *testing.B) { - var t depth.Tree - benchmarkTreeResolveStrings(&t, b) + benchmarkTreeResolveStrings(&depth.Tree{}, b) } func BenchmarkTree_ResolveStringsInternal(b *testing.B) { diff --git a/cmd/depth/depth.go b/cmd/depth/depth.go new file mode 100644 index 0000000..5aed4fd --- /dev/null +++ b/cmd/depth/depth.go @@ -0,0 +1,88 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "os" + "strings" + "sync" + + "github.com/KyleBanks/depth" +) + +const ( + outputPadding = " " + outputPrefix = "├ " + outputPrefixLast = "└ " +) + +var outputJSON bool + +func main() { + ResolveInternal := flag.Bool("internal", false, "If set, resolves dependencies of internal (stdlib) packages.") + ResolveTest := flag.Bool("test", false, "If set, resolves dependencies used for testing.") + MaxDepth := flag.Int("max", 0, "Sets the maximum depth of dependencies to resolve.") + outputJSON := flag.Bool("json", false, "If set, outputs the depencies in JSON format.") + flag.Parse() + + pkgs := flag.Args() + t := &depth.Tree{ + ResolveInternal: *ResolveInternal, + ResolveTest: *ResolveTest, + MaxDepth: *MaxDepth, + } + + handlePkgs(t, pkgs, *outputJSON) + +} + +func handlePkgs(t *depth.Tree, pkgs []string, outputJSON bool) { + var wg sync.WaitGroup + for _, pkg := range pkgs { + wg.Add(1) + go handlePkg(&wg, t, pkg, outputJSON) + } + wg.Wait() +} + +func handlePkg(wg *sync.WaitGroup, t *depth.Tree, pkg string, outputJSON bool) { + defer wg.Done() + err := t.Resolve(pkg) + if err != nil { + fmt.Printf("'%v': FATAL: %v\n", pkg, err) + return + } + if outputJSON { + writePkgJSON(os.Stdout, *t.Root) + } else { + writePkg(os.Stdout, *t.Root, 0, false) + } +} + +// writePkgJSON writes the full Pkg as JSON to the provided Writer. +func writePkgJSON(w io.Writer, p depth.Pkg) { + e := json.NewEncoder(w) + e.SetIndent("", " ") + e.Encode(p) +} + +// writePkg recursively prints a Pkg and its dependencies to the Writer provided. +func writePkg(w io.Writer, p depth.Pkg, indent int, isLast bool) { + var prefix string + if indent > 0 { + prefix = outputPrefix + + if isLast { + prefix = outputPrefixLast + } + } + + out := fmt.Sprintf("%v%v%v\n", strings.Repeat(outputPadding, indent), prefix, p.String()) + w.Write([]byte(out)) + + for idx, d := range p.Deps { + writePkg(w, d, indent+1, idx == len(p.Deps)-1) + } +} diff --git a/cmd/depth/depth_test.go b/cmd/depth/depth_test.go new file mode 100644 index 0000000..2393c9b --- /dev/null +++ b/cmd/depth/depth_test.go @@ -0,0 +1,100 @@ +package main + +import "github.com/KyleBanks/depth" + +func Example_handlePkgsStrings() { + var t depth.Tree + + handlePkgs(&t, []string{"strings"}, false) + // Output: + // strings + // ├ errors + // ├ io + // ├ unicode + // └ unicode/utf8 +} + +func Example_handlePkgsTestStrings() { + var t depth.Tree + t.ResolveTest = true + + handlePkgs(&t, []string{"strings"}, false) + // Output: + // strings + // ├ bytes + // ├ errors + // ├ fmt + // ├ io + // ├ io/ioutil + // ├ math/rand + // ├ reflect + // ├ sync + // ├ testing + // ├ unicode + // ├ unicode/utf8 + // └ unsafe +} + +func Example_handlePkgsDepth() { + var t depth.Tree + + handlePkgs(&t, []string{"github.com/KyleBanks/depth/cmd/depth"}, false) + // Output: + // github.com/KyleBanks/depth/cmd/depth + // ├ encoding/json + // ├ flag + // ├ fmt + // ├ io + // ├ os + // ├ strings + // ├ sync + // └ github.com/KyleBanks/depth + // ├ bytes + // ├ errors + // ├ go/build + // ├ os + // ├ path + // ├ sort + // └ strings +} + +func Example_handlePkgsUnknown() { + var t depth.Tree + + handlePkgs(&t, []string{"notreal"}, false) + // Output: + // 'notreal': FATAL: unable to resolve root package +} + +func Example_handlePkgsJson() { + var t depth.Tree + handlePkgs(&t, []string{"strings"}, true) + + // Output: + // { + // "name": "strings", + // "resolved": true, + // "deps": [ + // { + // "name": "errors", + // "resolved": true, + // "deps": null + // }, + // { + // "name": "io", + // "resolved": true, + // "deps": null + // }, + // { + // "name": "unicode", + // "resolved": true, + // "deps": null + // }, + // { + // "name": "unicode/utf8", + // "resolved": true, + // "deps": null + // } + // ] + // } +} diff --git a/cmd/depth/main.go b/cmd/depth/main.go deleted file mode 100644 index 71f1a57..0000000 --- a/cmd/depth/main.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "fmt" - "io" - "log" - "os" - "strings" - - "github.com/KyleBanks/depth" -) - -const ( - outputPadding = " " - outputPrefix = "├ " - outputPrefixLast = "└ " -) - -func main() { - var t depth.Tree - var outputJSON bool - - flag.BoolVar(&t.ResolveInternal, "internal", false, "If set, resolves dependencies of internal (stdlib) packages.") - flag.BoolVar(&t.ResolveTest, "test", false, "If set, resolves dependencies used for testing.") - flag.IntVar(&t.MaxDepth, "max", 0, "Sets the maximum depth of dependencies to resolve.") - flag.BoolVar(&outputJSON, "json", false, "If set, outputs the depencies in JSON format.") - flag.Parse() - - for _, arg := range flag.Args() { - err := t.Resolve(arg) - if err != nil { - log.Fatal(err) - } - - if outputJSON { - writePkgJSON(os.Stdout, t.Root) - continue - } - - writePkg(os.Stdout, t.Root, 0, false) - } -} - -// writePkgJSON writes the full Pkg as JSON to the provided Writer. -func writePkgJSON(w io.Writer, p *depth.Pkg) { - e := json.NewEncoder(w) - e.SetIndent("", " ") - e.Encode(p) -} - -// writePkg recursively prints a Pkg and its dependencies to the Writer provided. -func writePkg(w io.Writer, p *depth.Pkg, indent int, isLast bool) { - var prefix string - if indent > 0 { - prefix = outputPrefix - - if isLast { - prefix = outputPrefixLast - } - } - - out := fmt.Sprintf("%v%v%v\n", strings.Repeat(outputPadding, indent), prefix, p.Name) - w.Write([]byte(out)) - - for i := 0; i < len(p.Deps); i++ { - writePkg(w, &p.Deps[i], indent+1, i == len(p.Deps)-1) - } -} diff --git a/depth.go b/depth.go index ccfc5bd..0262823 100644 --- a/depth.go +++ b/depth.go @@ -18,10 +18,15 @@ package depth import ( + "errors" "go/build" "sync" ) +// ErrRootPkgNotResolved is returned when the root Pkg of the Tree cannot be resolved, +// typically because it does not exist. +var ErrRootPkgNotResolved = errors.New("unable to resolve root package") + // Importer defines a type that can import a package and return its details. type Importer interface { Import(name, srcDir string, im build.ImportMode) (*build.Package, error) @@ -47,11 +52,21 @@ type Tree struct { func (t *Tree) Resolve(name string) error { t.Root = &Pkg{Name: name, Tree: t} + // Reset the import cache each time to ensure a reused Tree doesn't + // reuse the same cache. + t.importCache = nil + + // Allow custom importers, but use build.Default if none is provided. if t.Importer == nil { t.Importer = &build.Default } - return t.Root.Resolve(t.Importer, true) + t.Root.Resolve(t.Importer) + if !t.Root.Resolved { + return ErrRootPkgNotResolved + } + + return nil } // shouldResolveInternal determines if internal packages should be further resolved beyond the diff --git a/depth_test.go b/depth_test.go index 1da7f9e..6fb8835 100644 --- a/depth_test.go +++ b/depth_test.go @@ -13,6 +13,34 @@ func (m MockImporter) Import(name, srcDir string, im build.ImportMode) (*build.P return m.ImportFn(name, srcDir, im) } +func TestTree_Resolve(t *testing.T) { + // Fail case, bad package name + var tr Tree + if err := tr.Resolve("name"); err != ErrRootPkgNotResolved { + t.Fatalf("Unexpected error, expected=%v, got=%b", ErrRootPkgNotResolved, err) + } + + // Positive case, expect deps + if err := tr.Resolve("strings"); err != nil { + t.Fatal(err) + } + if tr.Root == nil || tr.Root.Name != "strings" { + t.Fatalf("Unexpected Root, expected=%v, got=%v", "strings", tr.Root) + } else if len(tr.Root.Deps) == 0 { + t.Fatal("Expected positive number of Deps") + } + + // Reuse the same tree and the same package to ensure that the internal pkg cache + // is reset and dependencies are still resolved. + stringsDepCount := len(tr.Root.Deps) + if err := tr.Resolve("strings"); err != nil { + t.Fatal(err) + } + if len(tr.Root.Deps) != stringsDepCount { + t.Fatalf("Unexpected number of Deps, expected=%v, got=%b", stringsDepCount, len(tr.Root.Deps)) + } +} + func TestTree_shouldResolveInternal(t *testing.T) { var pt Tree pt.Root = &Pkg{} diff --git a/pkg.go b/pkg.go index 67b5d69..f66f5a8 100644 --- a/pkg.go +++ b/pkg.go @@ -1,53 +1,58 @@ package depth import ( + "bytes" "go/build" "path" "sort" "strings" - "sync" ) // Pkg represents a Go source package, and its dependencies. type Pkg struct { - Name string `json:"name"` - SrcDir string `json:"-"` - Internal bool `json:"-"` + Name string `json:"name"` + SrcDir string `json:"-"` + + Internal bool `json:"-"` + Resolved bool `json:"resolved"` Tree *Tree `json:"-"` Parent *Pkg `json:"-"` - mu *sync.Mutex Deps []Pkg `json:"deps"` } // Resolve recursively finds all dependencies for the Pkg and the packages it depends on. -func (p *Pkg) Resolve(i Importer, resolveImports bool) error { +func (p *Pkg) Resolve(i Importer) { + // Resolved is always true, regardless of if we skip the import, + // it is only false if there is an error while importing. + p.Resolved = true + name := p.cleanName() if name == "" { - return nil - } - - // Stop resolving imports if we've reached max depth. - if resolveImports && p.Tree.isAtMaxDepth(p) { - resolveImports = false + return } + // Stop resolving imports if we've reached max depth or found a duplicate. var importMode build.ImportMode - if !resolveImports { + if p.Tree.hasSeenImport(name) || p.Tree.isAtMaxDepth(p) { importMode = build.FindOnly } pkg, err := i.Import(name, p.SrcDir, importMode) if err != nil { - return err + // TODO: Check the error type? + p.Resolved = false + return } + // Update the name with the fully qualified import path. + p.Name = pkg.ImportPath + // If this is an internal dependency, we may need to skip it. if pkg.Goroot { p.Internal = true - if !p.Tree.shouldResolveInternal(p) { - return nil + return } } @@ -56,72 +61,44 @@ func (p *Pkg) Resolve(i Importer, resolveImports bool) error { imports = append(imports, append(pkg.TestImports, pkg.XTestImports...)...) } - return p.setDeps(i, imports, pkg.Dir) + p.setDeps(i, imports, pkg.Dir) } // setDeps takes a slice of import paths and the source directory they are relative to, // and creates the Deps of the Pkg. Each dependency is also further resolved prior to being added // to the Pkg. -func (p *Pkg) setDeps(i Importer, imports []string, srcDir string) error { - p.mu = &sync.Mutex{} +func (p *Pkg) setDeps(i Importer, imports []string, srcDir string) { unique := make(map[string]struct{}) - errCh := make(chan error) - var wg sync.WaitGroup for _, imp := range imports { // Mostly for testing files where cyclic imports are allowed. if imp == p.Name { continue } + // Skip duplicates. if _, ok := unique[imp]; ok { continue } unique[imp] = struct{}{} - wg.Add(1) - go func(imp string) { - err := p.addDep(i, imp, srcDir) - - wg.Done() - if err != nil { - errCh <- err - } - }(imp) + p.addDep(i, imp, srcDir) } - wg.Wait() - - select { - case err := <-errCh: - return err - default: - sort.Sort(byInternalAndName(p.Deps)) - return nil - } + sort.Sort(byInternalAndName(p.Deps)) } // addDep creates a Pkg and it's dependencies from an imported package name. -func (p *Pkg) addDep(i Importer, name string, srcDir string) error { - // Don't resolve imports for Pkgs that we've already seen and resolved. - resolveImports := !p.Tree.hasSeenImport(name) - +func (p *Pkg) addDep(i Importer, name string, srcDir string) { dep := Pkg{ Name: name, SrcDir: srcDir, Tree: p.Tree, Parent: p, } + dep.Resolve(i) - if err := dep.Resolve(i, resolveImports); err != nil { - return err - } - - p.mu.Lock() p.Deps = append(p.Deps, dep) - p.mu.Unlock() - - return nil } // isParent goes recursively up the chain of Pkgs to determine if the name provided is ever a @@ -169,6 +146,17 @@ func (p *Pkg) cleanName() string { return name } +// String returns a string representation of the Pkg containing the Pkg name and status. +func (p *Pkg) String() string { + b := bytes.NewBufferString(p.Name) + + if !p.Resolved { + b.Write([]byte(" (unresolved)")) + } + + return b.String() +} + // byInternalAndName ensures a slice of Pkgs are sorted such that the internal stdlib // packages are always above external packages (ie. github.com/whatever). type byInternalAndName []Pkg diff --git a/pkg_test.go b/pkg_test.go index 3e9be60..77c3a1a 100644 --- a/pkg_test.go +++ b/pkg_test.go @@ -32,6 +32,7 @@ func TestPkg_CleanName(t *testing.T) { func TestPkg_AddDepImportSeen(t *testing.T) { var m MockImporter var tr Tree + tr.Importer = m testName := "test" testSrcDir := "src/testing" @@ -53,15 +54,11 @@ func TestPkg_AddDepImportSeen(t *testing.T) { } // Hasn't seen the import - if err := p.addDep(m, testName, testSrcDir); err != nil { - t.Fatal(err) - } + p.addDep(m, testName, testSrcDir) // Has seen the import expectedIm = build.FindOnly - if err := p.addDep(m, testName, testSrcDir); err != nil { - t.Fatal(err) - } + p.addDep(m, testName, testSrcDir) } func TestByInternalAndName(t *testing.T) {