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=