diff --git a/README.md b/README.md index 43c6df4a..9004f7e3 100644 --- a/README.md +++ b/README.md @@ -18,24 +18,25 @@ Running ------- ``` -$ retrodep -h retrodep: help requested usage: retrodep [OPTION]... PATH -debug - show debugging output + show debugging output -deps - show vendored dependencies (default true) - -exclude-from string - ignore glob patterns listed in provided file + show vendored dependencies (default true) + -exclude-from exclusions + ignore directory entries matching globs in exclusions -help - print help + print help -importpath string - top-level import path + top-level import path + -o string + output format, one of: go-template=... -only-importpath - only show the top-level import path + only show the top-level import path -template string - go template to use for output with Repo, Rev, Tag and Ver - -x exit on the first failure + go template to use for output with Reference fields (deprecated) + -x exit on the first failure ``` In many cases retrodep can work out the import path for the top-level project. In those cases, simply supply the directory name to examine: @@ -68,24 +69,25 @@ Example output -------------- ``` -$ retrodep -importpath github.com/docker/distribution go/src/github.com/docker/distribution -*github.com/docker/distribution@90705d2fb81dda1466be49bd958ed8a0dd9a6145 ~v2.6.0rc.1-1.20180831002537-90705d2fb81d -github.com/opencontainers/image-spec@87998cd070d9e7a2c79f8b153a26bea0425582e5 =v1.0.0 ~v1.0.0 -github.com/ncw/swift@b964f2ca856aac39885e258ad25aec08d5f64ee6 ~v1.0.25-0.20160617142549-b964f2ca856a -golang.org/x/oauth2@2897dcade18a126645f1368de827f1e613a60049 ~v0.0.0-0.20160323192119-2897dcade18a -rsc.io/letsencrypt ? +$ retrodep $GOPATH/src/github.com/docker/distribution +github.com/docker/distribution:v2.7.1 +github.com/docker/distribution:v2.7.1/github.com/Azure/azure-sdk-for-go:v16.2.1 +github.com/docker/distribution:v2.7.1/github.com/Azure/go-autorest:v10.8.1 +github.com/docker/distribution:v2.7.1/github.com/Shopify/logrus-bugsnag:v0.0.0-0.20171204154709-577dee27f20d +github.com/docker/distribution:v2.7.1/github.com/aws/aws-sdk-go:v1.15.11 +github.com/docker/distribution:v2.7.1/github.com/beorn7/perks:v0.0.0-0.20160804124726-4c0e84591b9a +github.com/docker/distribution:v2.7.1/github.com/bshuster-repo/logrus-logstash-hook:0.4 +github.com/docker/distribution:v2.7.1/github.com/bugsnag/bugsnag-go:v1.0.3-0.20150204195350-f36a9b3a9e01 ... ``` In this example, -* github.com/docker/distribution is the top-level repository (indicated by \*) -* github.com/opencontainers/image-spec had a matching semantic version tag (v1.0.0) -* github.com/ncw/swift's matching commit had semantic version tag v1.0.24 reachable (so the next patch version would be v1.0.25) -* golang.org/x/oauth2 had a matching commit but no reachable semantic version tag -* rsc.io/letsencrypt had no matching commit (because the vendored source was from a fork) -* for github.com/docker/distribution, tag v2.6.0rc.1 was reachable from the matching commit (note, 1.timestamp instead of 0.timestamp) - +* github.com/docker/distribution is the top-level package, and the upstream semantic version tag v2.7.1 matches +* github.com/Azure/azure-sdk-for-go etc are vendored dependencies of distribution +* github.com/Azure/azure-sdk-for-go, github.com/Azure/go-autorest, github.com/aws/awk-sdk-go, and github.com/bshuster-repo/logrus-logstash-hook all had matches with upstream semantic version tags +* github.com/bugsnag/bugsnag-go matched a commit from which tag v1.0.2 was reachable (note: v1.0.2, not v1.0.3 -- see below) +* github.com/beorn7/perks matched a commit from which there were no reachable semantic version tags Pseudo-versions --------------- diff --git a/main.go b/main.go index 754d4af2..0cb0936b 100644 --- a/main.go +++ b/main.go @@ -24,11 +24,14 @@ import ( "path/filepath" "sort" "strings" + "text/template" "github.com/op/go-logging" "github.com/release-engineering/retrodep/retrodep" ) +const defaultTemplate string = "{{if .TopPkg}}{{.TopPkg}}:{{.TopVer}}/{{end}}{{.Pkg}}:{{.Ver}}" + var log = logging.MustGetLogger("retrodep") var helpFlag = flag.Bool("help", false, "print help") @@ -37,10 +40,12 @@ var onlyImportPath = flag.Bool("only-importpath", false, "only show the top-leve var depsFlag = flag.Bool("deps", true, "show vendored dependencies") var excludeFrom = flag.String("exclude-from", "", "ignore directory entries matching globs in `exclusions`") var debugFlag = flag.Bool("debug", false, "show debugging output") -var template = flag.String("template", "", "go template to use for output with Repo, Rev, Tag and Ver") +var outputArg = flag.String("o", "", "output format, one of: go-template=...") +var templateArg = flag.String("template", "", "go template to use for output with Pkg, Repo, Rev, Tag and Ver (deprecated)") var exitFirst = flag.Bool("x", false, "exit on the first failure") var errorShown = false +var usage func(string) func displayUnknown(name string) { fmt.Printf("%s ?\n", name) @@ -53,14 +58,10 @@ func displayUnknown(name string) { } } -func display(template string, name string, ref *retrodep.Reference) { +func display(tmpl *template.Template, name string, ref *retrodep.Reference) { var builder strings.Builder builder.WriteString(name) - var tmpl, err = retrodep.Display(template) - if err != nil { - log.Fatalf("Error parsing supplied template. %s ", err) - } - err = tmpl.Execute(&builder, ref) + err := tmpl.Execute(&builder, ref) if err != nil { log.Fatalf("Error generating output. %s ", err) } @@ -82,20 +83,30 @@ func getProject(src *retrodep.GoSource, importPath string) *retrodep.RepoPath { return main } -func showTopLevel(src *retrodep.GoSource) { +func showTopLevel(tmpl *template.Template, src *retrodep.GoSource) *retrodep.Reference { main := getProject(src, *importPath) - project, err := src.DescribeProject(main, src.Path) + project, err := src.DescribeProject(main, src.Path, nil) + var topLevelMarker string + if *templateArg != "" { + topLevelMarker = "*" + } switch err { case retrodep.ErrorVersionNotFound: - displayUnknown("*" + main.Root) + displayUnknown(topLevelMarker + main.Root) case nil: - display(*template, "*"+main.Root, project) + display(tmpl, topLevelMarker, project) default: log.Fatalf("%s: %s", src.Path, err) } + + if err != nil { + return nil + } + + return project } -func showVendored(src *retrodep.GoSource) { +func showVendored(tmpl *template.Template, src *retrodep.GoSource, top *retrodep.Reference) { vendored, err := src.VendoredProjects() if err != nil { log.Fatal(err) @@ -111,12 +122,12 @@ func showVendored(src *retrodep.GoSource) { // Describe each vendored project for _, repo := range repos { project := vendored[repo] - vp, err := src.DescribeVendoredProject(project) + vp, err := src.DescribeVendoredProject(project, top) switch err { case retrodep.ErrorVersionNotFound: displayUnknown(project.Root) case nil: - display(*template, project.Root, vp) + display(tmpl, "", vp) default: log.Fatalf("%s: %s\n", project.Root, err) } @@ -153,7 +164,7 @@ func processArgs(args []string) []*retrodep.GoSource { cli.Usage = func() {} usageMsg := fmt.Sprintf("usage: %s [OPTION]... PATH", progName) - usage := func(flaw string) { + usage = func(flaw string) { log.Fatalf("%s: %s\n%s\n", progName, flaw, usageMsg) } err := cli.Parse(args[1:]) @@ -198,14 +209,30 @@ func processArgs(args []string) []*retrodep.GoSource { func main() { srcs := processArgs(os.Args) + var customTemplate string + switch { + case *outputArg != "": + customTemplate = strings.TrimPrefix(*outputArg, "go-template=") + if customTemplate == *outputArg { + usage("unknown output format") + } + case *templateArg != "": + customTemplate = "{{.Pkg}}" + *templateArg + default: + customTemplate = defaultTemplate + } + tmpl, err := template.New("output").Parse(customTemplate) + if err != nil { + log.Fatal(err) + } for _, src := range srcs { if *onlyImportPath { main := getProject(src, *importPath) fmt.Println("*" + main.Root) } else { - showTopLevel(src) + top := showTopLevel(tmpl, src) if *depsFlag { - showVendored(src) + showVendored(tmpl, src, top) } } } diff --git a/retrodep/display.go b/retrodep/display.go deleted file mode 100644 index 240905d3..00000000 --- a/retrodep/display.go +++ /dev/null @@ -1,15 +0,0 @@ -package retrodep - -import ( - "text/template" -) - -const defaultTemplate string = "{{if .Rev}}@{{.Rev}}{{end}}{{if .Tag}} ={{.Tag}}{{end}}{{if .Ver}} ~{{.Ver}}{{end}}" - -//Display a Reference using a template -func Display(customTemplate string) (*template.Template, error) { - if customTemplate == "" { - customTemplate = defaultTemplate - } - return template.New("output").Parse(customTemplate) -} diff --git a/retrodep/display_test.go b/retrodep/display_test.go deleted file mode 100644 index 134ab1d7..00000000 --- a/retrodep/display_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package retrodep - -import ( - "fmt" - "strings" - "testing" -) - -func TestDisplayDefault(t *testing.T) { - const expected = "@4309345093405934509 =v0.0.1 ~v0.0.0.20181785" - ref := &Reference{Rev: "4309345093405934509", Tag: "v0.0.1", Ver: "v0.0.0.20181785"} - var builder strings.Builder - var tmpl, err = Display("") - if err != nil { - t.Fatal(err) - } - err = tmpl.Execute(&builder, ref) - if err != nil { - t.Fatal(err) - } - if builder.String() != expected { - t.Fatal(fmt.Sprintf("Expected: %s but got: %s", expected, builder.String())) - } -} - -func TestDisplayGarbage(t *testing.T) { - var _, err = Display("{{.") - if err == nil { - t.Fatal("Should have failed with Error") - } -} - -func TestDisplayNoTag(t *testing.T) { - const expected = "@4309345093405934509 ~v0.0.0.20181785" - ref := &Reference{Rev: "4309345093405934509", Ver: "v0.0.0.20181785"} - var builder strings.Builder - tmpl, err := Display("") - if err != nil { - t.Fatal(err) - } - err = tmpl.Execute(&builder, ref) - if err != nil { - t.Fatal(err) - } - if builder.String() != expected { - t.Fatal(fmt.Sprintf("Expected: %s but got: %s", expected, builder.String())) - } -} - -func TestDisplayTemplate(t *testing.T) { - const expected = ":v0.0.0.20181785" - ref := &Reference{Rev: "4309345093405934509", Ver: "v0.0.0.20181785"} - var builder strings.Builder - tmpl, err := Display("{{if .Tag}}:{{.Tag}}{{end}}{{if .Ver}}:{{.Ver}}{{end}}") - if err != nil { - t.Fatal(err) - } - err = tmpl.Execute(&builder, ref) - if err != nil { - t.Fatal(err) - } - if builder.String() != expected { - t.Fatal(fmt.Sprintf("Expected: %s but got: %s", expected, builder.String())) - } -} - -func TestDisplayTemplateElseIf(t *testing.T) { - const expected = ":v0.0.1" - ref := &Reference{Rev: "4309345093405934509", Tag: "v0.0.1", Ver: "v0.0.0.20181785"} - var builder strings.Builder - tmpl, err := Display("{{if .Tag}}:{{.Tag}}{{else if .Rev}}:{{.Rev}}{{end}}") - if err != nil { - t.Fatal(err) - } - err = tmpl.Execute(&builder, ref) - if err != nil { - t.Fatal(err) - } - if builder.String() != expected { - t.Fatal(fmt.Sprintf("Expected: %s but got: %s", expected, builder.String())) - } -} - -func TestDisplayRepo(t *testing.T) { - const repo = "https://github.com/release-engineering/retrodep" - ref := &Reference{Repo: repo} - var builder strings.Builder - tmpl, err := Display("{{.Repo}}") - if err != nil { - t.Fatal(err) - } - err = tmpl.Execute(&builder, ref) - if err != nil { - t.Fatal(err) - } - if builder.String() != repo { - t.Errorf("got %q, want %q", builder.String(), repo) - } -} diff --git a/retrodep/vendored.go b/retrodep/vendored.go index e4d1c2b4..991491bd 100644 --- a/retrodep/vendored.go +++ b/retrodep/vendored.go @@ -1,4 +1,4 @@ -// Copyright (C) 2018 Tim Waugh +// Copyright (C) 2018, 2019 Tim Waugh // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -230,6 +230,17 @@ func matchFromRefs(strip bool, hashes *FileHashes, wt WorkingTree, subPath strin // Reference describes the origin of a vendored project. type Reference struct { + // TopPkg is the name of the top-level package this package is + // vendored into, or "" if Pkg is the top-level package. + TopPkg string + + // TopVer is the Ver string (see below) for the TopPkg, if + // defined. + TopVer string + + // Pkg is the name of the package this Reference relates to. + Pkg string + // Repo is the URL for the repository holding the source code. Repo string @@ -239,7 +250,7 @@ type Reference struct { Tag string // Rev is the upstream revision from which the vendored - // copy was taken. If this is not known Reference is "". + // copy was taken. If this is not known Rev is "". Rev string // Ver is the semantic version or pseudo-version for the @@ -270,8 +281,13 @@ func chooseBestTag(tags []string) string { // DescribeProject attempts to identify the tag in the version control // system which corresponds to the project, based on comparison with // files in dir. Vendored files and files whose names begin with "." -// are ignored. -func (src GoSource) DescribeProject(project *RepoPath, dir string) (*Reference, error) { +// are ignored. If top is not nil, it should be a Reference to the +// top-level package this project is vendored into. +func (src GoSource) DescribeProject( + project *RepoPath, + dir string, + top *Reference, +) (*Reference, error) { wt, err := NewWorkingTree(&project.RepoRoot) if err != nil { return nil, err @@ -320,6 +336,12 @@ func (src GoSource) DescribeProject(project *RepoPath, dir string) (*Reference, // project). strip := src.usesGodep && dir != src.Path + var toppkg, topver string + if top != nil { + toppkg = top.Pkg + topver = top.Ver + } + // First try to match against a specific version, if specified if project.Version != "" { matches, err := matchFromRefs(strip, hashes, wt, @@ -335,9 +357,12 @@ func (src GoSource) DescribeProject(project *RepoPath, dir string) (*Reference, } return &Reference{ - Repo: project.Repo, - Rev: match, - Ver: ver, + TopPkg: toppkg, + TopVer: topver, + Pkg: project.Root, + Repo: project.Repo, + Rev: match, + Ver: ver, }, nil case ErrorVersionNotFound: // No match, carry on @@ -364,10 +389,13 @@ func (src GoSource) DescribeProject(project *RepoPath, dir string) (*Reference, } return &Reference{ - Repo: project.Repo, - Tag: match, - Rev: rev, - Ver: match, + TopPkg: toppkg, + TopVer: topver, + Pkg: project.Root, + Repo: project.Repo, + Tag: match, + Rev: rev, + Ver: match, }, nil case ErrorVersionNotFound: // No match, carry on @@ -395,18 +423,21 @@ func (src GoSource) DescribeProject(project *RepoPath, dir string) (*Reference, } return &Reference{ - Repo: project.Repo, - Rev: rev, - Ver: ver, + TopPkg: toppkg, + TopVer: topver, + Pkg: project.Root, + Repo: project.Repo, + Rev: rev, + Ver: ver, }, nil } // DescribeVendoredProject attempts to identify the tag in the version // control system which corresponds to the vendored copy of the // project. -func (src GoSource) DescribeVendoredProject(project *RepoPath) (*Reference, error) { +func (src GoSource) DescribeVendoredProject(project *RepoPath, top *Reference) (*Reference, error) { projRootImportPath := filepath.FromSlash(project.Root) projDir := filepath.Join(src.Vendor(), projRootImportPath) - ref, err := src.DescribeProject(project, projDir) + ref, err := src.DescribeProject(project, projDir, top) return ref, err }