cmd/gorelease: detect problems before releasing new module versions
gorelease is an experimental tool that helps module authors avoid
common problems before releasing a new version of a module.
gorelease is intended to eventually be merged into the go command as
"go release".
Updates golang/go#26420
Change-Id: I23fbab171b2cb9f4598fa525ecef5dcf006dc7c4
Reviewed-on: https://go-review.googlesource.com/c/exp/+/197299
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/cmd/gorelease/errors.go b/cmd/gorelease/errors.go
new file mode 100644
index 0000000..0ab2ffb
--- /dev/null
+++ b/cmd/gorelease/errors.go
@@ -0,0 +1,52 @@
+// Copyright 2019 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 (
+ "flag"
+ "fmt"
+ "os/exec"
+ "strings"
+
+ "golang.org/x/mod/module"
+ "golang.org/x/xerrors"
+)
+
+type usageError struct {
+ err error
+}
+
+func usageErrorf(format string, args ...interface{}) error {
+ return &usageError{err: fmt.Errorf(format, args...)}
+}
+
+const usageText = `usage: gorelease -base=version [-version=version]`
+
+func (e *usageError) Error() string {
+ msg := ""
+ if !xerrors.Is(e.err, flag.ErrHelp) {
+ msg = e.err.Error()
+ }
+ return usageText + "\n" + msg + "\nFor more information, run go doc golang.org/x/exp/cmd/gorelease"
+}
+
+type downloadError struct {
+ m module.Version
+ err error
+}
+
+func (e *downloadError) Error() string {
+ var msg string
+ if xerr, ok := e.err.(*exec.ExitError); ok {
+ msg = strings.TrimSpace(string(xerr.Stderr))
+ } else {
+ msg = e.err.Error()
+ }
+ sep := " "
+ if strings.Contains(msg, "\n") {
+ sep = "\n"
+ }
+ return fmt.Sprintf("error downloading module %s@%s:%s%s", e.m.Path, e.m.Version, sep, msg)
+}
diff --git a/cmd/gorelease/gorelease.go b/cmd/gorelease/gorelease.go
new file mode 100644
index 0000000..90dda61
--- /dev/null
+++ b/cmd/gorelease/gorelease.go
@@ -0,0 +1,884 @@
+// Copyright 2019 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.
+
+// gorelease is an experimental tool that helps module authors avoid common
+// problems before releasing a new version of a module.
+//
+// gorelease analyzes changes in the public API and dependencies of the main
+// module. It compares a base version (set with -base) with the currently
+// checked out revision. Given a proposed version to release (set with
+// -version), gorelease reports whether the changes are consistent with
+// semantic versioning. If no version is proposed with -version, gorelease
+// suggests the lowest version consistent with semantic versioning.
+//
+// If there are no visible changes in the module's public API, gorelease
+// accepts versions that increment the minor or patch version numbers. For
+// example, if the base version is "v2.3.1", gorelease would accept "v2.3.2" or
+// "v2.4.0" or any prerelease of those versions, like "v2.4.0-beta". If no
+// version is proposed, gorelease would suggest "v2.3.2".
+//
+// If there are only backward compatible differences in the module's public
+// API, gorelease only accepts versions that increment the minor version. For
+// example, if the base version is "v2.3.1", gorelease would accept "v2.4.0"
+// but not "v2.3.2".
+//
+// If there are incompatible API differences for a proposed version with
+// major version 1 or higher, gorelease will exit with a non-zero status.
+// Incompatible differences may only be released in a new major version, which
+// requires creating a module with a different path. For example, if
+// incompatible changes are made in the module "example.com/mod", a
+// new major version must be released as a new module, "example.com/mod/v2".
+// For a proposed version with major version 0, which allows incompatible
+// changes, gorelease will describe all changes, but incompatible changes
+// will not affect its exit status.
+//
+// For more information on semantic versioning, see https://semver.org.
+//
+// gorelease accepts the following flags:
+//
+// -base=version: The version that the current version of the module will be
+// compared against. The version must be a semantic version (for example,
+// "v2.3.4") or "none". If the version is "none", gorelease will not compare the
+// current version against any previous version; it will only validate the
+// current version. This is useful for checking the first release of a new major
+// version.
+//
+// -version=version: The proposed version to be released. If specified,
+// gorelease will confirm whether this version is consistent with changes made
+// to the module's public API. gorelease will exit with a non-zero status if the
+// version is not valid.
+//
+// gorelease is eventually intended to be merged into the go command
+// as "go release". See golang.org/issues/26420.
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
+ "golang.org/x/mod/zip"
+
+ "golang.org/x/exp/apidiff"
+ "golang.org/x/tools/go/packages"
+)
+
+// IDEAS:
+// * Should we suggest versions at all or should -version be mandatory?
+// * 'gorelease path1@version1 path2@version2' should compare two arbitrary
+// modules. Useful for comparing differences in forks.
+// * Verify downstream modules have licenses. May need an API or library
+// for this. Be clear that we can't provide legal advice.
+// * Internal packages may be relevant to submodules (for example,
+// golang.org/x/tools/internal/lsp is imported by golang.org/x/tools).
+// gorelease should detect whether this is the case and include internal
+// directories in comparison. It should be possible to opt out or specify
+// a different list of submodules.
+// * Decide what to do about build constraints, particularly GOOS and GOARCH.
+// The API may be different on some platforms (e.g., x/sys).
+// Should gorelease load packages in multiple configurations in the same run?
+// Is it a compatible change if the same API is available for more platforms?
+// Is it an incompatible change for fewer?
+// How about cgo? Is adding a new cgo dependency an incompatible change?
+// * Support splits and joins of nested modules. For example, if we are
+// proposing to tag a particular commit as both cloud.google.com/go v0.46.2
+// and cloud.google.com/go/storage v1.0.0, we should ensure that the sets of
+// packages provided by those modules are disjoint, and we should not report
+// the packages moved from one to the other as an incompatible change (since
+// the APIs are still compatible, just with a different module split).
+
+// TODO(jayconrod):
+// * Automatically detect base version if unspecified.
+// If -version is vX.Y.(Z+1), use vX.Y.Z (with a message if it doesn't exist)
+// If -version is vX.(Y+1).0, use vX.Y.0
+// If -version is vX.0.0, use none
+// If -version is a prerelease, use same base as if it were a release.
+// If -version is not set, use latest release version or none.
+// * Allow -base to be an arbitrary revision name that resolves to a version
+// or pseudo-version.
+// * Don't accept -version that increments minor or patch version by more than 1
+// or increments the minor version without zeroing the patch, compared with
+// existing versions. Note that -base may be distant from -version,
+// for example, when reverting an incompatible change accidentally released.
+// * Report errors when packages can't be loaded without replace / exclude.
+// * Clean up overuse of fmt.Errorf.
+// * Support -json output.
+// * Don't suggest a release tag that already exists.
+// * Suggest a minor release if dependency has been bumped by minor version.
+// * Updating go version, either in the main module or in a dependency that
+// provides packages transitively imported by non-internal, non-test packages
+// in the main module, should require a minor version bump.
+// * Support migration to modules after v2.x.y+incompatible. Requires comparing
+// packages with different module paths.
+// * Error when packages import from earlier major version of same module.
+// (this may be intentional; look for real examples first).
+// * Check that proposed prerelease will not sort below pseudo-versions.
+// * Error messages point to HTML documentation.
+// * Positional arguments should specify which packages to check. Without
+// these, we check all non-internal packages in the module.
+// * Mechanism to suppress error messages.
+// * Check that the main module does not transitively require a newer version
+// of itself.
+// * Invalid file names and import paths should be reported sensibly.
+// golang.org/x/mod/zip should return structured errors for this.
+
+func main() {
+ log.SetFlags(0)
+ log.SetPrefix("gorelease: ")
+ wd, err := os.Getwd()
+ if err != nil {
+ log.Fatal(err)
+ }
+ success, err := runRelease(os.Stdout, wd, os.Args[1:])
+ if err != nil {
+ if _, ok := err.(*usageError); ok {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(2)
+ } else {
+ log.Fatal(err)
+ }
+ }
+ if !success {
+ os.Exit(1)
+ }
+}
+
+// runRelease is the main function of gorelease. It's called by tests, so
+// it writes to w instead of os.Stdout and returns an error instead of
+// exiting.
+func runRelease(w io.Writer, dir string, args []string) (success bool, err error) {
+ // Validate arguments and flags. We'll print our own errors, since we want to
+ // test without printing to stderr.
+ fs := flag.NewFlagSet("gorelease", flag.ContinueOnError)
+ fs.Usage = func() {}
+ fs.SetOutput(ioutil.Discard)
+ var baseVersion, releaseVersion string
+ fs.StringVar(&baseVersion, "base", "", "previous version to compare against")
+ fs.StringVar(&releaseVersion, "version", "", "proposed version to be released")
+ if err := fs.Parse(args); err != nil {
+ return false, &usageError{err: err}
+ }
+
+ if len(fs.Args()) > 0 {
+ return false, usageErrorf("no arguments allowed")
+ }
+ if baseVersion == "" {
+ return false, usageErrorf("-base flag must be specified.\nUse -base=none if there is no previous version.")
+ }
+ if baseVersion != "none" {
+ if c := semver.Canonical(baseVersion); c != baseVersion {
+ return false, usageErrorf("base version %q is not a canonical semantic version", baseVersion)
+ }
+ }
+ if releaseVersion != "" {
+ if c := semver.Canonical(releaseVersion); c != releaseVersion {
+ return false, usageErrorf("release version %q is not a canonical semantic version", releaseVersion)
+ }
+ }
+ if baseVersion != "none" && releaseVersion != "" {
+ if cmp := semver.Compare(baseVersion, releaseVersion); cmp == 0 {
+ return false, usageErrorf("-base and -version must be different")
+ } else if cmp > 0 {
+ return false, usageErrorf("base version (%q) must be lower than release version (%q)", baseVersion, releaseVersion)
+ }
+ }
+
+ modRoot, err := findModuleRoot(dir)
+ if err != nil {
+ return false, err
+ }
+ repoRoot := findRepoRoot(modRoot)
+ if repoRoot == "" {
+ repoRoot = modRoot
+ }
+
+ report, err := makeReleaseReport(modRoot, repoRoot, baseVersion, releaseVersion)
+ if err != nil {
+ return false, err
+ }
+ if err := report.Text(w); err != nil {
+ return false, err
+ }
+ return report.isSuccessful(), nil
+}
+
+// makeReleaseReport returns a report comparing the current version of a
+// module with a previously released version. The report notes any backward
+// compatible and incompatible changes in the module's public API. It also
+// diagnoses common problems, such as go.mod or go.sum being incomplete.
+// The report recommends or validates a release version and indicates a
+// version control tag to use (with an appropriate prefix, for modules not
+// in the repository root directory).
+//
+// modRoot is the directory containing the module's go.mod file. It must not
+// be "".
+//
+// repoRoot the root directory of the version control repository containing
+// modRoot. It must not be ""; if there is no known repository, repoRoot
+// should be set to modRoot.
+//
+// baseVersion is a previously released version of the module to compare.
+// If baseVersion is "none", no comparison will be performed, and
+// the returned report will only describe problems with the release version.
+//
+// releaseVersion is the proposed version for the module in dir.
+// If releaseVersion is "", the report will suggest a release version based on
+// changes to the public API.
+func makeReleaseReport(modRoot, repoRoot, baseVersion, releaseVersion string) (report, error) {
+ if !hasFilePathPrefix(modRoot, repoRoot) {
+ // runRelease should always make sure this is true.
+ return report{}, fmt.Errorf("module root %q is not in repository root %q", modRoot, repoRoot)
+ }
+
+ // Read the module path from the go.mod file.
+ goModPath := filepath.Join(modRoot, "go.mod")
+ goModData, err := ioutil.ReadFile(goModPath)
+ if err != nil {
+ return report{}, err
+ }
+ modFile, err := modfile.ParseLax(goModPath, goModData, nil)
+ if err != nil {
+ return report{}, err
+ }
+ if modFile.Module == nil {
+ return report{}, fmt.Errorf("%s: module directive is missing", goModPath)
+ }
+ modPath := modFile.Module.Mod.Path
+ if err := checkModPath(modPath); err != nil {
+ return report{}, err
+ }
+ _, modPathMajor, ok := module.SplitPathVersion(modPath)
+ if !ok {
+ // we just validated the path above.
+ panic(fmt.Sprintf("could not find version suffix in module path %q", modPath))
+ }
+
+ if baseVersion != "none" {
+ if err := module.Check(modPath, baseVersion); err != nil {
+ return report{}, fmt.Errorf("can't compare major versions: base version %s does not belong to module %s", baseVersion, modPath)
+ }
+ }
+ // releaseVersion is checked by report.validateVersion.
+
+ // Check if a go version is present in go.mod.
+ var diagnostics []string
+ if modFile.Go == nil {
+ diagnostics = append(diagnostics, "go.mod: go directive is missing")
+ }
+
+ // Determine the version tag prefix for the module within the repository.
+ tagPrefix := ""
+ if modRoot != repoRoot {
+ if strings.HasPrefix(modPathMajor, ".") {
+ diagnostics = append(diagnostics, fmt.Sprintf("%s: module path starts with gopkg.in and must be declared in the root directory of the repository", modPath))
+ } else {
+ codeDir := filepath.ToSlash(modRoot[len(repoRoot)+1:])
+ var altGoModPath string
+ if modPathMajor == "" {
+ // module has no major version suffix.
+ // codeDir must be a suffix of modPath.
+ // tagPrefix is codeDir with a trailing slash.
+ if strings.HasSuffix(modPath, "/"+codeDir) {
+ tagPrefix = codeDir + "/"
+ } else {
+ diagnostics = append(diagnostics, fmt.Sprintf("%s: module path must end with %[2]q, since it is in subdirectory %[2]q", modPath, codeDir))
+ }
+ } else {
+ if strings.HasSuffix(modPath, "/"+codeDir) {
+ // module has a major version suffix and is in a major version subdirectory.
+ // codeDir must be a suffix of modPath.
+ // tagPrefix must not include the major version.
+ tagPrefix = codeDir[:len(codeDir)-len(modPathMajor)+1]
+ altGoModPath = modRoot[:len(modRoot)-len(modPathMajor)+1] + "go.mod"
+ } else if strings.HasSuffix(modPath, "/"+codeDir+modPathMajor) {
+ // module has a major version suffix and is not in a major version subdirectory.
+ // codeDir + modPathMajor is a suffix of modPath.
+ // tagPrefix is codeDir with a trailing slash.
+ tagPrefix = codeDir + "/"
+ altGoModPath = filepath.Join(modRoot, modPathMajor[1:], "go.mod")
+ } else {
+ diagnostics = append(diagnostics, fmt.Sprintf("%s: module path must end with %[2]q or %q, since it is in subdirectory %[2]q", modPath, codeDir, codeDir+modPathMajor))
+ }
+ }
+
+ // Modules with major version suffixes can be defined in two places
+ // (e.g., sub/go.mod and sub/v2/go.mod). They must not be defined in both.
+ if altGoModPath != "" {
+ if data, err := ioutil.ReadFile(altGoModPath); err == nil {
+ if altModPath := modfile.ModulePath(data); modPath == altModPath {
+ goModRel, _ := filepath.Rel(repoRoot, goModPath)
+ altGoModRel, _ := filepath.Rel(repoRoot, altGoModPath)
+ diagnostics = append(diagnostics, fmt.Sprintf("module is defined in two locations:\n\t%s\n\t%s", goModRel, altGoModRel))
+ }
+ }
+ }
+ }
+ }
+
+ // Load the base version of the module.
+ // We download it into the module cache, then create a go.mod in a temporary
+ // directory that requires it. It's important that we don't load the module
+ // as the main module so that replace and exclude directives are not applied.
+ var basePkgs []*packages.Package
+ if baseVersion != "none" {
+ baseMod := module.Version{Path: modPath, Version: baseVersion}
+ baseModRoot, err := downloadModule(baseMod)
+ if err != nil {
+ return report{}, err
+ }
+ baseLoadDir, goModData, goSumData, err := prepareExternalDirForBase(modPath, baseVersion, baseModRoot)
+ if err != nil {
+ return report{}, err
+ }
+ defer os.RemoveAll(baseLoadDir)
+ if basePkgs, _, err = loadPackages(modPath, baseModRoot, baseLoadDir, goModData, goSumData); err != nil {
+ return report{}, err
+ }
+ }
+
+ // Load the release version of the module.
+ // We pack it into a zip file and extract it to a temporary directory as if
+ // it were published and downloaded. We'll detect any errors that would occur
+ // (for example, invalid file name). Again, we avoid loading it as the
+ // main module.
+ releaseModRoot, err := copyModuleToTempDir(modPath, modRoot)
+ if err != nil {
+ return report{}, err
+ }
+ defer os.RemoveAll(releaseModRoot)
+ releaseLoadDir, goModData, goSumData, err := prepareExternalDirForRelease(modFile, modPath, releaseModRoot)
+ if err != nil {
+ return report{}, nil
+ }
+ releasePkgs, loadDiagnostics, err := loadPackages(modPath, releaseModRoot, releaseLoadDir, goModData, goSumData)
+ if err != nil {
+ return report{}, err
+ }
+ diagnostics = append(diagnostics, loadDiagnostics...)
+
+ // Compare each pair of packages.
+ // Ignore internal packages.
+ // If we don't have a base version to compare against,
+ // just check the new packages for errors.
+ shouldCompare := baseVersion != "none"
+ isInternal := func(pkgPath string) bool {
+ if !hasPathPrefix(pkgPath, modPath) {
+ panic(fmt.Sprintf("package %s not in module %s", pkgPath, modPath))
+ }
+ for pkgPath != modPath {
+ if path.Base(pkgPath) == "internal" {
+ return true
+ }
+ pkgPath = path.Dir(pkgPath)
+ }
+ return false
+ }
+ r := report{
+ modulePath: modPath,
+ baseVersion: baseVersion,
+ releaseVersion: releaseVersion,
+ tagPrefix: tagPrefix,
+ diagnostics: diagnostics,
+ }
+ for _, pair := range zipPackages(basePkgs, releasePkgs) {
+ basePkg, releasePkg := pair.base, pair.release
+ switch {
+ case releasePkg == nil:
+ // Package removed
+ if !isInternal(basePkg.PkgPath) || len(basePkg.Errors) > 0 {
+ pr := packageReport{
+ path: basePkg.PkgPath,
+ baseErrors: basePkg.Errors,
+ }
+ if !isInternal(basePkg.PkgPath) {
+ pr.Report = apidiff.Report{
+ Changes: []apidiff.Change{{
+ Message: "package removed",
+ Compatible: false,
+ }},
+ }
+ }
+ r.addPackage(pr)
+ }
+
+ case basePkg == nil:
+ // Package added
+ if !isInternal(releasePkg.PkgPath) && shouldCompare || len(releasePkg.Errors) > 0 {
+ pr := packageReport{
+ path: releasePkg.PkgPath,
+ releaseErrors: releasePkg.Errors,
+ }
+ if !isInternal(releasePkg.PkgPath) && shouldCompare {
+ // If we aren't comparing against a base version, don't say
+ // "package added". Only report packages with errors.
+ pr.Report = apidiff.Report{
+ Changes: []apidiff.Change{{
+ Message: "package added",
+ Compatible: true,
+ }},
+ }
+ }
+ r.addPackage(pr)
+ }
+
+ default:
+ // Matched packages
+ if !isInternal(basePkg.PkgPath) && basePkg.Name != "main" && releasePkg.Name != "main" {
+ pr := packageReport{
+ path: basePkg.PkgPath,
+ baseErrors: basePkg.Errors,
+ releaseErrors: releasePkg.Errors,
+ Report: apidiff.Changes(basePkg.Types, releasePkg.Types),
+ }
+ r.addPackage(pr)
+ }
+ }
+ }
+
+ if releaseVersion != "" {
+ r.validateVersion()
+ } else {
+ r.suggestVersion()
+ }
+
+ return r, nil
+}
+
+// findRepoRoot finds the root directory of the repository that contains dir.
+// findRepoRoot returns "" if it can't find the repository root.
+func findRepoRoot(dir string) string {
+ vcsDirs := []string{".git", ".hg", ".svn", ".bzr"}
+ d := filepath.Clean(dir)
+ for {
+ for _, vcsDir := range vcsDirs {
+ if _, err := os.Stat(filepath.Join(d, vcsDir)); err == nil {
+ return d
+ }
+ }
+ parent := filepath.Dir(d)
+ if parent == d {
+ return ""
+ }
+ d = parent
+ }
+}
+
+// findModuleRoot finds the root directory of the module that contains dir.
+func findModuleRoot(dir string) (string, error) {
+ d := filepath.Clean(dir)
+ for {
+ if fi, err := os.Stat(filepath.Join(d, "go.mod")); err == nil && !fi.IsDir() {
+ return dir, nil
+ }
+ parent := filepath.Dir(d)
+ if parent == d {
+ break
+ }
+ d = parent
+ }
+ return "", fmt.Errorf("%s: cannot find go.mod file", dir)
+}
+
+// checkModPath is like golang.org/x/mod/module.CheckPath, but it returns
+// friendlier error messages for common mistakes.
+//
+// TODO(jayconrod): update module.CheckPath and delete this function.
+func checkModPath(modPath string) error {
+ if path.IsAbs(modPath) || filepath.IsAbs(modPath) {
+ // TODO(jayconrod): improve error message in x/mod instead of checking here.
+ return fmt.Errorf("module path %q must not be an absolute path.\nIt must be an address where your module may be found.", modPath)
+ }
+ if suffix := dirMajorSuffix(modPath); suffix == "v0" || suffix == "v1" {
+ return fmt.Errorf("module path %q has major version suffix %q.\nA major version suffix is only allowed for v2 or later.", modPath, suffix)
+ } else if strings.HasPrefix(suffix, "v0") {
+ return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not have a leading zero.", modPath, suffix)
+ } else if strings.ContainsRune(suffix, '.') {
+ return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not contain dots.", modPath, suffix)
+ }
+ return module.CheckPath(modPath)
+}
+
+// dirMajorSuffix returns a major version suffix for a slash-separated path.
+// For example, for the path "foo/bar/v2", dirMajorSuffix would return "v2".
+// If no major version suffix is found, "" is returned.
+//
+// dirMajorSuffix is less strict than module.SplitPathVersion so that incorrect
+// suffixes like "v0", "v02", "v1.2" can be detected. It doesn't handle
+// special cases for gopkg.in paths.
+func dirMajorSuffix(path string) string {
+ i := len(path)
+ for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') || path[i-1] == '.' {
+ i--
+ }
+ if i <= 1 || i == len(path) || path[i-1] != 'v' || (i > 1 && path[i-2] != '/') {
+ return ""
+ }
+ return path[i-1:]
+}
+
+// hasPathPrefix reports whether the slash-separated path s
+// begins with the elements in prefix.
+// Copied from cmd/go/internal/str.HasPathPrefix.
+func hasPathPrefix(s, prefix string) bool {
+ if len(s) == len(prefix) {
+ return s == prefix
+ }
+ if prefix == "" {
+ return true
+ }
+ if len(s) > len(prefix) {
+ if prefix[len(prefix)-1] == '/' || s[len(prefix)] == '/' {
+ return s[:len(prefix)] == prefix
+ }
+ }
+ return false
+}
+
+// hasFilePathPrefix reports whether the filesystem path s
+// begins with the elements in prefix.
+// Copied from cmd/go/internal/str.HasFilePathPrefix.
+func hasFilePathPrefix(s, prefix string) bool {
+ sv := strings.ToUpper(filepath.VolumeName(s))
+ pv := strings.ToUpper(filepath.VolumeName(prefix))
+ s = s[len(sv):]
+ prefix = prefix[len(pv):]
+ switch {
+ default:
+ return false
+ case sv != pv:
+ return false
+ case len(s) == len(prefix):
+ return s == prefix
+ case prefix == "":
+ return true
+ case len(s) > len(prefix):
+ if prefix[len(prefix)-1] == filepath.Separator {
+ return strings.HasPrefix(s, prefix)
+ }
+ return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
+ }
+}
+
+// copyModuleToTempDir copies module files from modRoot to a subdirectory of
+// scratchDir. Submodules, vendor directories, and irregular files are excluded.
+// An error is returned if the module contains any files or directories that
+// can't be included in a module zip file (due to special characters,
+// excessive sizes, etc.).
+func copyModuleToTempDir(modPath, modRoot string) (dir string, err error) {
+ // Generate a fake version consistent with modPath. We need a canonical
+ // version to create a zip file.
+ version := "v0.0.0-gorelease"
+ _, majorPathSuffix, _ := module.SplitPathVersion(modPath)
+ if majorPathSuffix != "" {
+ version = majorPathSuffix[1:] + ".0.0-gorelease"
+ }
+ m := module.Version{Path: modPath, Version: version}
+
+ zipFile, err := ioutil.TempFile("", "gorelease-*.zip")
+ if err != nil {
+ return "", err
+ }
+ defer func() {
+ zipFile.Close()
+ os.Remove(zipFile.Name())
+ }()
+
+ dir, err = ioutil.TempDir("", "gorelease")
+ if err != nil {
+ return "", err
+ }
+ defer func() {
+ if err != nil {
+ os.RemoveAll(dir)
+ dir = ""
+ }
+ }()
+
+ if err := zip.CreateFromDir(zipFile, m, modRoot); err != nil {
+ return "", err
+ }
+ if err := zipFile.Close(); err != nil {
+ return "", err
+ }
+ if err := zip.Unzip(dir, m, zipFile.Name()); err != nil {
+ return "", err
+ }
+ return dir, nil
+}
+
+// downloadModule downloads a specific version of a module to the
+// module cache using 'go mod download'.
+func downloadModule(m module.Version) (modRoot string, err error) {
+ defer func() {
+ if err != nil {
+ err = &downloadError{m: m, err: err}
+ }
+ }()
+
+ // Run 'go mod download' from a temporary directory to avoid needing to load
+ // go.mod from gorelease's working directory (or a parent).
+ // go.mod may be broken, and we don't need it.
+ // TODO(golang.org/issue/36812): 'go mod download' reads go.mod even though
+ // we don't need information about the main module or the build list.
+ // If it didn't read go.mod in this case, we wouldn't need a temp directory.
+ tmpDir, err := ioutil.TempDir("", "gorelease-download")
+ if err != nil {
+ return "", err
+ }
+ defer os.Remove(tmpDir)
+ cmd := exec.Command("go", "mod", "download", "-json", "--", m.Path+"@"+m.Version)
+ cmd.Dir = tmpDir
+ out, err := cmd.Output()
+ var xerr *exec.ExitError
+ if err != nil {
+ var ok bool
+ if xerr, ok = err.(*exec.ExitError); !ok {
+ return "", err
+ }
+ }
+
+ // If 'go mod download' exited unsuccessfully but printed well-formed JSON
+ // with an error, return that error.
+ parsed := struct{ Dir, Error string }{}
+ if jsonErr := json.Unmarshal(out, &parsed); jsonErr != nil {
+ if xerr != nil {
+ return "", xerr
+ }
+ return "", jsonErr
+ }
+ if parsed.Error != "" {
+ return "", errors.New(parsed.Error)
+ }
+ if xerr != nil {
+ return "", xerr
+ }
+ return parsed.Dir, nil
+}
+
+// prepareExternalDirForBase creates a temporary directory and a go.mod file
+// that requires the module at the given version. go.sum is copied if present.
+func prepareExternalDirForBase(modPath, version, modRoot string) (dir string, goModData, goSumData []byte, err error) {
+ dir, err = ioutil.TempDir("", "gorelease-base")
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ buf := &bytes.Buffer{}
+ fmt.Fprintf(buf, `module gorelease-base-module
+
+require %s %s
+`, modPath, version)
+ goModData = buf.Bytes()
+ if err := ioutil.WriteFile(filepath.Join(dir, "go.mod"), goModData, 0666); err != nil {
+ return "", nil, nil, err
+ }
+
+ goSumData, err = ioutil.ReadFile(filepath.Join(modRoot, "go.sum"))
+ if err != nil && !os.IsNotExist(err) {
+ return "", nil, nil, err
+ }
+ if err := ioutil.WriteFile(filepath.Join(dir, "go.sum"), goSumData, 0666); err != nil {
+ return "", nil, nil, err
+ }
+
+ return dir, goModData, goSumData, nil
+}
+
+// prepareExternalDirForRelease creates a temporary directory and a go.mod file
+// that requires the module and replaces it with modRoot. go.sum is copied
+// if present.
+func prepareExternalDirForRelease(modFile *modfile.File, modPath, modRoot string) (dir string, goModData, goSumData []byte, err error) {
+ dir, err = ioutil.TempDir("", "gorelease-release")
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ version := "v0.0.0-gorelease"
+ if _, pathMajor, _ := module.SplitPathVersion(modPath); pathMajor != "" {
+ version = pathMajor[1:] + ".0.0-gorelease"
+ }
+
+ f := &modfile.File{}
+ f.AddModuleStmt("gorelease-release-module")
+ f.AddRequire(modPath, version)
+ f.AddReplace(modPath, version, modRoot, "")
+ for _, r := range modFile.Require {
+ f.AddRequire(r.Mod.Path, r.Mod.Version)
+ }
+ goModData, err = f.Format()
+ if err != nil {
+ return "", nil, nil, err
+ }
+ if err := ioutil.WriteFile(filepath.Join(dir, "go.mod"), goModData, 0666); err != nil {
+ return "", nil, nil, err
+ }
+
+ goSumData, err = ioutil.ReadFile(filepath.Join(modRoot, "go.sum"))
+ if err != nil && !os.IsNotExist(err) {
+ return "", nil, nil, err
+ }
+ if err := ioutil.WriteFile(filepath.Join(dir, "go.sum"), goSumData, 0666); err != nil {
+ return "", nil, nil, err
+ }
+
+ return dir, goModData, goSumData, nil
+}
+
+// loadPackages returns a list of all packages in the module modPath, sorted by
+// package path. modRoot is the module root directory, but packages are loaded
+// from loadDir, which must contain go.mod and go.sum containing goModData and
+// goSumData.
+//
+// We load packages from a temporary external module so that replace and exclude
+// directives are not applied. The loading process may also modify go.mod and
+// go.sum, and we want to detect and report differences.
+//
+// Package loading errors will be returned in the Errors field of each package.
+// Other diagnostics (such as the go.sum file being incomplete) will be
+// returned through diagnostics.
+// err will be non-nil in case of a fatal error that prevented packages
+// from being loaded.
+func loadPackages(modPath, modRoot, loadDir string, goModData, goSumData []byte) (pkgs []*packages.Package, diagnostics []string, err error) {
+ // List packages in the module.
+ // We can't just load example.com/mod/... because that might include packages
+ // in nested modules. We also can't filter packages from the output of
+ // packages.Load, since it doesn't tell us which module they came from.
+ format := fmt.Sprintf(`{{if .Module}}{{if eq .Module.Path %q}}{{.ImportPath}}{{end}}{{end}}`, modPath)
+ cmd := exec.Command("go", "list", "-e", "-f", format, "--", modPath+"/...")
+ cmd.Dir = loadDir
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, nil, err
+ }
+ var pkgPaths []string
+ for len(out) > 0 {
+ eol := bytes.IndexByte(out, '\n')
+ if eol < 0 {
+ eol = len(out)
+ }
+ pkgPaths = append(pkgPaths, string(out[:eol]))
+ out = out[eol+1:]
+ }
+
+ // Load packages.
+ // TODO(jayconrod): if there are errors loading packages in the release
+ // version, try loading in the release directory. Errors there would imply
+ // that packages don't load without replace / exclude directives.
+ cfg := &packages.Config{
+ Mode: packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
+ Dir: loadDir,
+ }
+ if len(pkgPaths) > 0 {
+ pkgs, err = packages.Load(cfg, pkgPaths...)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ // Trim modRoot from file paths in errors.
+ prefix := modRoot + string(os.PathSeparator)
+ for _, pkg := range pkgs {
+ for i := range pkg.Errors {
+ pkg.Errors[i].Pos = strings.TrimPrefix(pkg.Errors[i].Pos, prefix)
+ }
+ }
+
+ // Report new requirements in go.mod.
+ goModPath := filepath.Join(loadDir, "go.mod")
+ loadReqs := func(data []byte) (string, error) {
+ buf := &bytes.Buffer{}
+ modFile, err := modfile.ParseLax(goModPath, data, nil)
+ if err != nil {
+ return "", err
+ }
+ for _, req := range modFile.Require {
+ fmt.Fprintf(buf, "%v\n", req.Mod)
+ }
+ return buf.String(), nil
+ }
+
+ oldReqs, err := loadReqs(goModData)
+ if err != nil {
+ return nil, nil, err
+ }
+ newGoModData, err := ioutil.ReadFile(goModPath)
+ if err != nil {
+ return nil, nil, err
+ }
+ newReqs, err := loadReqs(newGoModData)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ goModChanged := oldReqs != newReqs
+ if goModChanged {
+ diagnostics = append(diagnostics, "go.mod: requirements are incomplete.\nRun 'go mod tidy' to add missing requirements.")
+ }
+
+ if !goModChanged {
+ newGoSumData, err := ioutil.ReadFile(filepath.Join(loadDir, "go.sum"))
+ if err != nil && !os.IsNotExist(err) {
+ return nil, nil, err
+ }
+ if !bytes.Equal(goSumData, newGoSumData) {
+ diagnostics = append(diagnostics, "go.sum: one or more sums are missing.\nRun 'go mod tidy' to add missing sums.")
+ }
+ }
+
+ return pkgs, diagnostics, nil
+}
+
+type packagePair struct {
+ base, release *packages.Package
+}
+
+// zipPackages combines two lists of packages, sorted by package path,
+// and returns a sorted list of pairs of packages with matching paths.
+// If a package is in one list but not the other (because it was added or
+// removed between releases), a pair will be returned with a nil
+// base or release field.
+func zipPackages(basePkgs, releasePkgs []*packages.Package) []packagePair {
+ baseIndex, releaseIndex := 0, 0
+ var pairs []packagePair
+ for baseIndex < len(basePkgs) || releaseIndex < len(releasePkgs) {
+ var basePkg, releasePkg *packages.Package
+ if baseIndex < len(basePkgs) {
+ basePkg = basePkgs[baseIndex]
+ }
+ if releaseIndex < len(releasePkgs) {
+ releasePkg = releasePkgs[releaseIndex]
+ }
+
+ var pair packagePair
+ if basePkg != nil && (releasePkg == nil || basePkg.PkgPath < releasePkg.PkgPath) {
+ // Package removed
+ pair = packagePair{basePkg, nil}
+ baseIndex++
+ } else if releasePkg != nil && (basePkg == nil || releasePkg.PkgPath < basePkg.PkgPath) {
+ // Package added
+ pair = packagePair{nil, releasePkg}
+ releaseIndex++
+ } else {
+ // Matched packages.
+ pair = packagePair{basePkg, releasePkg}
+ baseIndex++
+ releaseIndex++
+ }
+ pairs = append(pairs, pair)
+ }
+ return pairs
+}
diff --git a/cmd/gorelease/gorelease_test.go b/cmd/gorelease/gorelease_test.go
new file mode 100644
index 0000000..3e94866
--- /dev/null
+++ b/cmd/gorelease/gorelease_test.go
@@ -0,0 +1,327 @@
+// Copyright 2019 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"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/txtar"
+)
+
+var workDir string
+
+var (
+ testwork = flag.Bool("testwork", false, "preserve work directory")
+ updateGolden = flag.Bool("u", false, "update expected text in test files instead of failing")
+)
+
+func TestMain(m *testing.M) {
+ status := 1
+ defer func() {
+ if !*testwork && workDir != "" {
+ os.RemoveAll(workDir)
+ }
+ os.Exit(status)
+ }()
+
+ flag.Parse()
+
+ proxyDir, proxyURL, err := buildProxyDir()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return
+ }
+ os.Setenv("GOPROXY", proxyURL)
+ if *testwork {
+ fmt.Fprintf(os.Stderr, "test proxy dir: %s\ntest proxy URL: %s\n", proxyDir, proxyURL)
+ } else {
+ defer os.RemoveAll(proxyDir)
+ }
+
+ cacheDir, err := ioutil.TempDir("", "gorelease_test-gocache")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return
+ }
+ os.Setenv("GOPATH", cacheDir)
+ if *testwork {
+ fmt.Fprintf(os.Stderr, "test cache dir: %s\n", cacheDir)
+ } else {
+ defer func() {
+ if err := exec.Command("go", "clean", "-modcache").Run(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ if err := os.RemoveAll(cacheDir); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ }()
+ }
+
+ os.Setenv("GO111MODULE", "on")
+ os.Setenv("GOSUMDB", "off")
+
+ status = m.Run()
+}
+
+// test describes an individual test case, written as a .test file in the
+// testdata directory.
+//
+// Each test is a txtar archive (see golang.org/x/tools/txtar). The comment
+// section (before the first file) contains a sequence of key=value pairs
+// (one per line) that configure the test.
+//
+// Most tests include a file named "want". The output of gorelease is compared
+// against this file. If the -u flag is set, this file is replaced with the
+// actual output of gorelease, and the test is written back to disk. This is
+// useful for updating tests after cosmetic changes.
+type test struct {
+ txtar.Archive
+
+ // testPath is the name of the .test file describing the test.
+ testPath string
+
+ // modPath (set with mod=...) is the path of the module being tested. Used
+ // to retrieve files from the test proxy.
+ modPath string
+
+ // version (set with version=...) is the name of a version to check out
+ // from the test proxy into the working directory. Some tests use this
+ // instead of specifying files they need in the txtar archive.
+ version string
+
+ // baseVersion (set with base=...) is the value of the -base flag to pass
+ // to gorelease.
+ baseVersion string
+
+ // releaseVersion (set with version=...) is the value of the -version flag
+ // to pass to gorelease.
+ releaseVersion string
+
+ // dir (set with dir=...) is the directory where gorelease should be invoked.
+ // If unset, gorelease is invoked in the directory where the txtar archive
+ // is unpacked. This is useful for invoking gorelease in a subdirectory.
+ dir string
+
+ // wantError (set with error=...) is true if the test expects a hard error
+ // (returned by runRelease).
+ wantError bool
+
+ // wantSuccess (set with success=...) is true if the test expects a report
+ // to be returned without errors or diagnostics. True by default.
+ wantSuccess bool
+
+ // skip (set with skip=...) is non-empty if the test should be skipped.
+ skip string
+
+ // want is set to the contents of the file named "want" in the txtar archive.
+ want []byte
+}
+
+// readTest reads and parses a .test file with the given name.
+func readTest(testPath string) (*test, error) {
+ arc, err := txtar.ParseFile(testPath)
+ if err != nil {
+ return nil, err
+ }
+ t := &test{
+ Archive: *arc,
+ testPath: testPath,
+ wantSuccess: true,
+ }
+
+ for n, line := range bytes.Split(t.Comment, []byte("\n")) {
+ lineNum := n + 1
+ if i := bytes.IndexByte(line, '#'); i >= 0 {
+ line = line[:i]
+ }
+ line = bytes.TrimSpace(line)
+ if len(line) == 0 {
+ continue
+ }
+
+ var key, value string
+ if i := bytes.IndexByte(line, '='); i < 0 {
+ return nil, fmt.Errorf("%s:%d: no '=' found", testPath, lineNum)
+ } else {
+ key = strings.TrimSpace(string(line[:i]))
+ value = strings.TrimSpace(string(line[i+1:]))
+ }
+ switch key {
+ case "mod":
+ t.modPath = value
+ case "version":
+ t.version = value
+ case "base":
+ t.baseVersion = value
+ case "release":
+ t.releaseVersion = value
+ case "dir":
+ t.dir = value
+ case "skip":
+ t.skip = value
+ case "success":
+ t.wantSuccess, err = strconv.ParseBool(value)
+ if err != nil {
+ return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
+ }
+ case "error":
+ t.wantError, err = strconv.ParseBool(value)
+ if err != nil {
+ return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
+ }
+ default:
+ return nil, fmt.Errorf("%s:%d: unknown key: %q", testPath, lineNum, key)
+ }
+ }
+ if t.modPath == "" && (t.version != "" || (t.baseVersion != "" && t.baseVersion != "none")) {
+ return nil, fmt.Errorf("%s: version or base was set but mod was not set", testPath)
+ }
+
+ haveFiles := false
+ for _, f := range t.Files {
+ if f.Name == "want" {
+ t.want = bytes.TrimSpace(f.Data)
+ continue
+ }
+ haveFiles = true
+ }
+
+ if haveFiles && t.version != "" {
+ return nil, fmt.Errorf("%s: version is set but files are present", testPath)
+ }
+
+ return t, nil
+}
+
+// updateTest replaces the contents of the file named "want" within a test's
+// txtar archive, then formats and writes the test file.
+func updateTest(t *test, want []byte) error {
+ var wantFile *txtar.File
+ for i := range t.Files {
+ if t.Files[i].Name == "want" {
+ wantFile = &t.Files[i]
+ break
+ }
+ }
+ if wantFile == nil {
+ t.Files = append(t.Files, txtar.File{Name: "want"})
+ wantFile = &t.Files[len(t.Files)-1]
+ }
+
+ wantFile.Data = want
+ data := txtar.Format(&t.Archive)
+ return ioutil.WriteFile(t.testPath, data, 0666)
+}
+
+func TestRelease(t *testing.T) {
+ testPaths, err := filepath.Glob(filepath.FromSlash("testdata/*/*.test"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(testPaths) == 0 {
+ t.Fatal("no .test files found in testdata directory")
+ }
+
+ for _, testPath := range testPaths {
+ testPath := testPath
+ testName := strings.TrimSuffix(strings.TrimPrefix(filepath.ToSlash(testPath), "testdata/"), ".test")
+ t.Run(testName, func(t *testing.T) {
+ t.Parallel()
+
+ test, err := readTest(testPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if test.skip != "" {
+ t.Skip(test.skip)
+ }
+
+ // Extract the files in the release version. They may be part of the
+ // test archive or in testdata/mod.
+ testDir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *testwork {
+ fmt.Fprintf(os.Stderr, "test dir: %s\n", testDir)
+ } else {
+ defer os.RemoveAll(testDir)
+ }
+
+ var arc *txtar.Archive
+ if test.version != "" {
+ arcBase := fmt.Sprintf("%s_%s.txt", strings.ReplaceAll(test.modPath, "/", "_"), test.version)
+ arcPath := filepath.Join("testdata/mod", arcBase)
+ var err error
+ arc, err = txtar.ParseFile(arcPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ arc = &test.Archive
+ }
+ if err := extractTxtar(testDir, arc); err != nil {
+ t.Fatal(err)
+ }
+
+ // Generate the report and compare it against the expected text.
+ var args []string
+ if test.baseVersion != "" {
+ args = append(args, "-base="+test.baseVersion)
+ }
+ if test.releaseVersion != "" {
+ args = append(args, "-version="+test.releaseVersion)
+ }
+ buf := &bytes.Buffer{}
+ releaseDir := filepath.Join(testDir, test.dir)
+ success, err := runRelease(buf, releaseDir, args)
+ if err != nil {
+ if !test.wantError {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if errMsg := []byte(err.Error()); !bytes.Equal(errMsg, bytes.TrimSpace(test.want)) {
+ if *updateGolden {
+ if err := updateTest(test, errMsg); err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ t.Fatalf("got error: %s; want error: %s", errMsg, test.want)
+ }
+ }
+ return
+ }
+ if test.wantError {
+ t.Fatalf("got success; want error %s", test.want)
+ }
+
+ got := bytes.TrimSpace(buf.Bytes())
+ if filepath.Separator != '/' {
+ got = bytes.ReplaceAll(got, []byte{filepath.Separator}, []byte{'/'})
+ }
+ if !bytes.Equal(got, test.want) {
+ if *updateGolden {
+ if err := updateTest(test, got); err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ t.Fatalf("got:\n%s\n\nwant:\n%s", got, test.want)
+ }
+ }
+ if success != test.wantSuccess {
+ t.Fatalf("got success: %v; want success %v", success, test.wantSuccess)
+ }
+ })
+ }
+}
diff --git a/cmd/gorelease/proxy_test.go b/cmd/gorelease/proxy_test.go
new file mode 100644
index 0000000..8aaebea
--- /dev/null
+++ b/cmd/gorelease/proxy_test.go
@@ -0,0 +1,182 @@
+// Copyright 2019 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"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
+ "golang.org/x/mod/zip"
+ "golang.org/x/tools/txtar"
+)
+
+// buildProxyDir constructs a temporary directory suitable for use as a
+// module proxy with a file:// URL. The caller is responsible for deleting
+// the directory when it's no longer needed.
+func buildProxyDir() (proxyDir, proxyURL string, err error) {
+ proxyDir, err = ioutil.TempDir("", "gorelease-proxy")
+ if err != nil {
+ return "", "", err
+ }
+ defer func(proxyDir string) {
+ if err != nil {
+ os.RemoveAll(proxyDir)
+ }
+ }(proxyDir)
+
+ txtarPaths, err := filepath.Glob(filepath.FromSlash("testdata/mod/*.txt"))
+ if err != nil {
+ return "", "", err
+ }
+ versionLists := make(map[string][]string)
+ for _, txtarPath := range txtarPaths {
+ base := filepath.Base(txtarPath)
+ stem := base[:len(base)-len(".txt")]
+ i := strings.LastIndexByte(base, '_')
+ if i < 0 {
+ return "", "", fmt.Errorf("invalid module archive: %s", base)
+ }
+ modPath := strings.ReplaceAll(stem[:i], "_", "/")
+ version := stem[i+1:]
+ versionLists[modPath] = append(versionLists[modPath], version)
+
+ modDir := filepath.Join(proxyDir, modPath, "@v")
+ if err := os.MkdirAll(modDir, 0777); err != nil {
+ return "", "", err
+ }
+
+ arc, err := txtar.ParseFile(txtarPath)
+ if err != nil {
+ return "", "", err
+ }
+
+ isCanonical := version == module.CanonicalVersion(version)
+ var zipContents []zip.File
+ var haveInfo, haveMod bool
+ var goMod txtar.File
+ for _, af := range arc.Files {
+ if !isCanonical && af.Name != ".info" {
+ return "", "", fmt.Errorf("%s: version is non-canonical but contains files other than .info", txtarPath)
+ }
+ if af.Name == ".info" || af.Name == ".mod" {
+ if af.Name == ".info" {
+ haveInfo = true
+ } else {
+ haveMod = true
+ }
+ outPath := filepath.Join(modDir, version+af.Name)
+ if err := ioutil.WriteFile(outPath, af.Data, 0666); err != nil {
+ return "", "", err
+ }
+ continue
+ }
+ if af.Name == "go.mod" {
+ goMod = af
+ }
+
+ zipContents = append(zipContents, txtarFile{af})
+ }
+ if !isCanonical && !haveInfo {
+ return "", "", fmt.Errorf("%s: version is non-canonical but does not have .info", txtarPath)
+ }
+
+ if !haveInfo {
+ outPath := filepath.Join(modDir, version+".info")
+ outContent := fmt.Sprintf(`{"Version":"%s"}`, version)
+ if err := ioutil.WriteFile(outPath, []byte(outContent), 0666); err != nil {
+ return "", "", err
+ }
+ }
+ if !haveMod && goMod.Name != "" {
+ outPath := filepath.Join(modDir, version+".mod")
+ if err := ioutil.WriteFile(outPath, goMod.Data, 0666); err != nil {
+ return "", "", err
+ }
+ }
+
+ if len(zipContents) > 0 {
+ zipPath := filepath.Join(modDir, version+".zip")
+ zipFile, err := os.Create(zipPath)
+ if err != nil {
+ return "", "", err
+ }
+ defer zipFile.Close()
+ if err := zip.Create(zipFile, module.Version{Path: modPath, Version: version}, zipContents); err != nil {
+ return "", "", err
+ }
+ if err := zipFile.Close(); err != nil {
+ return "", "", err
+ }
+ }
+ }
+
+ buf := &bytes.Buffer{}
+ for modPath, versions := range versionLists {
+ outPath := filepath.Join(proxyDir, modPath, "@v", "list")
+ sort.Slice(versions, func(i, j int) bool {
+ return semver.Compare(versions[i], versions[j]) < 0
+ })
+ for _, v := range versions {
+ fmt.Fprintln(buf, v)
+ }
+ if err := ioutil.WriteFile(outPath, buf.Bytes(), 0666); err != nil {
+ return "", "", err
+ }
+ buf.Reset()
+ }
+
+ // Make sure the URL path starts with a slash on Windows. Absolute paths
+ // normally start with a drive letter.
+ // TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
+ if strings.HasPrefix(proxyDir, "/") {
+ proxyURL = "file://" + proxyDir
+ } else {
+ proxyURL = "file:///" + filepath.FromSlash(proxyDir)
+ }
+ return proxyDir, proxyURL, nil
+}
+
+type txtarFile struct {
+ f txtar.File
+}
+
+func (f txtarFile) Path() string { return f.f.Name }
+func (f txtarFile) Lstat() (os.FileInfo, error) { return txtarFileInfo{f.f}, nil }
+func (f txtarFile) Open() (io.ReadCloser, error) {
+ return ioutil.NopCloser(bytes.NewReader(f.f.Data)), nil
+}
+
+type txtarFileInfo struct {
+ f txtar.File
+}
+
+func (f txtarFileInfo) Name() string { return f.f.Name }
+func (f txtarFileInfo) Size() int64 { return int64(len(f.f.Data)) }
+func (f txtarFileInfo) Mode() os.FileMode { return 0444 }
+func (f txtarFileInfo) ModTime() time.Time { return time.Time{} }
+func (f txtarFileInfo) IsDir() bool { return false }
+func (f txtarFileInfo) Sys() interface{} { return nil }
+
+func extractTxtar(destDir string, arc *txtar.Archive) error {
+ for _, f := range arc.Files {
+ outPath := filepath.Join(destDir, f.Name)
+ if err := os.MkdirAll(filepath.Dir(outPath), 0777); err != nil {
+ return err
+ }
+ if err := ioutil.WriteFile(outPath, f.Data, 0666); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/cmd/gorelease/report.go b/cmd/gorelease/report.go
new file mode 100644
index 0000000..30943db
--- /dev/null
+++ b/cmd/gorelease/report.go
@@ -0,0 +1,344 @@
+// Copyright 2019 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"
+ "io"
+ "strings"
+
+ "golang.org/x/exp/apidiff"
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
+ "golang.org/x/tools/go/packages"
+)
+
+// report describes the differences in the public API between two versions
+// of a module.
+type report struct {
+ // modulePath is the name of the module.
+ modulePath string
+
+ // baseVersion is the "old" version of the module to compare against.
+ // It may be empty if there is no base version (for example, if this is
+ // the first release).
+ baseVersion string
+
+ // releaseVersion is the version of the module to release, either
+ // proposed with -version or inferred with suggestVersion.
+ releaseVersion string
+
+ // releaseVersionInferred is true if the release version was suggested
+ // (not specified with -version).
+ releaseVersionInferred bool
+
+ // tagPrefix is the prefix for VCS tags for this module. For example,
+ // if the module is defined in "foo/bar/v2/go.mod", tagPrefix will be
+ // "foo/bar/".
+ tagPrefix string
+
+ // packages is a list of package reports, describing the differences
+ // for individual packages, sorted by package path.
+ packages []packageReport
+
+ // diagnostics is a list of problems unrelated to the module API.
+ // For example, if go.mod is missing some requirements, that will be
+ // reported here.
+ diagnostics []string
+
+ // versionInvalid explains why the proposed or suggested version is not valid.
+ versionInvalid *versionMessage
+
+ // haveCompatibleChanges is true if there are any backward-compatible
+ // changes in non-internal packages.
+ haveCompatibleChanges bool
+
+ // haveIncompatibleChanges is true if there are any backward-incompatible
+ // changes in non-internal packages.
+ haveIncompatibleChanges bool
+
+ // haveBaseErrors is true if there were errors loading packages
+ // in the base version.
+ haveBaseErrors bool
+
+ // haveReleaseErrors is true if there were errors loading packages
+ // in the release version.
+ haveReleaseErrors bool
+}
+
+// Text formats and writes a report to w. The report lists errors, compatible
+// changes, and incompatible changes in each package. If releaseVersion is set,
+// it states whether releaseVersion is valid (and why). If releaseVersion is not
+// set, it suggests a new version.
+func (r *report) Text(w io.Writer) error {
+ buf := &bytes.Buffer{}
+ for _, p := range r.packages {
+ if err := p.Text(buf); err != nil {
+ return err
+ }
+ }
+
+ if len(r.diagnostics) > 0 {
+ for _, d := range r.diagnostics {
+ fmt.Fprintln(buf, d)
+ }
+ } else if r.versionInvalid != nil {
+ fmt.Fprintln(buf, r.versionInvalid)
+ } else if r.releaseVersionInferred {
+ if r.tagPrefix == "" {
+ fmt.Fprintf(buf, "Suggested version: %s\n", r.releaseVersion)
+ } else {
+ fmt.Fprintf(buf, "Suggested version: %[1]s (with tag %[2]s%[1]s)\n", r.releaseVersion, r.tagPrefix)
+ }
+ } else {
+ if r.tagPrefix == "" {
+ fmt.Fprintf(buf, "%s is a valid semantic version for this release.\n", r.releaseVersion)
+ } else {
+ fmt.Fprintf(buf, "%[1]s (with tag %[2]s%[1]s) is a valid semantic version for this release\n", r.releaseVersion, r.tagPrefix)
+ }
+ }
+
+ if r.versionInvalid == nil && r.haveBaseErrors {
+ fmt.Fprintln(buf, "Errors were found in the base version. Some API changes may be omitted.")
+ }
+
+ _, err := io.Copy(w, buf)
+ return err
+}
+
+func (r *report) addPackage(p packageReport) {
+ r.packages = append(r.packages, p)
+ if len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
+ // Only count compatible and incompatible changes if there were no errors.
+ // When there are errors, definitions may be missing, and fixes may appear
+ // incompatible when they are not. Changes will still be reported, but
+ // they won't affect version validation or suggestions.
+ for _, c := range p.Changes {
+ if !c.Compatible && len(p.releaseErrors) == 0 {
+ r.haveIncompatibleChanges = true
+ } else if c.Compatible && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
+ r.haveCompatibleChanges = true
+ }
+ }
+ }
+ if len(p.baseErrors) > 0 {
+ r.haveBaseErrors = true
+ }
+ if len(p.releaseErrors) > 0 {
+ r.haveReleaseErrors = true
+ }
+}
+
+// validateVersion checks whether r.releaseVersion is valid.
+// If r.releaseVersion is not valid, an error is returned explaining why.
+// r.releaseVersion must be set.
+func (r *report) validateVersion() {
+ if r.releaseVersion == "" {
+ panic("validateVersion called without version")
+ }
+ setNotValid := func(format string, args ...interface{}) {
+ r.versionInvalid = &versionMessage{
+ message: fmt.Sprintf("%s is not a valid semantic version for this release.", r.releaseVersion),
+ reason: fmt.Sprintf(format, args...),
+ }
+ }
+
+ if r.haveReleaseErrors {
+ if r.haveReleaseErrors {
+ setNotValid("Errors were found in one or more packages.")
+ return
+ }
+ }
+
+ // TODO(jayconrod): link to documentation for all of these errors.
+
+ // Check that the major version matches the module path.
+ _, suffix, ok := module.SplitPathVersion(r.modulePath)
+ if !ok {
+ setNotValid("%s: could not find version suffix in module path", r.modulePath)
+ return
+ }
+ if suffix != "" {
+ if suffix[0] != '/' && suffix[0] != '.' {
+ setNotValid("%s: unknown module path version suffix: %q", r.modulePath, suffix)
+ return
+ }
+ pathMajor := suffix[1:]
+ major := semver.Major(r.releaseVersion)
+ if pathMajor != major {
+ setNotValid(`The major version %s does not match the major version suffix
+in the module path: %s`, major, r.modulePath)
+ return
+ }
+ } else if major := semver.Major(r.releaseVersion); major != "v0" && major != "v1" {
+ setNotValid(`The module path does not end with the major version suffix /%s,
+which is required for major versions v2 or greater.`, major)
+ return
+ }
+
+ // Check that compatible / incompatible changes are consistent.
+ if semver.Major(r.baseVersion) == "v0" {
+ return
+ }
+ if r.haveIncompatibleChanges {
+ setNotValid("There are incompatible changes.")
+ return
+ }
+ if r.haveCompatibleChanges && semver.MajorMinor(r.baseVersion) == semver.MajorMinor(r.releaseVersion) {
+ setNotValid(`There are compatible changes, but the minor version is not incremented
+over the base version (%s).`, r.baseVersion)
+ return
+ }
+}
+
+// suggestVersion suggests a new version consistent with observed changes.
+func (r *report) suggestVersion() {
+ setNotValid := func(format string, args ...interface{}) {
+ r.versionInvalid = &versionMessage{
+ message: "Cannot suggest a release version.",
+ reason: fmt.Sprintf(format, args...),
+ }
+ }
+ setVersion := func(v string) {
+ r.releaseVersion = v
+ r.releaseVersionInferred = true
+ }
+
+ if r.haveReleaseErrors || r.haveBaseErrors {
+ setNotValid("Errors were found.")
+ return
+ }
+
+ var major, minor, patch, pre string
+ if r.baseVersion != "none" {
+ var err error
+ major, minor, patch, pre, _, err = parseVersion(r.baseVersion)
+ if err != nil {
+ panic(fmt.Sprintf("could not parse base version: %v", err))
+ }
+ }
+
+ if r.haveIncompatibleChanges && r.baseVersion != "none" && pre == "" && major != "0" {
+ setNotValid("Incompatible changes were detected.")
+ return
+ // TODO(jayconrod): briefly explain how to prepare major version releases
+ // and link to documentation.
+ }
+
+ if r.baseVersion == "none" {
+ if _, pathMajor, ok := module.SplitPathVersion(r.modulePath); !ok {
+ panic(fmt.Sprintf("could not parse module path %q", r.modulePath))
+ } else if pathMajor == "" {
+ setVersion("v0.1.0")
+ } else {
+ setVersion(pathMajor[1:] + ".0.0")
+ }
+ return
+ }
+
+ if pre != "" {
+ // suggest non-prerelease version
+ } else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") {
+ minor = incDecimal(minor)
+ patch = "0"
+ } else {
+ patch = incDecimal(patch)
+ }
+ setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch))
+}
+
+// isSuccessful returns true the module appears to be safe to release at the
+// proposed or suggested version.
+func (r *report) isSuccessful() bool {
+ return len(r.diagnostics) == 0 && r.versionInvalid == nil
+}
+
+type versionMessage struct {
+ message, reason string
+}
+
+func (m versionMessage) String() string {
+ return m.message + "\n" + m.reason + "\n"
+}
+
+// incDecimal returns the decimal string incremented by 1.
+func incDecimal(decimal string) string {
+ // Scan right to left turning 9s to 0s until you find a digit to increment.
+ digits := []byte(decimal)
+ i := len(digits) - 1
+ for ; i >= 0 && digits[i] == '9'; i-- {
+ digits[i] = '0'
+ }
+ if i >= 0 {
+ digits[i]++
+ } else {
+ // digits is all zeros
+ digits[0] = '1'
+ digits = append(digits, '0')
+ }
+ return string(digits)
+}
+
+type packageReport struct {
+ apidiff.Report
+ path string
+ baseErrors, releaseErrors []packages.Error
+}
+
+func (p *packageReport) Text(w io.Writer) error {
+ if len(p.Changes) == 0 && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
+ return nil
+ }
+ buf := &bytes.Buffer{}
+ fmt.Fprintf(buf, "%s\n%s\n", p.path, strings.Repeat("-", len(p.path)))
+ if len(p.baseErrors) > 0 {
+ fmt.Fprintf(buf, "errors in base version:\n")
+ for _, e := range p.baseErrors {
+ fmt.Fprintf(buf, "\t%v\n", e)
+ }
+ buf.WriteByte('\n')
+ }
+ if len(p.releaseErrors) > 0 {
+ fmt.Fprintf(buf, "errors in release version:\n")
+ for _, e := range p.releaseErrors {
+ fmt.Fprintf(buf, "\t%v\n", e)
+ }
+ buf.WriteByte('\n')
+ }
+ if len(p.Changes) > 0 {
+ if err := p.Report.Text(buf); err != nil {
+ return err
+ }
+ buf.WriteByte('\n')
+ }
+ _, err := io.Copy(w, buf)
+ return err
+}
+
+// parseVersion returns the major, minor, and patch numbers, prerelease text,
+// and metadata for a given version.
+//
+// TODO(jayconrod): extend semver to do this and delete this function.
+func parseVersion(vers string) (major, minor, patch, pre, meta string, err error) {
+ if !strings.HasPrefix(vers, "v") {
+ return "", "", "", "", "", fmt.Errorf("version %q does not start with 'v'", vers)
+ }
+ base := vers[1:]
+ if i := strings.IndexByte(base, '+'); i >= 0 {
+ meta = base[i+1:]
+ base = base[:i]
+ }
+ if i := strings.IndexByte(base, '-'); i >= 0 {
+ pre = base[i+1:]
+ base = base[:i]
+ }
+ parts := strings.Split(base, ".")
+ if len(parts) != 3 {
+ return "", "", "", "", "", fmt.Errorf("version %q should have three numbers", vers)
+ }
+ major, minor, patch = parts[0], parts[1], parts[2]
+ return major, minor, patch, pre, meta, nil
+}
diff --git a/cmd/gorelease/testdata/basic/README.txt b/cmd/gorelease/testdata/basic/README.txt
new file mode 100644
index 0000000..708a0c1
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/README.txt
@@ -0,0 +1,11 @@
+Module example.com/basic tests basic functionality of gorelease.
+It verifies that versions are correctly suggested or verified after
+various changes.
+
+All revisions are stored in the mod directory. The same series of
+changes is made across three major versions, v0, v1, and v2:
+
+vX.0.1 - simple package
+vX.1.0 - compatible change: add a function and a package
+vX.1.1 - internal change: function returns different value
+vX.1.2 - incompatible change: delete a function and a package
diff --git a/cmd/gorelease/testdata/basic/v0_compatible_suggest.test b/cmd/gorelease/testdata/basic/v0_compatible_suggest.test
new file mode 100644
index 0000000..32029f1
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_compatible_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/basic
+version=v0.1.0
+base=v0.0.1
+-- want --
+example.com/basic/a
+-------------------
+Compatible changes:
+- A2: added
+
+example.com/basic/b
+-------------------
+Compatible changes:
+- package added
+
+Suggested version: v0.1.0
diff --git a/cmd/gorelease/testdata/basic/v0_compatible_verify.test b/cmd/gorelease/testdata/basic/v0_compatible_verify.test
new file mode 100644
index 0000000..8324045
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_compatible_verify.test
@@ -0,0 +1,16 @@
+mod=example.com/basic
+version=v0.1.0
+base=v0.0.1
+release=v0.1.0
+-- want --
+example.com/basic/a
+-------------------
+Compatible changes:
+- A2: added
+
+example.com/basic/b
+-------------------
+Compatible changes:
+- package added
+
+v0.1.0 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v0_incompatible_suggest.test b/cmd/gorelease/testdata/basic/v0_incompatible_suggest.test
new file mode 100644
index 0000000..312b6b3
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_incompatible_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/basic
+version=v0.1.2
+base=v0.1.1
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+Suggested version: v0.2.0
diff --git a/cmd/gorelease/testdata/basic/v0_incompatible_verify.test b/cmd/gorelease/testdata/basic/v0_incompatible_verify.test
new file mode 100644
index 0000000..4784e3d
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_incompatible_verify.test
@@ -0,0 +1,16 @@
+mod=example.com/basic
+version=v0.1.2
+base=v0.1.1
+release=v0.1.2
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+v0.1.2 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v0_nobase_suggest.test b/cmd/gorelease/testdata/basic/v0_nobase_suggest.test
new file mode 100644
index 0000000..8f17473
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_nobase_suggest.test
@@ -0,0 +1,5 @@
+mod=example.com/basic
+version=v0.1.1
+base=none
+-- want --
+Suggested version: v0.1.0
diff --git a/cmd/gorelease/testdata/basic/v0_patch_suggest.test b/cmd/gorelease/testdata/basic/v0_patch_suggest.test
new file mode 100644
index 0000000..cee5349
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_patch_suggest.test
@@ -0,0 +1,5 @@
+mod=example.com/basic
+version=v0.1.1
+base=v0.1.0
+-- want --
+Suggested version: v0.1.1
diff --git a/cmd/gorelease/testdata/basic/v0_patch_verify.test b/cmd/gorelease/testdata/basic/v0_patch_verify.test
new file mode 100644
index 0000000..a641f8c
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_patch_verify.test
@@ -0,0 +1,6 @@
+mod=example.com/basic
+version=v0.1.1
+base=v0.1.0
+release=v0.1.1
+-- want --
+v0.1.1 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v0_pre_suggest.test b/cmd/gorelease/testdata/basic/v0_pre_suggest.test
new file mode 100644
index 0000000..2c09e5f
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_pre_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/basic
+version=v0.1.2
+base=v0.1.1-pre
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+Suggested version: v0.1.1
diff --git a/cmd/gorelease/testdata/basic/v0_v1_incompatible_verify.test b/cmd/gorelease/testdata/basic/v0_v1_incompatible_verify.test
new file mode 100644
index 0000000..1eb08c0
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v0_v1_incompatible_verify.test
@@ -0,0 +1,16 @@
+mod=example.com/basic
+version=v0.1.2
+base=v0.1.1
+release=v1.0.0
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+v1.0.0 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v1_compatible_suggest.test b/cmd/gorelease/testdata/basic/v1_compatible_suggest.test
new file mode 100644
index 0000000..fb27db1
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_compatible_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/basic
+version=v1.1.0
+base=v1.0.1
+-- want --
+example.com/basic/a
+-------------------
+Compatible changes:
+- A2: added
+
+example.com/basic/b
+-------------------
+Compatible changes:
+- package added
+
+Suggested version: v1.1.0
diff --git a/cmd/gorelease/testdata/basic/v1_compatible_verify.test b/cmd/gorelease/testdata/basic/v1_compatible_verify.test
new file mode 100644
index 0000000..8c98ee1
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_compatible_verify.test
@@ -0,0 +1,16 @@
+mod=example.com/basic
+version=v1.1.0
+base=v1.0.1
+release=v1.1.0
+-- want --
+example.com/basic/a
+-------------------
+Compatible changes:
+- A2: added
+
+example.com/basic/b
+-------------------
+Compatible changes:
+- package added
+
+v1.1.0 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v1_incompatible_suggest.test b/cmd/gorelease/testdata/basic/v1_incompatible_suggest.test
new file mode 100644
index 0000000..49b5eaa
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_incompatible_suggest.test
@@ -0,0 +1,17 @@
+mod=example.com/basic
+version=v1.1.2
+base=v1.1.1
+success=false
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+Cannot suggest a release version.
+Incompatible changes were detected.
diff --git a/cmd/gorelease/testdata/basic/v1_incompatible_verify.test b/cmd/gorelease/testdata/basic/v1_incompatible_verify.test
new file mode 100644
index 0000000..e5d6c4d
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_incompatible_verify.test
@@ -0,0 +1,18 @@
+mod=example.com/basic
+version=v1.1.2
+base=v1.1.1
+success=false
+release=v1.1.2
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+v1.1.2 is not a valid semantic version for this release.
+There are incompatible changes.
diff --git a/cmd/gorelease/testdata/basic/v1_patch_suggest.test b/cmd/gorelease/testdata/basic/v1_patch_suggest.test
new file mode 100644
index 0000000..a0effa1
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_patch_suggest.test
@@ -0,0 +1,5 @@
+mod=example.com/basic
+version=v1.1.1
+base=v1.1.0
+-- want --
+Suggested version: v1.1.1
diff --git a/cmd/gorelease/testdata/basic/v1_patch_verify.test b/cmd/gorelease/testdata/basic/v1_patch_verify.test
new file mode 100644
index 0000000..64c734b
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_patch_verify.test
@@ -0,0 +1,6 @@
+mod=example.com/basic
+version=v1.1.1
+base=v1.1.0
+release=v1.1.1
+-- want --
+v1.1.1 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v1_pre_suggest.test b/cmd/gorelease/testdata/basic/v1_pre_suggest.test
new file mode 100644
index 0000000..de0f2c0
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_pre_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/basic
+version=v1.1.2
+base=v1.1.1-pre
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+Suggested version: v1.1.1
diff --git a/cmd/gorelease/testdata/basic/v1_v2_incompatible_verify.test b/cmd/gorelease/testdata/basic/v1_v2_incompatible_verify.test
new file mode 100644
index 0000000..2017c87
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_v2_incompatible_verify.test
@@ -0,0 +1,19 @@
+mod=example.com/basic
+version=v1.1.2
+base=v1.1.1
+release=v2.0.0
+success=false
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/b
+-------------------
+Incompatible changes:
+- package removed
+
+v2.0.0 is not a valid semantic version for this release.
+The module path does not end with the major version suffix /v2,
+which is required for major versions v2 or greater.
diff --git a/cmd/gorelease/testdata/basic/v1_v2_incompatible_verify_suffix.test b/cmd/gorelease/testdata/basic/v1_v2_incompatible_verify_suffix.test
new file mode 100644
index 0000000..9d6a303
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v1_v2_incompatible_verify_suffix.test
@@ -0,0 +1,7 @@
+mod=example.com/basic/v2
+version=v2.1.2
+base=v1.1.1
+release=v2.0.0
+error=1
+-- want --
+can't compare major versions: base version v1.1.1 does not belong to module example.com/basic/v2
diff --git a/cmd/gorelease/testdata/basic/v2_compatible_suggest.test b/cmd/gorelease/testdata/basic/v2_compatible_suggest.test
new file mode 100644
index 0000000..a421c56
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_compatible_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/basic/v2
+version=v2.1.0
+base=v2.0.1
+-- want --
+example.com/basic/v2/a
+----------------------
+Compatible changes:
+- A2: added
+
+example.com/basic/v2/b
+----------------------
+Compatible changes:
+- package added
+
+Suggested version: v2.1.0
diff --git a/cmd/gorelease/testdata/basic/v2_compatible_verify.test b/cmd/gorelease/testdata/basic/v2_compatible_verify.test
new file mode 100644
index 0000000..ac138a7
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_compatible_verify.test
@@ -0,0 +1,16 @@
+mod=example.com/basic/v2
+version=v2.1.0
+base=v2.0.1
+release=v2.1.0
+-- want --
+example.com/basic/v2/a
+----------------------
+Compatible changes:
+- A2: added
+
+example.com/basic/v2/b
+----------------------
+Compatible changes:
+- package added
+
+v2.1.0 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v2_incompatible_suggest.test b/cmd/gorelease/testdata/basic/v2_incompatible_suggest.test
new file mode 100644
index 0000000..694892c
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_incompatible_suggest.test
@@ -0,0 +1,17 @@
+mod=example.com/basic/v2
+version=v2.1.2
+base=v2.1.1
+success=false
+-- want --
+example.com/basic/v2/a
+----------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/v2/b
+----------------------
+Incompatible changes:
+- package removed
+
+Cannot suggest a release version.
+Incompatible changes were detected.
diff --git a/cmd/gorelease/testdata/basic/v2_incompatible_verify.test b/cmd/gorelease/testdata/basic/v2_incompatible_verify.test
new file mode 100644
index 0000000..8ea2095
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_incompatible_verify.test
@@ -0,0 +1,18 @@
+mod=example.com/basic/v2
+version=v2.1.2
+base=v2.1.1
+success=false
+release=v2.1.2
+-- want --
+example.com/basic/v2/a
+----------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/v2/b
+----------------------
+Incompatible changes:
+- package removed
+
+v2.1.2 is not a valid semantic version for this release.
+There are incompatible changes.
diff --git a/cmd/gorelease/testdata/basic/v2_nobase_suggest.test b/cmd/gorelease/testdata/basic/v2_nobase_suggest.test
new file mode 100644
index 0000000..957f82d
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_nobase_suggest.test
@@ -0,0 +1,5 @@
+mod=example.com/basic/v2
+version=v2.1.1
+base=none
+-- want --
+Suggested version: v2.0.0
diff --git a/cmd/gorelease/testdata/basic/v2_patch_suggest.test b/cmd/gorelease/testdata/basic/v2_patch_suggest.test
new file mode 100644
index 0000000..1416690
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_patch_suggest.test
@@ -0,0 +1,5 @@
+mod=example.com/basic/v2
+version=v2.1.1
+base=v2.1.0
+-- want --
+Suggested version: v2.1.1
diff --git a/cmd/gorelease/testdata/basic/v2_patch_verify.test b/cmd/gorelease/testdata/basic/v2_patch_verify.test
new file mode 100644
index 0000000..4aead44
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_patch_verify.test
@@ -0,0 +1,6 @@
+mod=example.com/basic/v2
+version=v2.1.1
+base=v2.1.0
+release=v2.1.1
+-- want --
+v2.1.1 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v2_pre_suggest.test b/cmd/gorelease/testdata/basic/v2_pre_suggest.test
new file mode 100644
index 0000000..c786dcb
--- /dev/null
+++ b/cmd/gorelease/testdata/basic/v2_pre_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/basic/v2
+version=v2.1.2
+base=v2.1.1-pre
+-- want --
+example.com/basic/v2/a
+----------------------
+Incompatible changes:
+- A2: removed
+
+example.com/basic/v2/b
+----------------------
+Incompatible changes:
+- package removed
+
+Suggested version: v2.1.1
diff --git a/cmd/gorelease/testdata/cgo/README.txt b/cmd/gorelease/testdata/cgo/README.txt
new file mode 100644
index 0000000..8bbf9c6
--- /dev/null
+++ b/cmd/gorelease/testdata/cgo/README.txt
@@ -0,0 +1,7 @@
+Module example.com/cgo is used to test that packages with cgo code
+can be loaded without errors when cgo is enabled.
+
+TODO(jayconrod): test modules with cgo-only and cgo / pure Go implementations
+with CGO_ENABLED=0 and 1. But first, decide how multiple platforms and
+build constraints should be handled. Currently, gorelease only considers
+the same configuration as 'go list'.
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/cgo/cgo.test b/cmd/gorelease/testdata/cgo/cgo.test
new file mode 100644
index 0000000..4152bf5
--- /dev/null
+++ b/cmd/gorelease/testdata/cgo/cgo.test
@@ -0,0 +1,15 @@
+base=none
+release=v1.0.0
+-- go.mod --
+module example.com/cgo
+
+go 1.13
+-- c.go --
+package cgo
+
+// const int x = 12;
+import "C"
+
+func X() int { return int(C.x) }
+-- want --
+v1.0.0 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/empty/README.txt b/cmd/gorelease/testdata/empty/README.txt
new file mode 100644
index 0000000..283befa
--- /dev/null
+++ b/cmd/gorelease/testdata/empty/README.txt
@@ -0,0 +1,2 @@
+Module example.com/empty is used to test that gorelease works
+in a module with no packages.
diff --git a/cmd/gorelease/testdata/empty/empty.test b/cmd/gorelease/testdata/empty/empty.test
new file mode 100644
index 0000000..c87fd6b
--- /dev/null
+++ b/cmd/gorelease/testdata/empty/empty.test
@@ -0,0 +1,6 @@
+mod=example.com/empty
+base=v0.0.1
+version=v0.0.2
+release=v0.0.2
+-- want --
+v0.0.2 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/errors/README.txt b/cmd/gorelease/testdata/errors/README.txt
new file mode 100644
index 0000000..071708a
--- /dev/null
+++ b/cmd/gorelease/testdata/errors/README.txt
@@ -0,0 +1,2 @@
+Tests in this directory check that user errors invoking gorelease
+are correctly reported.
diff --git a/cmd/gorelease/testdata/errors/bad_base.test b/cmd/gorelease/testdata/errors/bad_base.test
new file mode 100644
index 0000000..d03f493
--- /dev/null
+++ b/cmd/gorelease/testdata/errors/bad_base.test
@@ -0,0 +1,8 @@
+mod=example.com/errors
+base=master
+error=true
+
+-- want --
+usage: gorelease -base=version [-version=version]
+base version "master" is not a canonical semantic version
+For more information, run go doc golang.org/x/exp/cmd/gorelease
diff --git a/cmd/gorelease/testdata/errors/bad_release.test b/cmd/gorelease/testdata/errors/bad_release.test
new file mode 100644
index 0000000..46868a2
--- /dev/null
+++ b/cmd/gorelease/testdata/errors/bad_release.test
@@ -0,0 +1,9 @@
+mod=example.com/errors
+base=v0.1.0
+release=master
+error=true
+
+-- want --
+usage: gorelease -base=version [-version=version]
+release version "master" is not a canonical semantic version
+For more information, run go doc golang.org/x/exp/cmd/gorelease
diff --git a/cmd/gorelease/testdata/errors/base_higher.test b/cmd/gorelease/testdata/errors/base_higher.test
new file mode 100644
index 0000000..e5cb36c
--- /dev/null
+++ b/cmd/gorelease/testdata/errors/base_higher.test
@@ -0,0 +1,9 @@
+mod=example.com/errors
+base=v0.2.0
+release=v0.1.0
+error=true
+
+-- want --
+usage: gorelease -base=version [-version=version]
+base version ("v0.2.0") must be lower than release version ("v0.1.0")
+For more information, run go doc golang.org/x/exp/cmd/gorelease
diff --git a/cmd/gorelease/testdata/errors/errors.test b/cmd/gorelease/testdata/errors/errors.test
new file mode 100644
index 0000000..c5d5711
--- /dev/null
+++ b/cmd/gorelease/testdata/errors/errors.test
@@ -0,0 +1,41 @@
+mod=example.com/errors
+version=v0.2.0
+base=v0.1.0
+release=v0.2.0
+success=false
+
+-- want --
+example.com/errors/added
+------------------------
+errors in release version:
+ added/added.go:3:15: cannot convert "not an int" (untyped string constant) to int
+
+Compatible changes:
+- package added
+
+example.com/errors/broken
+-------------------------
+errors in release version:
+ broken/broken.go:3:15: cannot convert "no longer an int" (untyped string constant) to int
+
+Incompatible changes:
+- X: value changed from 12 to unknown
+
+example.com/errors/deleted
+--------------------------
+errors in base version:
+ deleted/deleted.go:3:15: cannot convert "not an int" (untyped string constant) to int
+
+Incompatible changes:
+- package removed
+
+example.com/errors/fixed
+------------------------
+errors in base version:
+ fixed/fixed.go:3:15: cannot convert "not an int" (untyped string constant) to int
+
+Incompatible changes:
+- X: value changed from unknown to 12
+
+v0.2.0 is not a valid semantic version for this release.
+Errors were found in one or more packages.
diff --git a/cmd/gorelease/testdata/errors/same_base_release.test b/cmd/gorelease/testdata/errors/same_base_release.test
new file mode 100644
index 0000000..f249699
--- /dev/null
+++ b/cmd/gorelease/testdata/errors/same_base_release.test
@@ -0,0 +1,9 @@
+mod=example.com/errors
+base=v0.1.0
+release=v0.1.0
+error=true
+
+-- want --
+usage: gorelease -base=version [-version=version]
+-base and -version must be different
+For more information, run go doc golang.org/x/exp/cmd/gorelease
diff --git a/cmd/gorelease/testdata/first/README.txt b/cmd/gorelease/testdata/first/README.txt
new file mode 100644
index 0000000..c735527
--- /dev/null
+++ b/cmd/gorelease/testdata/first/README.txt
@@ -0,0 +1 @@
+Module example.com/first is used to test the first tag for a major version.
diff --git a/cmd/gorelease/testdata/first/v0_0_0.test b/cmd/gorelease/testdata/first/v0_0_0.test
new file mode 100644
index 0000000..d3004df
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v0_0_0.test
@@ -0,0 +1,12 @@
+mod=example.com/first
+base=none
+release=v0.0.0
+
+-- want --
+v0.0.0 is a valid semantic version for this release.
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package p
diff --git a/cmd/gorelease/testdata/first/v0_0_1.test b/cmd/gorelease/testdata/first/v0_0_1.test
new file mode 100644
index 0000000..acbc4c9
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v0_0_1.test
@@ -0,0 +1,12 @@
+mod=example.com/first
+base=none
+release=v0.0.1
+
+-- want --
+v0.0.1 is a valid semantic version for this release.
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package p
diff --git a/cmd/gorelease/testdata/first/v0_1_0-alpha.1.test b/cmd/gorelease/testdata/first/v0_1_0-alpha.1.test
new file mode 100644
index 0000000..6c2df3f
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v0_1_0-alpha.1.test
@@ -0,0 +1,12 @@
+mod=example.com/first
+base=none
+release=v0.1.0-alpha.1
+
+-- want --
+v0.1.0-alpha.1 is a valid semantic version for this release.
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package p
diff --git a/cmd/gorelease/testdata/first/v0_1_0.test b/cmd/gorelease/testdata/first/v0_1_0.test
new file mode 100644
index 0000000..8c56a93
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v0_1_0.test
@@ -0,0 +1,12 @@
+mod=example.com/first
+base=none
+release=v0.1.0
+
+-- want --
+v0.1.0 is a valid semantic version for this release.
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package p
diff --git a/cmd/gorelease/testdata/first/v0_err.test b/cmd/gorelease/testdata/first/v0_err.test
new file mode 100644
index 0000000..623a4ee
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v0_err.test
@@ -0,0 +1,20 @@
+mod=example.com/first
+base=none
+release=v0.0.0
+success=false
+
+# TODO(golang.org/issue/36087): go list doesn't report positions in correct
+# place for scanner errors.
+skip=packages.Load gives error with extra "-: " prefix
+
+-- want --
+example.com/first
+-----------------
+errors in new version:
+ p.go:1:9: illegal character U+003F '?'
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package ?
diff --git a/cmd/gorelease/testdata/first/v1_0_0.test b/cmd/gorelease/testdata/first/v1_0_0.test
new file mode 100644
index 0000000..9bbd271
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v1_0_0.test
@@ -0,0 +1,12 @@
+mod=example.com/first
+base=none
+release=v1.0.0
+
+-- want --
+v1.0.0 is a valid semantic version for this release.
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package p
diff --git a/cmd/gorelease/testdata/first/v2_err.test b/cmd/gorelease/testdata/first/v2_err.test
new file mode 100644
index 0000000..09938c8
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v2_err.test
@@ -0,0 +1,20 @@
+mod=example.com/first/v2
+base=none
+release=v2.0.0
+success=false
+
+# TODO(golang.org/issue/36087): go list doesn't report positions in correct
+# place for scanner errors.
+skip=packages.Load gives error with extra "-: " prefix
+
+-- want --
+example.com/first
+-----------------
+errors in new version:
+ p.go:1:9: illegal character U+003F '?'
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package ?
diff --git a/cmd/gorelease/testdata/first/v2_moderr.test b/cmd/gorelease/testdata/first/v2_moderr.test
new file mode 100644
index 0000000..2b6879c
--- /dev/null
+++ b/cmd/gorelease/testdata/first/v2_moderr.test
@@ -0,0 +1,15 @@
+mod=example.com/first
+base=none
+release=v2.0.0
+success=false
+
+-- want --
+v2.0.0 is not a valid semantic version for this release.
+The module path does not end with the major version suffix /v2,
+which is required for major versions v2 or greater.
+-- go.mod --
+module example.com/first
+
+go 1.12
+-- p.go --
+package p
diff --git a/cmd/gorelease/testdata/fix/README.txt b/cmd/gorelease/testdata/fix/README.txt
new file mode 100644
index 0000000..e153198
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/README.txt
@@ -0,0 +1,7 @@
+Tests in this directory cover scenarios where errors in a package are fixed.
+
+v1.0.0 is used as the base version for all tests.
+It has an error: the return type of bad.Broken is undefined.
+
+Each test fixes the error and may make other changes (compatible or not).
+Note that fixing a type error in the API appears to be an incompatible change.
diff --git a/cmd/gorelease/testdata/fix/compatible_other_suggest.test b/cmd/gorelease/testdata/fix/compatible_other_suggest.test
new file mode 100644
index 0000000..ed1d3db
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/compatible_other_suggest.test
@@ -0,0 +1,20 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-compatible-other
+success=false
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+
+example.com/fix/good
+--------------------
+Compatible changes:
+- Better: added
+
+Cannot suggest a release version.
+Errors were found.
diff --git a/cmd/gorelease/testdata/fix/compatible_other_verify.test b/cmd/gorelease/testdata/fix/compatible_other_verify.test
new file mode 100644
index 0000000..e0fb5c8
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/compatible_other_verify.test
@@ -0,0 +1,20 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-compatible-other
+release=v1.1.0
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+
+example.com/fix/good
+--------------------
+Compatible changes:
+- Better: added
+
+v1.1.0 is a valid semantic version for this release.
+Errors were found in the base version. Some API changes may be omitted.
diff --git a/cmd/gorelease/testdata/fix/compatible_other_verify_err.test b/cmd/gorelease/testdata/fix/compatible_other_verify_err.test
new file mode 100644
index 0000000..afc588b
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/compatible_other_verify_err.test
@@ -0,0 +1,22 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-compatible-other
+release=v1.0.1
+success=false
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+
+example.com/fix/good
+--------------------
+Compatible changes:
+- Better: added
+
+v1.0.1 is not a valid semantic version for this release.
+There are compatible changes, but the minor version is not incremented
+over the base version (v1.0.0).
diff --git a/cmd/gorelease/testdata/fix/compatible_same_suggest.test b/cmd/gorelease/testdata/fix/compatible_same_suggest.test
new file mode 100644
index 0000000..b068d77
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/compatible_same_suggest.test
@@ -0,0 +1,17 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-compatible-same
+success=false
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+Compatible changes:
+- Worse: added
+
+Cannot suggest a release version.
+Errors were found.
diff --git a/cmd/gorelease/testdata/fix/compatible_same_verify.test b/cmd/gorelease/testdata/fix/compatible_same_verify.test
new file mode 100644
index 0000000..7aec674
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/compatible_same_verify.test
@@ -0,0 +1,17 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-compatible-same
+release=v1.0.1 # not actually valid, but gorelease can't tell
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+Compatible changes:
+- Worse: added
+
+v1.0.1 is a valid semantic version for this release.
+Errors were found in the base version. Some API changes may be omitted.
diff --git a/cmd/gorelease/testdata/fix/incompatible_other_suggest.test b/cmd/gorelease/testdata/fix/incompatible_other_suggest.test
new file mode 100644
index 0000000..90b0ce7
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/incompatible_other_suggest.test
@@ -0,0 +1,20 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-incompatible-other
+success=false
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+
+example.com/fix/good
+--------------------
+Incompatible changes:
+- Good: changed from func() int to func() string
+
+Cannot suggest a release version.
+Errors were found.
diff --git a/cmd/gorelease/testdata/fix/incompatible_other_verify.test b/cmd/gorelease/testdata/fix/incompatible_other_verify.test
new file mode 100644
index 0000000..fccdf58
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/incompatible_other_verify.test
@@ -0,0 +1,21 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-incompatible-other
+release=v1.1.0
+success=false
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+
+example.com/fix/good
+--------------------
+Incompatible changes:
+- Good: changed from func() int to func() string
+
+v1.1.0 is not a valid semantic version for this release.
+There are incompatible changes.
diff --git a/cmd/gorelease/testdata/fix/incompatible_same_suggest.test b/cmd/gorelease/testdata/fix/incompatible_same_suggest.test
new file mode 100644
index 0000000..cb1a11a
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/incompatible_same_suggest.test
@@ -0,0 +1,16 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-incompatible-same
+success=false
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Bad: changed from func() int to func() string
+- Broken: changed from func() invalid type to func() int
+
+Cannot suggest a release version.
+Errors were found.
diff --git a/cmd/gorelease/testdata/fix/incompatible_same_verify.test b/cmd/gorelease/testdata/fix/incompatible_same_verify.test
new file mode 100644
index 0000000..b4c2155
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/incompatible_same_verify.test
@@ -0,0 +1,16 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.1.0-incompatible-same
+release=v1.0.1 # not actually valid, but gorelease can't tell
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Bad: changed from func() int to func() string
+- Broken: changed from func() invalid type to func() int
+
+v1.0.1 is a valid semantic version for this release.
+Errors were found in the base version. Some API changes may be omitted.
diff --git a/cmd/gorelease/testdata/fix/patch_suggest.test b/cmd/gorelease/testdata/fix/patch_suggest.test
new file mode 100644
index 0000000..3f32b27
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/patch_suggest.test
@@ -0,0 +1,15 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.0.1-patch
+success=false
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+
+Cannot suggest a release version.
+Errors were found.
diff --git a/cmd/gorelease/testdata/fix/patch_verify.test b/cmd/gorelease/testdata/fix/patch_verify.test
new file mode 100644
index 0000000..0546a2a
--- /dev/null
+++ b/cmd/gorelease/testdata/fix/patch_verify.test
@@ -0,0 +1,15 @@
+mod=example.com/fix
+base=v1.0.0
+version=v1.0.1-patch
+release=v1.0.1
+-- want --
+example.com/fix/bad
+-------------------
+errors in base version:
+ bad/bad.go:3:15: undeclared name: NOTYPE
+
+Incompatible changes:
+- Broken: changed from func() invalid type to func() int
+
+v1.0.1 is a valid semantic version for this release.
+Errors were found in the base version. Some API changes may be omitted.
diff --git a/cmd/gorelease/testdata/main/README.txt b/cmd/gorelease/testdata/main/README.txt
new file mode 100644
index 0000000..a8d8104
--- /dev/null
+++ b/cmd/gorelease/testdata/main/README.txt
@@ -0,0 +1,3 @@
+Module example.com/main is used to test changes in main packages.
+Main packages aren't importable, so changes to exported functions should not
+be reported. But we should still report when packages are added or deleted.
diff --git a/cmd/gorelease/testdata/main/add.test b/cmd/gorelease/testdata/main/add.test
new file mode 100644
index 0000000..73b2cae
--- /dev/null
+++ b/cmd/gorelease/testdata/main/add.test
@@ -0,0 +1,21 @@
+mod=example.com/main
+base=v0.0.1
+-- want --
+example.com/main/b
+------------------
+Compatible changes:
+- package added
+
+Suggested version: v0.1.0
+-- go.mod --
+module example.com/main
+
+go 1.14
+-- a/a.go --
+package main
+
+func main() {}
+-- b/b.go --
+package main
+
+func main() {}
diff --git a/cmd/gorelease/testdata/main/change.test b/cmd/gorelease/testdata/main/change.test
new file mode 100644
index 0000000..688839e
--- /dev/null
+++ b/cmd/gorelease/testdata/main/change.test
@@ -0,0 +1,14 @@
+mod=example.com/main
+base=v0.0.1
+-- want --
+Suggested version: v0.0.2
+-- go.mod --
+module example.com/main
+
+go 1.14
+-- a/a.go --
+package main
+
+func Foo() {}
+
+func main() { Foo() }
diff --git a/cmd/gorelease/testdata/main/remove.test b/cmd/gorelease/testdata/main/remove.test
new file mode 100644
index 0000000..41ac909
--- /dev/null
+++ b/cmd/gorelease/testdata/main/remove.test
@@ -0,0 +1,13 @@
+mod=example.com/main
+base=v0.0.1
+-- want --
+example.com/main/a
+------------------
+Incompatible changes:
+- package removed
+
+Suggested version: v0.1.0
+-- go.mod --
+module example.com/main
+
+go 1.14
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_basic_v0.0.1.txt
new file mode 100644
index 0000000..e5352bf
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v0.0.1.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v0.1.0.txt b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.0.txt
new file mode 100644
index 0000000..bb39290
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.0.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v0.1.1-pre.txt b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.1-pre.txt
new file mode 100644
index 0000000..743517b
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.1-pre.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v0.1.1.txt b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.1.txt
new file mode 100644
index 0000000..743517b
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.1.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v0.1.2.txt b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.2.txt
new file mode 100644
index 0000000..4055eaa
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v0.1.2.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v1.0.1.txt b/cmd/gorelease/testdata/mod/example.com_basic_v1.0.1.txt
new file mode 100644
index 0000000..e5352bf
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v1.0.1.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v1.1.0.txt b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.0.txt
new file mode 100644
index 0000000..bb39290
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.0.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v1.1.1-pre.txt b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.1-pre.txt
new file mode 100644
index 0000000..743517b
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.1-pre.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v1.1.1.txt b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.1.txt
new file mode 100644
index 0000000..743517b
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.1.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v1.1.2.txt b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.2.txt
new file mode 100644
index 0000000..4055eaa
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v1.1.2.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.0.1.txt b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.0.1.txt
new file mode 100644
index 0000000..97ea69f
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.0.1.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/basic/v2
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.0.txt b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.0.txt
new file mode 100644
index 0000000..e3593b9
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.0.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic/v2
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.1-pre.txt b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.1-pre.txt
new file mode 100644
index 0000000..aaa86be
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.1-pre.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic/v2
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.1.txt b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.1.txt
new file mode 100644
index 0000000..aaa86be
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.1.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/basic/v2
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.2.txt b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.2.txt
new file mode 100644
index 0000000..0d950c1
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_basic_v2_v2.1.2.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/basic/v2
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
diff --git a/cmd/gorelease/testdata/mod/example.com_empty_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_empty_v0.0.1.txt
new file mode 100644
index 0000000..8e7df04
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_empty_v0.0.1.txt
@@ -0,0 +1,4 @@
+-- go.mod --
+module example.com/empty
+
+go 1.14
diff --git a/cmd/gorelease/testdata/mod/example.com_empty_v0.0.2.txt b/cmd/gorelease/testdata/mod/example.com_empty_v0.0.2.txt
new file mode 100644
index 0000000..8e7df04
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_empty_v0.0.2.txt
@@ -0,0 +1,4 @@
+-- go.mod --
+module example.com/empty
+
+go 1.14
diff --git a/cmd/gorelease/testdata/mod/example.com_errors_master.txt b/cmd/gorelease/testdata/mod/example.com_errors_master.txt
new file mode 100644
index 0000000..c4c52ed
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_errors_master.txt
@@ -0,0 +1,4 @@
+Non-canonical version, referenced in errors/bad_base.test.
+For now, it's an error to use a non-canonical -base. It won't be in the future.
+-- .info --
+{"Version":"v0.2.0"}
diff --git a/cmd/gorelease/testdata/mod/example.com_errors_v0.1.0.txt b/cmd/gorelease/testdata/mod/example.com_errors_v0.1.0.txt
new file mode 100644
index 0000000..f049377
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_errors_v0.1.0.txt
@@ -0,0 +1,26 @@
+Module example.com/errors is used to compare modules with errors across
+two versions.
+
+* Package "fixed" has type errors in v0.1.0, fixed in v0.2.0.
+* Package "deleted" has type errors in v0.1.0, deleted in v0.2.0.
+* Package "broken" is correct in v0.1.0, has type errors in v0.2.0.
+* Package "added" doesn't exist in v0.1.0, has type errors in v0.2.0.
+
+-- go.mod --
+module example.com/errors
+
+go 1.12
+-- fixed/fixed.go --
+package fixed
+
+const X int = "not an int"
+
+-- deleted/deleted.go --
+package deleted
+
+const X int = "not an int"
+
+-- broken/broken.go --
+package broken
+
+const X int = 12
diff --git a/cmd/gorelease/testdata/mod/example.com_errors_v0.2.0.txt b/cmd/gorelease/testdata/mod/example.com_errors_v0.2.0.txt
new file mode 100644
index 0000000..1f01c27
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_errors_v0.2.0.txt
@@ -0,0 +1,18 @@
+-- go.mod --
+module example.com/errors
+
+go 1.12
+-- fixed/fixed.go --
+package fixed
+
+const X int = 12
+
+-- broken/broken.go --
+package broken
+
+const X int = "no longer an int"
+
+-- added/added.go --
+package added
+
+const X int = "not an int"
diff --git a/cmd/gorelease/testdata/mod/example.com_fix_v1.0.0.txt b/cmd/gorelease/testdata/mod/example.com_fix_v1.0.0.txt
new file mode 100644
index 0000000..21daf65
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_fix_v1.0.0.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/fix
+
+go 1.13
+-- bad/bad.go --
+package bad
+
+func Broken() NOTYPE { return 0 }
+func Bad() int { return 1 }
+-- good/good.go --
+package good
+
+func Good() int { return 1 }
diff --git a/cmd/gorelease/testdata/mod/example.com_fix_v1.0.1-patch.txt b/cmd/gorelease/testdata/mod/example.com_fix_v1.0.1-patch.txt
new file mode 100644
index 0000000..56c1c66
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_fix_v1.0.1-patch.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/fix
+
+go 1.13
+-- bad/bad.go --
+package bad
+
+func Broken() int { return 0 }
+func Bad() int { return 1 }
+-- good/good.go --
+package good
+
+func Good() int { return 1 }
diff --git a/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-compatible-other.txt b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-compatible-other.txt
new file mode 100644
index 0000000..068ed8f
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-compatible-other.txt
@@ -0,0 +1,14 @@
+-- go.mod --
+module example.com/fix
+
+go 1.13
+-- bad/bad.go --
+package bad
+
+func Broken() int { return 0 }
+func Bad() int { return 1 }
+-- good/good.go --
+package good
+
+func Good() int { return 1 }
+func Better() int { return 2 }
diff --git a/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-compatible-same.txt b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-compatible-same.txt
new file mode 100644
index 0000000..6ff9299
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-compatible-same.txt
@@ -0,0 +1,14 @@
+-- go.mod --
+module example.com/fix
+
+go 1.13
+-- bad/bad.go --
+package bad
+
+func Broken() int { return 0 }
+func Bad() int { return 1 }
+func Worse() int { return -1 }
+-- good/good.go --
+package good
+
+func Good() int { return 1 }
diff --git a/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-incompatible-other.txt b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-incompatible-other.txt
new file mode 100644
index 0000000..a7013b8
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-incompatible-other.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/fix
+
+go 1.13
+-- bad/bad.go --
+package bad
+
+func Broken() int { return 0 }
+func Bad() int { return 1 }
+-- good/good.go --
+package good
+
+func Good() string { return "1" }
diff --git a/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-incompatible-same.txt b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-incompatible-same.txt
new file mode 100644
index 0000000..1fb4891
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_fix_v1.1.0-incompatible-same.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/fix
+
+go 1.13
+-- bad/bad.go --
+package bad
+
+func Broken() int { return 0 }
+func Bad() string { return "1" }
+-- good/good.go --
+package good
+
+func Good() int { return 1 }
diff --git a/cmd/gorelease/testdata/mod/example.com_main_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_main_v0.0.1.txt
new file mode 100644
index 0000000..a76b5b2
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_main_v0.0.1.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/main
+
+go 1.14
+-- a/a.go --
+package main
+
+func main() {}
diff --git a/cmd/gorelease/testdata/mod/example.com_nomod_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_nomod_v0.0.1.txt
new file mode 100644
index 0000000..760a8e8
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_nomod_v0.0.1.txt
@@ -0,0 +1,6 @@
+-- .mod --
+module example.com/nomod
+-- p/p.go --
+package p // import "example.com/something/different"
+
+// The import comment above is ignored by gorelease and by modules.
diff --git a/cmd/gorelease/testdata/mod/example.com_nomod_v0.0.2.txt b/cmd/gorelease/testdata/mod/example.com_nomod_v0.0.2.txt
new file mode 100644
index 0000000..06e2024
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_nomod_v0.0.2.txt
@@ -0,0 +1,6 @@
+-- go.mod --
+module example.com/nomod
+
+go 1.12
+-- p/p.go --
+package p
diff --git a/cmd/gorelease/testdata/mod/example.com_private_v1.0.0.txt b/cmd/gorelease/testdata/mod/example.com_private_v1.0.0.txt
new file mode 100644
index 0000000..ca71c99
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_private_v1.0.0.txt
@@ -0,0 +1,17 @@
+-- go.mod --
+module example.com/private
+
+go 1.12
+-- p/p.go --
+package p
+
+import "example.com/private/internal/i"
+
+type P i.I
+
+-- internal/i/i.go --
+package i
+
+type I interface{}
+
+func Delete() {}
diff --git a/cmd/gorelease/testdata/mod/example.com_private_v1.0.1.txt b/cmd/gorelease/testdata/mod/example.com_private_v1.0.1.txt
new file mode 100644
index 0000000..1fe445c
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_private_v1.0.1.txt
@@ -0,0 +1,15 @@
+-- go.mod --
+module example.com/private
+
+go 1.12
+-- p/p.go --
+package p
+
+import "example.com/private/internal/i"
+
+type P i.I
+
+-- internal/i/i.go --
+package i
+
+type I interface{}
diff --git a/cmd/gorelease/testdata/mod/example.com_private_v1.0.2-break.txt b/cmd/gorelease/testdata/mod/example.com_private_v1.0.2-break.txt
new file mode 100644
index 0000000..84e3616
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_private_v1.0.2-break.txt
@@ -0,0 +1,17 @@
+-- go.mod --
+module example.com/private
+
+go 1.12
+-- p/p.go --
+package p
+
+import "example.com/private/internal/i"
+
+type P i.I
+
+-- internal/i/i.go --
+package i
+
+type I interface{
+ Foo()
+}
diff --git a/cmd/gorelease/testdata/mod/example.com_sub_nest_v1.0.0.txt b/cmd/gorelease/testdata/mod/example.com_sub_nest_v1.0.0.txt
new file mode 100644
index 0000000..36f5b33
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_sub_nest_v1.0.0.txt
@@ -0,0 +1,6 @@
+-- go.mod --
+module example.com/sub/nest
+
+go 1.12
+-- nest.go --
+package nest
diff --git a/cmd/gorelease/testdata/mod/example.com_sub_nest_v2_v2.0.0.txt b/cmd/gorelease/testdata/mod/example.com_sub_nest_v2_v2.0.0.txt
new file mode 100644
index 0000000..b7dc8a7
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_sub_nest_v2_v2.0.0.txt
@@ -0,0 +1,6 @@
+-- go.mod --
+module example.com/sub/nest/v2
+
+go 1.12
+-- nest.go --
+package nest
diff --git a/cmd/gorelease/testdata/mod/example.com_sub_v2_v2.0.0.txt b/cmd/gorelease/testdata/mod/example.com_sub_v2_v2.0.0.txt
new file mode 100644
index 0000000..488f2dc
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_sub_v2_v2.0.0.txt
@@ -0,0 +1,6 @@
+-- go.mod --
+module example.com/sub/v2
+
+go 1.12
+-- sub.go --
+package sub
diff --git a/cmd/gorelease/testdata/mod/example.com_tidy_a_v0.1.0.txt b/cmd/gorelease/testdata/mod/example.com_tidy_a_v0.1.0.txt
new file mode 100644
index 0000000..d9c71aa
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_tidy_a_v0.1.0.txt
@@ -0,0 +1,10 @@
+-- go.mod --
+module example.com/tidy/a
+
+go 1.12
+
+require example.com/tidy/b v0.2.0
+-- p.go --
+package a
+
+import _ "example.com/tidy/b"
diff --git a/cmd/gorelease/testdata/mod/example.com_tidy_b_v0.1.0.txt b/cmd/gorelease/testdata/mod/example.com_tidy_b_v0.1.0.txt
new file mode 100644
index 0000000..d169972
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_tidy_b_v0.1.0.txt
@@ -0,0 +1,6 @@
+-- go.mod --
+module example.com/tidy/b
+
+go 1.12
+-- p.go --
+package b
diff --git a/cmd/gorelease/testdata/mod/example.com_tidy_b_v0.2.0.txt b/cmd/gorelease/testdata/mod/example.com_tidy_b_v0.2.0.txt
new file mode 100644
index 0000000..470b743
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_tidy_b_v0.2.0.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/tidy/b
+
+go 1.12
+-- p.go --
+package b
+
+func B() {}
diff --git a/cmd/gorelease/testdata/mod/example.com_tidy_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_tidy_v0.0.1.txt
new file mode 100644
index 0000000..f6e7fff
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_tidy_v0.0.1.txt
@@ -0,0 +1,6 @@
+-- go.mod --
+module example.com/tidy
+
+go 1.12
+-- tidy.go --
+package tidy
diff --git a/cmd/gorelease/testdata/nomod/README.txt b/cmd/gorelease/testdata/nomod/README.txt
new file mode 100644
index 0000000..ed50bb0
--- /dev/null
+++ b/cmd/gorelease/testdata/nomod/README.txt
@@ -0,0 +1,2 @@
+Module example.com/nomod is used to test situations where no go.mod file
+is present.
diff --git a/cmd/gorelease/testdata/nomod/nomod.test b/cmd/gorelease/testdata/nomod/nomod.test
new file mode 100644
index 0000000..9f332fe
--- /dev/null
+++ b/cmd/gorelease/testdata/nomod/nomod.test
@@ -0,0 +1,6 @@
+mod=example.com/nomod
+version=v0.0.2
+base=v0.0.1
+release=v0.0.2
+-- want --
+v0.0.2 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/patherrors/README.txt b/cmd/gorelease/testdata/patherrors/README.txt
new file mode 100644
index 0000000..82f95f1
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/README.txt
@@ -0,0 +1 @@
+Module example.com/patherrors tests errors related to paths.
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/patherrors/abspath.test b/cmd/gorelease/testdata/patherrors/abspath.test
new file mode 100644
index 0000000..317364e
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/abspath.test
@@ -0,0 +1,8 @@
+mod=example.com/patherrors
+base=none
+error=true
+-- want --
+module path "/home/gopher/go/src/mymod" must not be an absolute path.
+It must be an address where your module may be found.
+-- go.mod --
+module /home/gopher/go/src/mymod
diff --git a/cmd/gorelease/testdata/patherrors/badmajor.test b/cmd/gorelease/testdata/patherrors/badmajor.test
new file mode 100644
index 0000000..4f5d027
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/badmajor.test
@@ -0,0 +1,10 @@
+mod=example.com/patherrors
+base=none
+error=true
+-- want --
+module path "example.com/patherrors/v0" has major version suffix "v0".
+A major version suffix is only allowed for v2 or later.
+-- go.mod --
+module example.com/patherrors/v0
+
+go 1.12
diff --git a/cmd/gorelease/testdata/patherrors/dup_roots_branch.test b/cmd/gorelease/testdata/patherrors/dup_roots_branch.test
new file mode 100644
index 0000000..7e27b87
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/dup_roots_branch.test
@@ -0,0 +1,17 @@
+dir=dup
+base=none
+success=false
+-- .git/empty --
+empty file to mark repository root
+-- dup/go.mod --
+module example.com/dup/v2
+
+go 1.13
+-- dup/v2/go.mod --
+module example.com/dup/v2
+
+go 1.13
+-- want --
+module is defined in two locations:
+ dup/go.mod
+ dup/v2/go.mod
diff --git a/cmd/gorelease/testdata/patherrors/dup_roots_dir.test b/cmd/gorelease/testdata/patherrors/dup_roots_dir.test
new file mode 100644
index 0000000..41d6efc
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/dup_roots_dir.test
@@ -0,0 +1,17 @@
+dir=dup/v2
+base=none
+success=false
+-- .git/empty --
+empty file to mark repository root
+-- dup/go.mod --
+module example.com/dup/v2
+
+go 1.13
+-- dup/v2/go.mod --
+module example.com/dup/v2
+
+go 1.13
+-- want --
+module is defined in two locations:
+ dup/v2/go.mod
+ dup/go.mod
diff --git a/cmd/gorelease/testdata/patherrors/dup_roots_ok.test b/cmd/gorelease/testdata/patherrors/dup_roots_ok.test
new file mode 100644
index 0000000..b8716a5
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/dup_roots_ok.test
@@ -0,0 +1,14 @@
+dir=dup/v2
+base=none
+-- .git/empty --
+empty file to mark repository root
+-- dup/go.mod --
+module example.com/dup
+
+go 1.13
+-- dup/v2/go.mod --
+module example.com/dup/v2
+
+go 1.13
+-- want --
+Suggested version: v2.0.0 (with tag dup/v2.0.0)
diff --git a/cmd/gorelease/testdata/patherrors/gopkginsub.test b/cmd/gorelease/testdata/patherrors/gopkginsub.test
new file mode 100644
index 0000000..659e87d
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/gopkginsub.test
@@ -0,0 +1,12 @@
+mod=example.com/patherrors
+base=none
+dir=yaml
+success=false
+-- want --
+go.mod: go directive is missing
+gopkg.in/yaml.v2: module path starts with gopkg.in and must be declared in the root directory of the repository
+-- .mod --
+module example.com/patherrors
+-- .git/HEAD --
+-- yaml/go.mod --
+module gopkg.in/yaml.v2
diff --git a/cmd/gorelease/testdata/patherrors/pathsub.test b/cmd/gorelease/testdata/patherrors/pathsub.test
new file mode 100644
index 0000000..382fef7
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/pathsub.test
@@ -0,0 +1,13 @@
+mod=example.com/patherrors
+dir=x
+base=none
+success=false
+-- want --
+example.com/y: module path must end with "x", since it is in subdirectory "x"
+-- .mod --
+module example.com/patherrors
+-- .git/HEAD --
+-- x/go.mod --
+module example.com/y
+
+go 1.12
diff --git a/cmd/gorelease/testdata/patherrors/pathsubv2.test b/cmd/gorelease/testdata/patherrors/pathsubv2.test
new file mode 100644
index 0000000..a436d1a
--- /dev/null
+++ b/cmd/gorelease/testdata/patherrors/pathsubv2.test
@@ -0,0 +1,13 @@
+mod=example.com/patherrors
+base=none
+dir=x
+success=false
+-- want --
+example.com/y/v2: module path must end with "x" or "x/v2", since it is in subdirectory "x"
+-- .mod --
+module example.com/patherrors
+-- .git/HEAD --
+-- x/go.mod --
+module example.com/y/v2
+
+go 1.12
diff --git a/cmd/gorelease/testdata/private/README.txt b/cmd/gorelease/testdata/private/README.txt
new file mode 100644
index 0000000..226b06d
--- /dev/null
+++ b/cmd/gorelease/testdata/private/README.txt
@@ -0,0 +1,3 @@
+Module example.com/private is used to test that changes to
+internal packages are not reported unless they affect the exported
+API of non-internal packages.
diff --git a/cmd/gorelease/testdata/private/break.test b/cmd/gorelease/testdata/private/break.test
new file mode 100644
index 0000000..14967ab
--- /dev/null
+++ b/cmd/gorelease/testdata/private/break.test
@@ -0,0 +1,12 @@
+mod=example.com/private
+version=v1.0.2-break
+base=v1.0.1
+success=false
+-- want --
+example.com/private/p
+---------------------
+Incompatible changes:
+- I.Foo: added
+
+Cannot suggest a release version.
+Incompatible changes were detected.
diff --git a/cmd/gorelease/testdata/private/unreported.test b/cmd/gorelease/testdata/private/unreported.test
new file mode 100644
index 0000000..3aabad3
--- /dev/null
+++ b/cmd/gorelease/testdata/private/unreported.test
@@ -0,0 +1,5 @@
+mod=example.com/private
+version=v1.0.1
+base=v1.0.0
+-- want --
+Suggested version: v1.0.1
diff --git a/cmd/gorelease/testdata/sub/README.txt b/cmd/gorelease/testdata/sub/README.txt
new file mode 100644
index 0000000..54ff480
--- /dev/null
+++ b/cmd/gorelease/testdata/sub/README.txt
@@ -0,0 +1,10 @@
+This directory contains tests for modules that aren't at the root
+of the repository, which is marked with a .git directory.
+We're comparing against an earlier published version with a
+trivial package. Nothing has changed except the location of the
+module within the repository.
+
+ example.com/sub - corresponds to the root directory. Not a module.
+ example.com/sub/v2 - may be in v2 subdirectory.
+ example.com/sub/nest - nested module in subdirectory
+ example.com/sub/nest/v2 - may be in nest or nest/v2.
diff --git a/cmd/gorelease/testdata/sub/nest.test b/cmd/gorelease/testdata/sub/nest.test
new file mode 100644
index 0000000..7557f23
--- /dev/null
+++ b/cmd/gorelease/testdata/sub/nest.test
@@ -0,0 +1,12 @@
+mod=example.com/sub/nest
+dir=nest
+base=v1.0.0
+-- want --
+Suggested version: v1.0.1 (with tag nest/v1.0.1)
+-- .git/HEAD --
+-- nest/go.mod --
+module example.com/sub/nest
+
+go 1.12
+-- nest/nest.go --
+package nest
diff --git a/cmd/gorelease/testdata/sub/nest_v2.test b/cmd/gorelease/testdata/sub/nest_v2.test
new file mode 100644
index 0000000..cc54576
--- /dev/null
+++ b/cmd/gorelease/testdata/sub/nest_v2.test
@@ -0,0 +1,12 @@
+mod=example.com/sub/nest/v2
+dir=nest
+base=v2.0.0
+-- want --
+Suggested version: v2.0.1 (with tag nest/v2.0.1)
+-- .git/HEAD --
+-- nest/go.mod --
+module example.com/sub/nest/v2
+
+go 1.12
+-- nest/nest.go --
+package nest
diff --git a/cmd/gorelease/testdata/sub/nest_v2_dir.test b/cmd/gorelease/testdata/sub/nest_v2_dir.test
new file mode 100644
index 0000000..aea619d
--- /dev/null
+++ b/cmd/gorelease/testdata/sub/nest_v2_dir.test
@@ -0,0 +1,12 @@
+mod=example.com/sub/nest/v2
+dir=nest/v2
+base=v2.0.0
+-- want --
+Suggested version: v2.0.1 (with tag nest/v2.0.1)
+-- .git/HEAD --
+-- nest/v2/go.mod --
+module example.com/sub/nest/v2
+
+go 1.12
+-- nest/v2/nest.go --
+package nest
diff --git a/cmd/gorelease/testdata/sub/v2_dir.test b/cmd/gorelease/testdata/sub/v2_dir.test
new file mode 100644
index 0000000..c1a4da7
--- /dev/null
+++ b/cmd/gorelease/testdata/sub/v2_dir.test
@@ -0,0 +1,12 @@
+mod=example.com/sub/v2
+dir=v2
+base=v2.0.0
+-- want --
+Suggested version: v2.0.1
+-- .git/HEAD --
+-- v2/go.mod --
+module example.com/sub/v2
+
+go 1.12
+-- v2/sub.go --
+package sub
diff --git a/cmd/gorelease/testdata/sub/v2_root.test b/cmd/gorelease/testdata/sub/v2_root.test
new file mode 100644
index 0000000..f53cadd
--- /dev/null
+++ b/cmd/gorelease/testdata/sub/v2_root.test
@@ -0,0 +1,11 @@
+mod=example.com/sub/v2
+base=v2.0.0
+-- want --
+Suggested version: v2.0.1
+-- .git/HEAD --
+-- go.mod --
+module example.com/sub/v2
+
+go 1.12
+-- sub.go --
+package sub
diff --git a/cmd/gorelease/testdata/tidy/README.txt b/cmd/gorelease/testdata/tidy/README.txt
new file mode 100644
index 0000000..0ca2fb4
--- /dev/null
+++ b/cmd/gorelease/testdata/tidy/README.txt
@@ -0,0 +1,5 @@
+Module example.com/tidy tests versions that do not have tidy
+go.mod or go.sum files.
+
+v0.0.1 has a trivial package with no imports. It has no requirements
+and no go.sum, so it is tidy. Tests make changes on top of this.
diff --git a/cmd/gorelease/testdata/tidy/empty_sum.test b/cmd/gorelease/testdata/tidy/empty_sum.test
new file mode 100644
index 0000000..3cfeeac
--- /dev/null
+++ b/cmd/gorelease/testdata/tidy/empty_sum.test
@@ -0,0 +1,17 @@
+mod=example.com/tidy
+base=v0.0.1
+success=0
+-- want --
+go.sum: one or more sums are missing.
+Run 'go mod tidy' to add missing sums.
+-- go.mod --
+module example.com/tidy
+
+go 1.12
+
+require example.com/basic v1.0.1
+-- go.sum --
+-- tidy.go --
+package tidy
+
+import _ "example.com/basic/a"
diff --git a/cmd/gorelease/testdata/tidy/extra_req.test b/cmd/gorelease/testdata/tidy/extra_req.test
new file mode 100644
index 0000000..58973c3
--- /dev/null
+++ b/cmd/gorelease/testdata/tidy/extra_req.test
@@ -0,0 +1,17 @@
+mod=example.com/tidy
+base=v0.0.1
+-- want --
+Suggested version: v0.0.2
+-- go.mod --
+module example.com/tidy
+
+go 1.12
+
+require example.com/basic v1.0.1
+-- go.sum --
+example.com/basic v1.0.1/go.mod h1:pv9xTX7lhV6R1XNYo1EcI/DQqKxDyhNTN+K1DjHW2Oo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+-- tidy.go --
+package tidy
diff --git a/cmd/gorelease/testdata/tidy/misleading_req.test b/cmd/gorelease/testdata/tidy/misleading_req.test
new file mode 100644
index 0000000..ee970a9
--- /dev/null
+++ b/cmd/gorelease/testdata/tidy/misleading_req.test
@@ -0,0 +1,26 @@
+mod=example.com/tidy
+base=none
+success=false
+-- go.mod --
+module example.com/tidy
+
+go 1.12
+
+require (
+ example.com/tidy/a v0.1.0 // actually transitively requires v0.2.0
+ example.com/tidy/b v0.1.0
+)
+-- go.sum --
+example.com/tidy/a v0.1.0 h1:hxFAdyLfJ6TV25ffYI2oA+g3ffLp+XJgo6lrVkT8ufU=
+example.com/tidy/a v0.1.0/go.mod h1:/KTGkbP1cnyJLO5kGL/QSCswh5I8R66epCmEAxgAK+I=
+example.com/tidy/b v0.1.0/go.mod h1:92saqyRYqaI4eqrr6LGMnPfBDXc2yofWznwSxsvqfEw=
+example.com/tidy/b v0.2.0 h1:dSh97fZcMRg87GDb1Gqwy8/mebsrmE4kX3S7d+KeSZU=
+example.com/tidy/b v0.2.0/go.mod h1:92saqyRYqaI4eqrr6LGMnPfBDXc2yofWznwSxsvqfEw=
+-- tidy.go --
+package tidy
+
+import _ "example.com/tidy/a"
+import _ "example.com/tidy/b"
+-- want --
+go.mod: requirements are incomplete.
+Run 'go mod tidy' to add missing requirements.
diff --git a/cmd/gorelease/testdata/tidy/missing_go.test b/cmd/gorelease/testdata/tidy/missing_go.test
new file mode 100644
index 0000000..9ef09d9
--- /dev/null
+++ b/cmd/gorelease/testdata/tidy/missing_go.test
@@ -0,0 +1,9 @@
+mod=example.com/tidy
+base=v0.0.1
+success=0
+-- want --
+go.mod: go directive is missing
+-- go.mod --
+module example.com/tidy
+-- tidy.go --
+package tidy
diff --git a/cmd/gorelease/testdata/tidy/missing_req.test b/cmd/gorelease/testdata/tidy/missing_req.test
new file mode 100644
index 0000000..3e43e76
--- /dev/null
+++ b/cmd/gorelease/testdata/tidy/missing_req.test
@@ -0,0 +1,14 @@
+mod=example.com/tidy
+base=v0.0.1
+success=false
+-- want --
+go.mod: requirements are incomplete.
+Run 'go mod tidy' to add missing requirements.
+-- go.mod --
+module example.com/tidy
+
+go 1.12
+-- tidy.go --
+package tidy
+
+import _ "example.com/basic/a"
diff --git a/cmd/gorelease/testdata/tidy/no_sum.test b/cmd/gorelease/testdata/tidy/no_sum.test
new file mode 100644
index 0000000..58ce747
--- /dev/null
+++ b/cmd/gorelease/testdata/tidy/no_sum.test
@@ -0,0 +1,16 @@
+mod=example.com/tidy
+base=v0.0.1
+success=0
+-- want --
+go.sum: one or more sums are missing.
+Run 'go mod tidy' to add missing sums.
+-- go.mod --
+module example.com/tidy
+
+go 1.12
+
+require example.com/basic v1.1.2
+-- tidy.go --
+package tidy
+
+import _ "example.com/basic/a"
diff --git a/go.mod b/go.mod
index bc31db7..2cc8780 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,8 @@
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028
- golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee
- golang.org/x/sys v0.0.0-20190412213103-97732733099d
+ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b
+ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa
+ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898
)
diff --git a/go.sum b/go.sum
index 9a50bee..8558af0 100644
--- a/go.sum
+++ b/go.sum
@@ -13,8 +13,9 @@
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b h1:GgiSbuUyC0BlbUmHQBgFqu32eiRR/CEYdjOjOd4zE6Y=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -22,6 +23,8 @@
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/sys v0.0.0-20191001151750-bb3f8db39f24 h1:R8bzl0244nw47n1xKs1MUMAaTNgjavKcN/aX2Ss3+Fo=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=