golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/cmd/gorelease/gorelease.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // gorelease is an experimental tool that helps module authors avoid common
     6  // problems before releasing a new version of a module.
     7  //
     8  // Usage:
     9  //
    10  //	gorelease [-base={version|none}] [-version=version]
    11  //
    12  // Examples:
    13  //
    14  //	# Compare with the latest version and suggest a new version.
    15  //	gorelease
    16  //
    17  //	# Compare with a specific version and suggest a new version.
    18  //	gorelease -base=v1.2.3
    19  //
    20  //	# Compare with the latest version and check a specific new version for compatibility.
    21  //	gorelease -version=v1.3.0
    22  //
    23  //	# Compare with a specific version and check a specific new version for compatibility.
    24  //	gorelease -base=v1.2.3 -version=v1.3.0
    25  //
    26  // gorelease analyzes changes in the public API and dependencies of the main
    27  // module. It compares a base version (set with -base) with the currently
    28  // checked out revision. Given a proposed version to release (set with
    29  // -version), gorelease reports whether the changes are consistent with
    30  // semantic versioning. If no version is proposed with -version, gorelease
    31  // suggests the lowest version consistent with semantic versioning.
    32  //
    33  // If there are no visible changes in the module's public API, gorelease
    34  // accepts versions that increment the minor or patch version numbers. For
    35  // example, if the base version is "v2.3.1", gorelease would accept "v2.3.2" or
    36  // "v2.4.0" or any prerelease of those versions, like "v2.4.0-beta". If no
    37  // version is proposed, gorelease would suggest "v2.3.2".
    38  //
    39  // If there are only backward compatible differences in the module's public
    40  // API, gorelease only accepts versions that increment the minor version. For
    41  // example, if the base version is "v2.3.1", gorelease would accept "v2.4.0"
    42  // but not "v2.3.2".
    43  //
    44  // If there are incompatible API differences for a proposed version with
    45  // major version 1 or higher, gorelease will exit with a non-zero status.
    46  // Incompatible differences may only be released in a new major version, which
    47  // requires creating a module with a different path. For example, if
    48  // incompatible changes are made in the module "example.com/mod", a
    49  // new major version must be released as a new module, "example.com/mod/v2".
    50  // For a proposed version with major version 0, which allows incompatible
    51  // changes, gorelease will describe all changes, but incompatible changes
    52  // will not affect its exit status.
    53  //
    54  // For more information on semantic versioning, see https://semver.org.
    55  //
    56  // Note: gorelease does not accept build metadata in releases (like
    57  // v1.0.0+debug). Although it is valid semver, the Go tool and other tools in
    58  // the ecosystem do not support it, so its use is not recommended.
    59  //
    60  // gorelease accepts the following flags:
    61  //
    62  // -base=version: The version that the current version of the module will be
    63  // compared against. This may be a version like "v1.5.2", a version query like
    64  // "latest", or "none". If the version is "none", gorelease will not compare the
    65  // current version against any previous version; it will only validate the
    66  // current version. This is useful for checking the first release of a new major
    67  // version. The version may be preceded by a different module path and an '@',
    68  // like -base=example.com/mod/v2@v2.5.2. This is useful to compare against
    69  // an earlier major version or a fork. If -base is not specified, gorelease will
    70  // attempt to infer a base version from the -version flag and available released
    71  // versions.
    72  //
    73  // -version=version: The proposed version to be released. If specified,
    74  // gorelease will confirm whether this version is consistent with changes made
    75  // to the module's public API. gorelease will exit with a non-zero status if the
    76  // version is not valid.
    77  //
    78  // gorelease is eventually intended to be merged into the go command
    79  // as "go release". See golang.org/issues/26420.
    80  package main
    81  
    82  import (
    83  	"bytes"
    84  	"context"
    85  	"encoding/json"
    86  	"errors"
    87  	"flag"
    88  	"fmt"
    89  	"go/build"
    90  	"io"
    91  	"log"
    92  	"os"
    93  	"os/exec"
    94  	"path"
    95  	"path/filepath"
    96  	"sort"
    97  	"strings"
    98  	"unicode"
    99  
   100  	"golang.org/x/exp/apidiff"
   101  	"golang.org/x/mod/modfile"
   102  	"golang.org/x/mod/module"
   103  	"golang.org/x/mod/semver"
   104  	"golang.org/x/mod/zip"
   105  	"golang.org/x/tools/go/packages"
   106  )
   107  
   108  // IDEAS:
   109  // * Should we suggest versions at all or should -version be mandatory?
   110  // * Verify downstream modules have licenses. May need an API or library
   111  //   for this. Be clear that we can't provide legal advice.
   112  // * Internal packages may be relevant to submodules (for example,
   113  //   golang.org/x/tools/internal/lsp is imported by golang.org/x/tools).
   114  //   gorelease should detect whether this is the case and include internal
   115  //   directories in comparison. It should be possible to opt out or specify
   116  //   a different list of submodules.
   117  // * Decide what to do about build constraints, particularly GOOS and GOARCH.
   118  //   The API may be different on some platforms (e.g., x/sys).
   119  //   Should gorelease load packages in multiple configurations in the same run?
   120  //   Is it a compatible change if the same API is available for more platforms?
   121  //   Is it an incompatible change for fewer?
   122  //   How about cgo? Is adding a new cgo dependency an incompatible change?
   123  // * Support splits and joins of nested modules. For example, if we are
   124  //   proposing to tag a particular commit as both cloud.google.com/go v0.46.2
   125  //   and cloud.google.com/go/storage v1.0.0, we should ensure that the sets of
   126  //   packages provided by those modules are disjoint, and we should not report
   127  //   the packages moved from one to the other as an incompatible change (since
   128  //   the APIs are still compatible, just with a different module split).
   129  
   130  // TODO(jayconrod):
   131  // * Clean up overuse of fmt.Errorf.
   132  // * Support migration to modules after v2.x.y+incompatible. Requires comparing
   133  //   packages with different module paths.
   134  // * Error when packages import from earlier major version of same module.
   135  //   (this may be intentional; look for real examples first).
   136  // * Mechanism to suppress error messages.
   137  
   138  func main() {
   139  	log.SetFlags(0)
   140  	log.SetPrefix("gorelease: ")
   141  	wd, err := os.Getwd()
   142  	if err != nil {
   143  		log.Fatal(err)
   144  	}
   145  	ctx := context.WithValue(context.Background(), "env", append(os.Environ(), "GO111MODULE=on"))
   146  	success, err := runRelease(ctx, os.Stdout, wd, os.Args[1:])
   147  	if err != nil {
   148  		if _, ok := err.(*usageError); ok {
   149  			fmt.Fprintln(os.Stderr, err)
   150  			os.Exit(2)
   151  		} else {
   152  			log.Fatal(err)
   153  		}
   154  	}
   155  	if !success {
   156  		os.Exit(1)
   157  	}
   158  }
   159  
   160  // runRelease is the main function of gorelease. It's called by tests, so
   161  // it writes to w instead of os.Stdout and returns an error instead of
   162  // exiting.
   163  func runRelease(ctx context.Context, w io.Writer, dir string, args []string) (success bool, err error) {
   164  	// Validate arguments and flags. We'll print our own errors, since we want to
   165  	// test without printing to stderr.
   166  	fs := flag.NewFlagSet("gorelease", flag.ContinueOnError)
   167  	fs.Usage = func() {}
   168  	fs.SetOutput(io.Discard)
   169  	var baseOpt, releaseVersion string
   170  	fs.StringVar(&baseOpt, "base", "", "previous version to compare against")
   171  	fs.StringVar(&releaseVersion, "version", "", "proposed version to be released")
   172  	if err := fs.Parse(args); err != nil {
   173  		return false, &usageError{err: err}
   174  	}
   175  
   176  	if len(fs.Args()) > 0 {
   177  		return false, usageErrorf("no arguments allowed")
   178  	}
   179  
   180  	if releaseVersion != "" {
   181  		if semver.Build(releaseVersion) != "" {
   182  			return false, usageErrorf("release version %q is not a canonical semantic version: build metadata is not supported", releaseVersion)
   183  		}
   184  		if c := semver.Canonical(releaseVersion); c != releaseVersion {
   185  			return false, usageErrorf("release version %q is not a canonical semantic version", releaseVersion)
   186  		}
   187  	}
   188  
   189  	var baseModPath, baseVersion string
   190  	if at := strings.Index(baseOpt, "@"); at >= 0 {
   191  		baseModPath = baseOpt[:at]
   192  		baseVersion = baseOpt[at+1:]
   193  	} else if dot, slash := strings.Index(baseOpt, "."), strings.Index(baseOpt, "/"); dot >= 0 && slash >= 0 && dot < slash {
   194  		baseModPath = baseOpt
   195  	} else {
   196  		baseVersion = baseOpt
   197  	}
   198  	if baseModPath == "" {
   199  		if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && releaseVersion != "" {
   200  			if cmp := semver.Compare(baseOpt, releaseVersion); cmp == 0 {
   201  				return false, usageErrorf("-base and -version must be different")
   202  			} else if cmp > 0 {
   203  				return false, usageErrorf("base version (%q) must be lower than release version (%q)", baseVersion, releaseVersion)
   204  			}
   205  		}
   206  	} else if baseModPath != "" && baseVersion == "none" {
   207  		return false, usageErrorf(`base version (%q) cannot have version "none" with explicit module path`, baseOpt)
   208  	}
   209  
   210  	// Find the local module and repository root directories.
   211  	modRoot, err := findModuleRoot(dir)
   212  	if err != nil {
   213  		return false, err
   214  	}
   215  	repoRoot := findRepoRoot(modRoot)
   216  
   217  	// Load packages for the version to be released from the local directory.
   218  	release, err := loadLocalModule(ctx, modRoot, repoRoot, releaseVersion)
   219  	if err != nil {
   220  		return false, err
   221  	}
   222  
   223  	// Find the base version if there is one, download it, and load packages from
   224  	// the module cache.
   225  	var max string
   226  	if baseModPath == "" {
   227  		if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && module.Check(release.modPath, baseVersion) != nil {
   228  			// Base version was specified, but it's not consistent with the release
   229  			// module path, for example, the module path is example.com/m/v2, but
   230  			// the user said -base=v1.0.0. Instead of making the user explicitly
   231  			// specify the base module path, we'll adjust the major version suffix.
   232  			prefix, _, _ := module.SplitPathVersion(release.modPath)
   233  			major := semver.Major(baseVersion)
   234  			if strings.HasPrefix(prefix, "gopkg.in/") {
   235  				baseModPath = prefix + "." + semver.Major(baseVersion)
   236  			} else if major >= "v2" {
   237  				baseModPath = prefix + "/" + major
   238  			} else {
   239  				baseModPath = prefix
   240  			}
   241  		} else {
   242  			baseModPath = release.modPath
   243  			max = releaseVersion
   244  		}
   245  	}
   246  	base, err := loadDownloadedModule(ctx, baseModPath, baseVersion, max)
   247  	if err != nil {
   248  		return false, err
   249  	}
   250  
   251  	// Compare packages and check for other issues.
   252  	report, err := makeReleaseReport(ctx, base, release)
   253  	if err != nil {
   254  		return false, err
   255  	}
   256  	if _, err := fmt.Fprint(w, report.String()); err != nil {
   257  		return false, err
   258  	}
   259  	return report.isSuccessful(), nil
   260  }
   261  
   262  type moduleInfo struct {
   263  	modRoot                  string // module root directory
   264  	repoRoot                 string // repository root directory (may be "")
   265  	modPath                  string // module path in go.mod
   266  	version                  string // resolved version or "none"
   267  	versionQuery             string // a query like "latest" or "dev-branch", if specified
   268  	versionInferred          bool   // true if the version was unspecified and inferred
   269  	highestTransitiveVersion string // version of the highest transitive self-dependency (cycle)
   270  	modPathMajor             string // major version suffix like "/v3" or ".v2"
   271  	tagPrefix                string // prefix for version tags if module not in repo root
   272  
   273  	goModPath string        // file path to go.mod
   274  	goModData []byte        // content of go.mod
   275  	goSumData []byte        // content of go.sum
   276  	goModFile *modfile.File // parsed go.mod file
   277  
   278  	diagnostics []string            // problems not related to loading specific packages
   279  	pkgs        []*packages.Package // loaded packages with type information
   280  
   281  	// Versions of this module which already exist. Only loaded for release
   282  	// (not base).
   283  	existingVersions []string
   284  }
   285  
   286  // loadLocalModule loads information about a module and its packages from a
   287  // local directory.
   288  //
   289  // modRoot is the directory containing the module's go.mod file.
   290  //
   291  // repoRoot is the root directory of the repository containing the module or "".
   292  //
   293  // version is a proposed version for the module or "".
   294  func loadLocalModule(ctx context.Context, modRoot, repoRoot, version string) (m moduleInfo, err error) {
   295  	if repoRoot != "" && !hasFilePathPrefix(modRoot, repoRoot) {
   296  		return moduleInfo{}, fmt.Errorf("module root %q is not in repository root %q", modRoot, repoRoot)
   297  	}
   298  
   299  	// Load the go.mod file and check the module path and go version.
   300  	m = moduleInfo{
   301  		modRoot:   modRoot,
   302  		repoRoot:  repoRoot,
   303  		version:   version,
   304  		goModPath: filepath.Join(modRoot, "go.mod"),
   305  	}
   306  
   307  	if version != "" && semver.Compare(version, "v0.0.0-99999999999999-zzzzzzzzzzzz") < 0 {
   308  		m.diagnostics = append(m.diagnostics, fmt.Sprintf("Version %s is lower than most pseudo-versions. Consider releasing v0.1.0-0 instead.", version))
   309  	}
   310  
   311  	m.goModData, err = os.ReadFile(m.goModPath)
   312  	if err != nil {
   313  		return moduleInfo{}, err
   314  	}
   315  	m.goModFile, err = modfile.ParseLax(m.goModPath, m.goModData, nil)
   316  	if err != nil {
   317  		return moduleInfo{}, err
   318  	}
   319  	if m.goModFile.Module == nil {
   320  		return moduleInfo{}, fmt.Errorf("%s: module directive is missing", m.goModPath)
   321  	}
   322  	m.modPath = m.goModFile.Module.Mod.Path
   323  	if err := checkModPath(m.modPath); err != nil {
   324  		return moduleInfo{}, err
   325  	}
   326  	var ok bool
   327  	_, m.modPathMajor, ok = module.SplitPathVersion(m.modPath)
   328  	if !ok {
   329  		// we just validated the path above.
   330  		panic(fmt.Sprintf("could not find version suffix in module path %q", m.modPath))
   331  	}
   332  	if m.goModFile.Go == nil {
   333  		m.diagnostics = append(m.diagnostics, "go.mod: go directive is missing")
   334  	}
   335  
   336  	// Determine the version tag prefix for the module within the repository.
   337  	if repoRoot != "" && modRoot != repoRoot {
   338  		if strings.HasPrefix(m.modPathMajor, ".") {
   339  			m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path starts with gopkg.in and must be declared in the root directory of the repository", m.modPath))
   340  		} else {
   341  			codeDir := filepath.ToSlash(modRoot[len(repoRoot)+1:])
   342  			var altGoModPath string
   343  			if m.modPathMajor == "" {
   344  				// module has no major version suffix.
   345  				// codeDir must be a suffix of modPath.
   346  				// tagPrefix is codeDir with a trailing slash.
   347  				if strings.HasSuffix(m.modPath, "/"+codeDir) {
   348  					m.tagPrefix = codeDir + "/"
   349  				} else {
   350  					m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path must end with %[2]q, since it is in subdirectory %[2]q", m.modPath, codeDir))
   351  				}
   352  			} else {
   353  				if strings.HasSuffix(m.modPath, "/"+codeDir) {
   354  					// module has a major version suffix and is in a major version subdirectory.
   355  					// codeDir must be a suffix of modPath.
   356  					// tagPrefix must not include the major version.
   357  					m.tagPrefix = codeDir[:len(codeDir)-len(m.modPathMajor)+1]
   358  					altGoModPath = modRoot[:len(modRoot)-len(m.modPathMajor)+1] + "go.mod"
   359  				} else if strings.HasSuffix(m.modPath, "/"+codeDir+m.modPathMajor) {
   360  					// module has a major version suffix and is not in a major version subdirectory.
   361  					// codeDir + modPathMajor is a suffix of modPath.
   362  					// tagPrefix is codeDir with a trailing slash.
   363  					m.tagPrefix = codeDir + "/"
   364  					altGoModPath = filepath.Join(modRoot, m.modPathMajor[1:], "go.mod")
   365  				} else {
   366  					m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path must end with %[2]q or %q, since it is in subdirectory %[2]q", m.modPath, codeDir, codeDir+m.modPathMajor))
   367  				}
   368  			}
   369  
   370  			// Modules with major version suffixes can be defined in two places
   371  			// (e.g., sub/go.mod and sub/v2/go.mod). They must not be defined in both.
   372  			if altGoModPath != "" {
   373  				if data, err := os.ReadFile(altGoModPath); err == nil {
   374  					if altModPath := modfile.ModulePath(data); m.modPath == altModPath {
   375  						goModRel, _ := filepath.Rel(repoRoot, m.goModPath)
   376  						altGoModRel, _ := filepath.Rel(repoRoot, altGoModPath)
   377  						m.diagnostics = append(m.diagnostics, fmt.Sprintf("module is defined in two locations:\n\t%s\n\t%s", goModRel, altGoModRel))
   378  					}
   379  				}
   380  			}
   381  		}
   382  	}
   383  
   384  	// Load the module's packages.
   385  	// We pack the module into a zip file and extract it to a temporary directory
   386  	// as if it were published and downloaded. We'll detect any errors that would
   387  	// occur (for example, invalid file names). We avoid loading it as the
   388  	// main module.
   389  	tmpModRoot, err := copyModuleToTempDir(repoRoot, m.modPath, m.modRoot)
   390  	if err != nil {
   391  		return moduleInfo{}, err
   392  	}
   393  	defer func() {
   394  		if rerr := os.RemoveAll(tmpModRoot); err == nil && rerr != nil {
   395  			err = fmt.Errorf("removing temporary module directory: %v", rerr)
   396  		}
   397  	}()
   398  	tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, prepareDiagnostics, err := prepareLoadDir(ctx, m.goModFile, m.modPath, tmpModRoot, version, false)
   399  	if err != nil {
   400  		return moduleInfo{}, err
   401  	}
   402  	defer func() {
   403  		if rerr := os.RemoveAll(tmpLoadDir); err == nil && rerr != nil {
   404  			err = fmt.Errorf("removing temporary load directory: %v", rerr)
   405  		}
   406  	}()
   407  
   408  	var loadDiagnostics []string
   409  	m.pkgs, loadDiagnostics, err = loadPackages(ctx, m.modPath, tmpModRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths)
   410  	if err != nil {
   411  		return moduleInfo{}, err
   412  	}
   413  
   414  	m.diagnostics = append(m.diagnostics, prepareDiagnostics...)
   415  	m.diagnostics = append(m.diagnostics, loadDiagnostics...)
   416  
   417  	highestVersion, err := findSelectedVersion(ctx, tmpLoadDir, m.modPath)
   418  	if err != nil {
   419  		return moduleInfo{}, err
   420  	}
   421  
   422  	if highestVersion != "" {
   423  		// A version of the module is included in the transitive dependencies.
   424  		// Add it to the moduleInfo so that the release report stage can use it
   425  		// in verifying the version or suggestion a new version, depending on
   426  		// whether the user provided a version already.
   427  		m.highestTransitiveVersion = highestVersion
   428  	}
   429  
   430  	retracted, err := loadRetractions(ctx, tmpLoadDir)
   431  	if err != nil {
   432  		return moduleInfo{}, err
   433  	}
   434  	m.diagnostics = append(m.diagnostics, retracted...)
   435  
   436  	return m, nil
   437  }
   438  
   439  // loadDownloadedModule downloads a module and loads information about it and
   440  // its packages from the module cache.
   441  //
   442  // modPath is the module path used to fetch the module. The module's path in
   443  // go.mod (m.modPath) may be different, for example in a soft fork intended as
   444  // a replacement.
   445  //
   446  // version is the version to load. It may be "none" (indicating nothing should
   447  // be loaded), "" (the highest available version below max should be used), a
   448  // version query (to be resolved with 'go list'), or a canonical version.
   449  //
   450  // If version is "" and max is not "", available versions greater than or equal
   451  // to max will not be considered. Typically, loadDownloadedModule is used to
   452  // load the base version, and max is the release version.
   453  func loadDownloadedModule(ctx context.Context, modPath, version, max string) (m moduleInfo, err error) {
   454  	// Check the module path and version.
   455  	// If the version is a query, resolve it to a canonical version.
   456  	m = moduleInfo{modPath: modPath}
   457  	if err := checkModPath(modPath); err != nil {
   458  		return moduleInfo{}, err
   459  	}
   460  
   461  	var ok bool
   462  	_, m.modPathMajor, ok = module.SplitPathVersion(modPath)
   463  	if !ok {
   464  		// we just validated the path above.
   465  		panic(fmt.Sprintf("could not find version suffix in module path %q", modPath))
   466  	}
   467  
   468  	if version == "none" {
   469  		// We don't have a base version to compare against.
   470  		m.version = "none"
   471  		return m, nil
   472  	}
   473  	if version == "" {
   474  		// Unspecified version: use the highest version below max.
   475  		m.versionInferred = true
   476  		if m.version, err = inferBaseVersion(ctx, modPath, max); err != nil {
   477  			return moduleInfo{}, err
   478  		}
   479  		if m.version == "none" {
   480  			return m, nil
   481  		}
   482  	} else if version != module.CanonicalVersion(version) {
   483  		// Version query: find the real version.
   484  		m.versionQuery = version
   485  		if m.version, err = queryVersion(ctx, modPath, version); err != nil {
   486  			return moduleInfo{}, err
   487  		}
   488  		if m.version != "none" && max != "" && semver.Compare(m.version, max) >= 0 {
   489  			// TODO(jayconrod): reconsider this comparison for pseudo-versions in
   490  			// general. A query might match different pseudo-versions over time,
   491  			// depending on ancestor versions, so this might start failing with
   492  			// no local change.
   493  			return moduleInfo{}, fmt.Errorf("base version %s (%s) must be lower than release version %s", m.version, m.versionQuery, max)
   494  		}
   495  	} else {
   496  		// Canonical version: make sure it matches the module path.
   497  		if err := module.CheckPathMajor(version, m.modPathMajor); err != nil {
   498  			// TODO(golang.org/issue/39666): don't assume this is the base version
   499  			// or that we're comparing across major versions.
   500  			return moduleInfo{}, fmt.Errorf("can't compare major versions: base version %s does not belong to module %s", version, modPath)
   501  		}
   502  		m.version = version
   503  	}
   504  
   505  	// Download the module into the cache and load the mod file.
   506  	// Note that goModPath is $GOMODCACHE/cache/download/$modPath/@v/$version.mod,
   507  	// which is not inside modRoot. This is what the go command uses. Even if
   508  	// the module didn't have a go.mod file, one will be synthesized there.
   509  	v := module.Version{Path: modPath, Version: m.version}
   510  	if m.modRoot, m.goModPath, err = downloadModule(ctx, v); err != nil {
   511  		return moduleInfo{}, err
   512  	}
   513  	if m.goModData, err = os.ReadFile(m.goModPath); err != nil {
   514  		return moduleInfo{}, err
   515  	}
   516  	if m.goModFile, err = modfile.ParseLax(m.goModPath, m.goModData, nil); err != nil {
   517  		return moduleInfo{}, err
   518  	}
   519  	if m.goModFile.Module == nil {
   520  		return moduleInfo{}, fmt.Errorf("%s: missing module directive", m.goModPath)
   521  	}
   522  	m.modPath = m.goModFile.Module.Mod.Path
   523  
   524  	// Load packages.
   525  	tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, _, err := prepareLoadDir(ctx, nil, m.modPath, m.modRoot, m.version, true)
   526  	if err != nil {
   527  		return moduleInfo{}, err
   528  	}
   529  	defer func() {
   530  		if rerr := os.RemoveAll(tmpLoadDir); err == nil && rerr != nil {
   531  			err = fmt.Errorf("removing temporary load directory: %v", err)
   532  		}
   533  	}()
   534  
   535  	if m.pkgs, _, err = loadPackages(ctx, m.modPath, m.modRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths); err != nil {
   536  		return moduleInfo{}, err
   537  	}
   538  
   539  	// Calculate the existing versions.
   540  	ev, err := existingVersions(ctx, m.modPath, tmpLoadDir)
   541  	if err != nil {
   542  		return moduleInfo{}, err
   543  	}
   544  	m.existingVersions = ev
   545  
   546  	return m, nil
   547  }
   548  
   549  // makeReleaseReport returns a report comparing the current version of a
   550  // module with a previously released version. The report notes any backward
   551  // compatible and incompatible changes in the module's public API. It also
   552  // diagnoses common problems, such as go.mod or go.sum being incomplete.
   553  // The report recommends or validates a release version and indicates a
   554  // version control tag to use (with an appropriate prefix, for modules not
   555  // in the repository root directory).
   556  func makeReleaseReport(ctx context.Context, base, release moduleInfo) (report, error) {
   557  	// TODO: use apidiff.ModuleChanges.
   558  	// Compare each pair of packages.
   559  	// Ignore internal packages.
   560  	// If we don't have a base version to compare against just check the new
   561  	// packages for errors.
   562  	shouldCompare := base.version != "none"
   563  	isInternal := func(modPath, pkgPath string) bool {
   564  		if !hasPathPrefix(pkgPath, modPath) {
   565  			panic(fmt.Sprintf("package %s not in module %s", pkgPath, modPath))
   566  		}
   567  		for pkgPath != modPath {
   568  			if path.Base(pkgPath) == "internal" {
   569  				return true
   570  			}
   571  			pkgPath = path.Dir(pkgPath)
   572  		}
   573  		return false
   574  	}
   575  	r := report{
   576  		base:    base,
   577  		release: release,
   578  	}
   579  	for _, pair := range zipPackages(base.modPath, base.pkgs, release.modPath, release.pkgs) {
   580  		basePkg, releasePkg := pair.base, pair.release
   581  		switch {
   582  		case releasePkg == nil:
   583  			// Package removed
   584  			if internal := isInternal(base.modPath, basePkg.PkgPath); !internal || len(basePkg.Errors) > 0 {
   585  				pr := packageReport{
   586  					path:       basePkg.PkgPath,
   587  					baseErrors: basePkg.Errors,
   588  				}
   589  				if !internal {
   590  					pr.Report = apidiff.Report{
   591  						Changes: []apidiff.Change{{
   592  							Message:    "package removed",
   593  							Compatible: false,
   594  						}},
   595  					}
   596  				}
   597  				r.addPackage(pr)
   598  			}
   599  
   600  		case basePkg == nil:
   601  			// Package added
   602  			if internal := isInternal(release.modPath, releasePkg.PkgPath); !internal && shouldCompare || len(releasePkg.Errors) > 0 {
   603  				pr := packageReport{
   604  					path:          releasePkg.PkgPath,
   605  					releaseErrors: releasePkg.Errors,
   606  				}
   607  				if !internal && shouldCompare {
   608  					// If we aren't comparing against a base version, don't say
   609  					// "package added". Only report packages with errors.
   610  					pr.Report = apidiff.Report{
   611  						Changes: []apidiff.Change{{
   612  							Message:    "package added",
   613  							Compatible: true,
   614  						}},
   615  					}
   616  				}
   617  				r.addPackage(pr)
   618  			}
   619  
   620  		default:
   621  			// Matched packages
   622  			// Both packages are internal or neither; we only consider path components
   623  			// after the module path.
   624  			internal := isInternal(release.modPath, releasePkg.PkgPath)
   625  			if !internal && basePkg.Name != "main" && releasePkg.Name != "main" {
   626  				pr := packageReport{
   627  					path:          basePkg.PkgPath,
   628  					baseErrors:    basePkg.Errors,
   629  					releaseErrors: releasePkg.Errors,
   630  					Report:        apidiff.Changes(basePkg.Types, releasePkg.Types),
   631  				}
   632  				r.addPackage(pr)
   633  			}
   634  		}
   635  	}
   636  
   637  	if r.canVerifyReleaseVersion() {
   638  		if release.version == "" {
   639  			r.suggestReleaseVersion()
   640  		} else {
   641  			r.validateReleaseVersion()
   642  		}
   643  	}
   644  
   645  	return r, nil
   646  }
   647  
   648  // existingVersions returns the versions that already exist for the given
   649  // modPath.
   650  func existingVersions(ctx context.Context, modPath, modRoot string) (versions []string, err error) {
   651  	defer func() {
   652  		if err != nil {
   653  			err = fmt.Errorf("listing versions of %s: %w", modPath, err)
   654  		}
   655  	}()
   656  
   657  	type listVersions struct {
   658  		Versions []string
   659  	}
   660  	cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-versions", modPath)
   661  	cmd.Env = copyEnv(ctx, cmd.Env)
   662  	cmd.Dir = modRoot
   663  	out, err := cmd.Output()
   664  	if err != nil {
   665  		return nil, cleanCmdError(err)
   666  	}
   667  	if len(out) == 0 {
   668  		return nil, nil
   669  	}
   670  
   671  	var lv listVersions
   672  	if err := json.Unmarshal(out, &lv); err != nil {
   673  		return nil, err
   674  	}
   675  	return lv.Versions, nil
   676  }
   677  
   678  // findRepoRoot finds the root directory of the repository that contains dir.
   679  // findRepoRoot returns "" if it can't find the repository root.
   680  func findRepoRoot(dir string) string {
   681  	vcsDirs := []string{".git", ".hg", ".svn", ".bzr"}
   682  	d := filepath.Clean(dir)
   683  	for {
   684  		for _, vcsDir := range vcsDirs {
   685  			if _, err := os.Stat(filepath.Join(d, vcsDir)); err == nil {
   686  				return d
   687  			}
   688  		}
   689  		parent := filepath.Dir(d)
   690  		if parent == d {
   691  			return ""
   692  		}
   693  		d = parent
   694  	}
   695  }
   696  
   697  // findModuleRoot finds the root directory of the module that contains dir.
   698  func findModuleRoot(dir string) (string, error) {
   699  	d := filepath.Clean(dir)
   700  	for {
   701  		if fi, err := os.Stat(filepath.Join(d, "go.mod")); err == nil && !fi.IsDir() {
   702  			return dir, nil
   703  		}
   704  		parent := filepath.Dir(d)
   705  		if parent == d {
   706  			break
   707  		}
   708  		d = parent
   709  	}
   710  	return "", fmt.Errorf("%s: cannot find go.mod file", dir)
   711  }
   712  
   713  // checkModPath is like golang.org/x/mod/module.CheckPath, but it returns
   714  // friendlier error messages for common mistakes.
   715  //
   716  // TODO(jayconrod): update module.CheckPath and delete this function.
   717  func checkModPath(modPath string) error {
   718  	if path.IsAbs(modPath) || filepath.IsAbs(modPath) {
   719  		// TODO(jayconrod): improve error message in x/mod instead of checking here.
   720  		return fmt.Errorf("module path %q must not be an absolute path.\nIt must be an address where your module may be found.", modPath)
   721  	}
   722  	if suffix := dirMajorSuffix(modPath); suffix == "v0" || suffix == "v1" {
   723  		return fmt.Errorf("module path %q has major version suffix %q.\nA major version suffix is only allowed for v2 or later.", modPath, suffix)
   724  	} else if strings.HasPrefix(suffix, "v0") {
   725  		return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not have a leading zero.", modPath, suffix)
   726  	} else if strings.ContainsRune(suffix, '.') {
   727  		return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not contain dots.", modPath, suffix)
   728  	}
   729  	return module.CheckPath(modPath)
   730  }
   731  
   732  // inferBaseVersion returns an appropriate base version if one was not specified
   733  // explicitly.
   734  //
   735  // If max is not "", inferBaseVersion returns the highest available release
   736  // version of the module lower than max. Otherwise, inferBaseVersion returns the
   737  // highest available release version. Pre-release versions are not considered.
   738  // If there is no available version, and max appears to be the first release
   739  // version (for example, "v0.1.0", "v2.0.0"), "none" is returned.
   740  func inferBaseVersion(ctx context.Context, modPath, max string) (baseVersion string, err error) {
   741  	defer func() {
   742  		if err != nil {
   743  			err = &baseVersionError{err: err, modPath: modPath}
   744  		}
   745  	}()
   746  
   747  	versions, err := loadVersions(ctx, modPath)
   748  	if err != nil {
   749  		return "", err
   750  	}
   751  
   752  	for i := len(versions) - 1; i >= 0; i-- {
   753  		v := versions[i]
   754  		if semver.Prerelease(v) == "" &&
   755  			(max == "" || semver.Compare(v, max) < 0) {
   756  			return v, nil
   757  		}
   758  	}
   759  
   760  	if max == "" || maybeFirstVersion(max) {
   761  		return "none", nil
   762  	}
   763  	return "", fmt.Errorf("no versions found lower than %s", max)
   764  }
   765  
   766  // queryVersion returns the canonical version for a given module version query.
   767  func queryVersion(ctx context.Context, modPath, query string) (resolved string, err error) {
   768  	defer func() {
   769  		if err != nil {
   770  			err = fmt.Errorf("could not resolve version %s@%s: %w", modPath, query, err)
   771  		}
   772  	}()
   773  	if query == "upgrade" || query == "patch" {
   774  		return "", errors.New("query is based on requirements in main go.mod file")
   775  	}
   776  
   777  	tmpDir, err := os.MkdirTemp("", "")
   778  	if err != nil {
   779  		return "", err
   780  	}
   781  	defer func() {
   782  		if rerr := os.Remove(tmpDir); rerr != nil && err == nil {
   783  			err = rerr
   784  		}
   785  	}()
   786  	arg := modPath + "@" + query
   787  	cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", arg)
   788  	cmd.Env = copyEnv(ctx, cmd.Env)
   789  	cmd.Dir = tmpDir
   790  	cmd.Env = append(cmd.Env, "GO111MODULE=on")
   791  	out, err := cmd.Output()
   792  	if err != nil {
   793  		return "", cleanCmdError(err)
   794  	}
   795  	return strings.TrimSpace(string(out)), nil
   796  }
   797  
   798  // loadVersions loads the list of versions for the given module using
   799  // 'go list -m -versions'. The returned versions are sorted in ascending
   800  // semver order.
   801  func loadVersions(ctx context.Context, modPath string) (versions []string, err error) {
   802  	defer func() {
   803  		if err != nil {
   804  			err = fmt.Errorf("could not load versions for %s: %v", modPath, err)
   805  		}
   806  	}()
   807  
   808  	tmpDir, err := os.MkdirTemp("", "")
   809  	if err != nil {
   810  		return nil, err
   811  	}
   812  	defer func() {
   813  		if rerr := os.Remove(tmpDir); rerr != nil && err == nil {
   814  			err = rerr
   815  		}
   816  	}()
   817  	cmd := exec.CommandContext(ctx, "go", "list", "-m", "-versions", "--", modPath)
   818  	cmd.Env = copyEnv(ctx, cmd.Env)
   819  	cmd.Dir = tmpDir
   820  	out, err := cmd.Output()
   821  	if err != nil {
   822  		return nil, cleanCmdError(err)
   823  	}
   824  	versions = strings.Fields(string(out))
   825  	if len(versions) > 0 {
   826  		versions = versions[1:] // skip module path
   827  	}
   828  
   829  	// Sort versions defensively. 'go list -m -versions' should always returns
   830  	// a sorted list of versions, but it's fast and easy to sort them here, too.
   831  	sort.Slice(versions, func(i, j int) bool {
   832  		return semver.Compare(versions[i], versions[j]) < 0
   833  	})
   834  	return versions, nil
   835  }
   836  
   837  // maybeFirstVersion returns whether v appears to be the first version
   838  // of a module.
   839  func maybeFirstVersion(v string) bool {
   840  	major, minor, patch, _, _, err := parseVersion(v)
   841  	if err != nil {
   842  		return false
   843  	}
   844  	if major == "0" {
   845  		return minor == "0" && patch == "0" ||
   846  			minor == "0" && patch == "1" ||
   847  			minor == "1" && patch == "0"
   848  	}
   849  	return minor == "0" && patch == "0"
   850  }
   851  
   852  // dirMajorSuffix returns a major version suffix for a slash-separated path.
   853  // For example, for the path "foo/bar/v2", dirMajorSuffix would return "v2".
   854  // If no major version suffix is found, "" is returned.
   855  //
   856  // dirMajorSuffix is less strict than module.SplitPathVersion so that incorrect
   857  // suffixes like "v0", "v02", "v1.2" can be detected. It doesn't handle
   858  // special cases for gopkg.in paths.
   859  func dirMajorSuffix(path string) string {
   860  	i := len(path)
   861  	for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') || path[i-1] == '.' {
   862  		i--
   863  	}
   864  	if i <= 1 || i == len(path) || path[i-1] != 'v' || (i > 1 && path[i-2] != '/') {
   865  		return ""
   866  	}
   867  	return path[i-1:]
   868  }
   869  
   870  // copyModuleToTempDir copies module files from modRoot to a subdirectory of
   871  // scratchDir. Submodules, vendor directories, and irregular files are excluded.
   872  // An error is returned if the module contains any files or directories that
   873  // can't be included in a module zip file (due to special characters,
   874  // excessive sizes, etc.).
   875  func copyModuleToTempDir(repoRoot, modPath, modRoot string) (dir string, err error) {
   876  	// Generate a fake version consistent with modPath. We need a canonical
   877  	// version to create a zip file.
   878  	version := "v0.0.0-gorelease"
   879  	_, majorPathSuffix, _ := module.SplitPathVersion(modPath)
   880  	if majorPathSuffix != "" {
   881  		version = majorPathSuffix[1:] + ".0.0-gorelease"
   882  	}
   883  	m := module.Version{Path: modPath, Version: version}
   884  
   885  	zipFile, err := os.CreateTemp("", "gorelease-*.zip")
   886  	if err != nil {
   887  		return "", err
   888  	}
   889  	defer func() {
   890  		zipFile.Close()
   891  		os.Remove(zipFile.Name())
   892  	}()
   893  
   894  	dir, err = os.MkdirTemp("", "gorelease")
   895  	if err != nil {
   896  		return "", err
   897  	}
   898  	defer func() {
   899  		if err != nil {
   900  			os.RemoveAll(dir)
   901  			dir = ""
   902  		}
   903  	}()
   904  
   905  	var fallbackToDir bool
   906  	if repoRoot != "" {
   907  		var err error
   908  		fallbackToDir, err = tryCreateFromVCS(zipFile, m, modRoot, repoRoot)
   909  		if err != nil {
   910  			return "", err
   911  		}
   912  	}
   913  
   914  	if repoRoot == "" || fallbackToDir {
   915  		// Not a recognised repo: fall back to creating from dir.
   916  		if err := zip.CreateFromDir(zipFile, m, modRoot); err != nil {
   917  			var e zip.FileErrorList
   918  			if errors.As(err, &e) {
   919  				return "", e
   920  			}
   921  			return "", err
   922  		}
   923  	}
   924  
   925  	if err := zipFile.Close(); err != nil {
   926  		return "", err
   927  	}
   928  	if err := zip.Unzip(dir, m, zipFile.Name()); err != nil {
   929  		return "", err
   930  	}
   931  	return dir, nil
   932  }
   933  
   934  // tryCreateFromVCS tries to create a module zip file from VCS. If it succeeds,
   935  // it returns fallBackToDir false and a nil err. If it fails in a recoverable
   936  // way, it returns fallBackToDir true and a nil err. If it fails in an
   937  // unrecoverable way, it returns a non-nil err.
   938  func tryCreateFromVCS(zipFile io.Writer, m module.Version, modRoot, repoRoot string) (fallbackToDir bool, _ error) {
   939  	// We recognised a repo: create from VCS.
   940  	if !hasFilePathPrefix(modRoot, repoRoot) {
   941  		panic(fmt.Sprintf("repo root %q is not a prefix of mod root %q", repoRoot, modRoot))
   942  	}
   943  	hasUncommitted, err := hasGitUncommittedChanges(repoRoot)
   944  	if err != nil {
   945  		// Fallback to CreateFromDir.
   946  		return true, nil
   947  	}
   948  	if hasUncommitted {
   949  		return false, fmt.Errorf("repo %s has uncommitted changes", repoRoot)
   950  	}
   951  	modRel := filepath.ToSlash(trimFilePathPrefix(modRoot, repoRoot))
   952  	if err := zip.CreateFromVCS(zipFile, m, repoRoot, "HEAD", modRel); err != nil {
   953  		var fel zip.FileErrorList
   954  		if errors.As(err, &fel) {
   955  			return false, fel
   956  		}
   957  		var uve *zip.UnrecognizedVCSError
   958  		if errors.As(err, &uve) {
   959  			// Fallback to CreateFromDir.
   960  			return true, nil
   961  		}
   962  		return false, err
   963  	}
   964  	// Success!
   965  	return false, nil
   966  }
   967  
   968  // downloadModule downloads a specific version of a module to the
   969  // module cache using 'go mod download'.
   970  func downloadModule(ctx context.Context, m module.Version) (modRoot, goModPath string, err error) {
   971  	defer func() {
   972  		if err != nil {
   973  			err = &downloadError{m: m, err: cleanCmdError(err)}
   974  		}
   975  	}()
   976  
   977  	// Run 'go mod download' from a temporary directory to avoid needing to load
   978  	// go.mod from gorelease's working directory (or a parent).
   979  	// go.mod may be broken, and we don't need it.
   980  	// TODO(golang.org/issue/36812): 'go mod download' reads go.mod even though
   981  	// we don't need information about the main module or the build list.
   982  	// If it didn't read go.mod in this case, we wouldn't need a temp directory.
   983  	tmpDir, err := os.MkdirTemp("", "gorelease-download")
   984  	if err != nil {
   985  		return "", "", err
   986  	}
   987  	defer os.Remove(tmpDir)
   988  	cmd := exec.CommandContext(ctx, "go", "mod", "download", "-json", "--", m.Path+"@"+m.Version)
   989  	cmd.Env = copyEnv(ctx, cmd.Env)
   990  	cmd.Dir = tmpDir
   991  	out, err := cmd.Output()
   992  	var xerr *exec.ExitError
   993  	if err != nil {
   994  		var ok bool
   995  		if xerr, ok = err.(*exec.ExitError); !ok {
   996  			return "", "", err
   997  		}
   998  	}
   999  
  1000  	// If 'go mod download' exited unsuccessfully but printed well-formed JSON
  1001  	// with an error, return that error.
  1002  	parsed := struct{ Dir, GoMod, Error string }{}
  1003  	if jsonErr := json.Unmarshal(out, &parsed); jsonErr != nil {
  1004  		if xerr != nil {
  1005  			return "", "", cleanCmdError(xerr)
  1006  		}
  1007  		return "", "", jsonErr
  1008  	}
  1009  	if parsed.Error != "" {
  1010  		return "", "", errors.New(parsed.Error)
  1011  	}
  1012  	if xerr != nil {
  1013  		return "", "", cleanCmdError(xerr)
  1014  	}
  1015  	return parsed.Dir, parsed.GoMod, nil
  1016  }
  1017  
  1018  // prepareLoadDir creates a temporary directory and a go.mod file that requires
  1019  // the module being loaded. go.sum is copied if present. It also creates a .go
  1020  // file that imports every package in the given modPath. This temporary module
  1021  // is useful for two reasons. First, replace and exclude directives from the
  1022  // target module aren't applied, so we have the same view as a dependent module.
  1023  // Second, we can run commands like 'go get' without modifying the original
  1024  // go.mod and go.sum files.
  1025  //
  1026  // modFile is the pre-parsed go.mod file. If non-nil, its requirements and
  1027  // go version will be copied so that incomplete and out-of-date requirements
  1028  // may be reported later.
  1029  //
  1030  // modPath is the module's path.
  1031  //
  1032  // modRoot is the module's root directory.
  1033  //
  1034  // version is the version of the module being loaded. If must be canonical
  1035  // for modules loaded from the cache. Otherwise, it may be empty (for example,
  1036  // when no release version is proposed).
  1037  //
  1038  // cached indicates whether the module is being loaded from the module cache.
  1039  // If cached is true, then the module lives in the cache at
  1040  // $GOMODCACHE/$modPath@$version/. Its go.mod file is at
  1041  // $GOMODCACHE/cache/download/$modPath/@v/$version.mod. It must be referenced
  1042  // with a simple require. A replace directive won't work because it may not have
  1043  // a go.mod file in modRoot.
  1044  // If cached is false, then modRoot is somewhere outside the module cache
  1045  // (ex /tmp). We'll reference it with a local replace directive. It must have a
  1046  // go.mod file in modRoot.
  1047  //
  1048  // dir is the location of the temporary directory.
  1049  //
  1050  // goModData and goSumData are the contents of the go.mod and go.sum files,
  1051  // respectively.
  1052  //
  1053  // pkgPaths are the import paths of the module being loaded, including the path
  1054  // to any main packages (as if they were importable).
  1055  func prepareLoadDir(ctx context.Context, modFile *modfile.File, modPath, modRoot, version string, cached bool) (dir string, goModData, goSumData []byte, pkgPaths []string, diagnostics []string, err error) {
  1056  	defer func() {
  1057  		if err != nil {
  1058  			if cached {
  1059  				err = fmt.Errorf("preparing to load packages for %s@%s: %w", modPath, version, err)
  1060  			} else {
  1061  				err = fmt.Errorf("preparing to load packages for %s: %w", modPath, err)
  1062  			}
  1063  		}
  1064  	}()
  1065  
  1066  	if module.Check(modPath, version) != nil {
  1067  		// If no version is proposed or if the version isn't valid, use a fake
  1068  		// version that matches the module's major version suffix. If the version
  1069  		// is invalid, that will be reported elsewhere.
  1070  		version = "v0.0.0-gorelease"
  1071  		if _, pathMajor, _ := module.SplitPathVersion(modPath); pathMajor != "" {
  1072  			version = pathMajor[1:] + ".0.0-gorelease"
  1073  		}
  1074  	}
  1075  
  1076  	dir, err = os.MkdirTemp("", "gorelease-load")
  1077  	if err != nil {
  1078  		return "", nil, nil, nil, nil, err
  1079  	}
  1080  
  1081  	f := &modfile.File{}
  1082  	f.AddModuleStmt("gorelease-load-module")
  1083  	f.AddRequire(modPath, version)
  1084  	if !cached {
  1085  		f.AddReplace(modPath, version, modRoot, "")
  1086  	}
  1087  	if modFile != nil {
  1088  		if modFile.Go != nil {
  1089  			f.AddGoStmt(modFile.Go.Version)
  1090  		}
  1091  		for _, r := range modFile.Require {
  1092  			f.AddRequire(r.Mod.Path, r.Mod.Version)
  1093  		}
  1094  	}
  1095  	goModData, err = f.Format()
  1096  	if err != nil {
  1097  		return "", nil, nil, nil, nil, err
  1098  	}
  1099  	if err := os.WriteFile(filepath.Join(dir, "go.mod"), goModData, 0666); err != nil {
  1100  		return "", nil, nil, nil, nil, err
  1101  	}
  1102  
  1103  	goSumData, err = os.ReadFile(filepath.Join(modRoot, "go.sum"))
  1104  	if err != nil && !os.IsNotExist(err) {
  1105  		return "", nil, nil, nil, nil, err
  1106  	}
  1107  	if err := os.WriteFile(filepath.Join(dir, "go.sum"), goSumData, 0666); err != nil {
  1108  		return "", nil, nil, nil, nil, err
  1109  	}
  1110  
  1111  	// Add a .go file with requirements, so that `go get` won't blat
  1112  	// requirements.
  1113  	fakeImports := &strings.Builder{}
  1114  	fmt.Fprint(fakeImports, "package tmp\n")
  1115  	imps, err := collectImportPaths(modPath, modRoot)
  1116  	if err != nil {
  1117  		return "", nil, nil, nil, nil, err
  1118  	}
  1119  	for _, imp := range imps {
  1120  		fmt.Fprintf(fakeImports, "import _ %q\n", imp)
  1121  	}
  1122  	if err := os.WriteFile(filepath.Join(dir, "tmp.go"), []byte(fakeImports.String()), 0666); err != nil {
  1123  		return "", nil, nil, nil, nil, err
  1124  	}
  1125  
  1126  	// Add missing requirements.
  1127  	cmd := exec.CommandContext(ctx, "go", "get", "-d", ".")
  1128  	cmd.Env = copyEnv(ctx, cmd.Env)
  1129  	cmd.Dir = dir
  1130  	if _, err := cmd.Output(); err != nil {
  1131  		return "", nil, nil, nil, nil, fmt.Errorf("looking for missing dependencies: %w", cleanCmdError(err))
  1132  	}
  1133  
  1134  	// Report new requirements in go.mod.
  1135  	goModPath := filepath.Join(dir, "go.mod")
  1136  	loadReqs := func(data []byte) (reqs []module.Version, err error) {
  1137  		modFile, err := modfile.ParseLax(goModPath, data, nil)
  1138  		if err != nil {
  1139  			return nil, err
  1140  		}
  1141  		for _, r := range modFile.Require {
  1142  			reqs = append(reqs, r.Mod)
  1143  		}
  1144  		return reqs, nil
  1145  	}
  1146  
  1147  	oldReqs, err := loadReqs(goModData)
  1148  	if err != nil {
  1149  		return "", nil, nil, nil, nil, err
  1150  	}
  1151  	newGoModData, err := os.ReadFile(goModPath)
  1152  	if err != nil {
  1153  		return "", nil, nil, nil, nil, err
  1154  	}
  1155  	newReqs, err := loadReqs(newGoModData)
  1156  	if err != nil {
  1157  		return "", nil, nil, nil, nil, err
  1158  	}
  1159  
  1160  	oldMap := make(map[module.Version]bool)
  1161  	for _, req := range oldReqs {
  1162  		oldMap[req] = true
  1163  	}
  1164  	var missing []module.Version
  1165  	for _, req := range newReqs {
  1166  		// Ignore cyclic imports, since a module never needs to require itself.
  1167  		if req.Path == modPath {
  1168  			continue
  1169  		}
  1170  		if !oldMap[req] {
  1171  			missing = append(missing, req)
  1172  		}
  1173  	}
  1174  
  1175  	if len(missing) > 0 {
  1176  		var missingReqs []string
  1177  		for _, m := range missing {
  1178  			missingReqs = append(missingReqs, m.String())
  1179  		}
  1180  		diagnostics = append(diagnostics, fmt.Sprintf("go.mod: the following requirements are needed\n\t%s\nRun 'go mod tidy' to add missing requirements.", strings.Join(missingReqs, "\n\t")))
  1181  		return dir, goModData, goSumData, imps, diagnostics, nil
  1182  	}
  1183  
  1184  	// Cached modules may have no go.sum.
  1185  	// We skip comparison because a downloaded module is outside the user's
  1186  	// control.
  1187  	if !cached {
  1188  		// Check if 'go get' added new hashes to go.sum.
  1189  		goSumPath := filepath.Join(dir, "go.sum")
  1190  		newGoSumData, err := os.ReadFile(goSumPath)
  1191  		if err != nil {
  1192  			if !os.IsNotExist(err) {
  1193  				return "", nil, nil, nil, nil, err
  1194  			}
  1195  			// If the sum doesn't exist, that's ok: we'll treat "no go.sum" like
  1196  			// "empty go.sum".
  1197  		}
  1198  
  1199  		if !sumsMatchIgnoringPath(string(goSumData), string(newGoSumData), modPath) {
  1200  			diagnostics = append(diagnostics, "go.sum: one or more sums are missing. Run 'go mod tidy' to add missing sums.")
  1201  		}
  1202  	}
  1203  
  1204  	return dir, goModData, goSumData, imps, diagnostics, nil
  1205  }
  1206  
  1207  // sumsMatchIgnoringPath checks whether the two sums match. It ignores any lines
  1208  // which contains the given modPath.
  1209  func sumsMatchIgnoringPath(sum1, sum2, modPathToIgnore string) bool {
  1210  	lines1 := make(map[string]bool)
  1211  	for _, line := range strings.Split(string(sum1), "\n") {
  1212  		if line == "" {
  1213  			continue
  1214  		}
  1215  		lines1[line] = true
  1216  	}
  1217  	for _, line := range strings.Split(string(sum2), "\n") {
  1218  		if line == "" {
  1219  			continue
  1220  		}
  1221  		parts := strings.Fields(line)
  1222  		if len(parts) < 1 {
  1223  			panic(fmt.Sprintf("go.sum malformed: unexpected line %s", line))
  1224  		}
  1225  		if parts[0] == modPathToIgnore {
  1226  			continue
  1227  		}
  1228  
  1229  		if !lines1[line] {
  1230  			return false
  1231  		}
  1232  	}
  1233  
  1234  	lines2 := make(map[string]bool)
  1235  	for _, line := range strings.Split(string(sum2), "\n") {
  1236  		if line == "" {
  1237  			continue
  1238  		}
  1239  		lines2[line] = true
  1240  	}
  1241  	for _, line := range strings.Split(string(sum1), "\n") {
  1242  		if line == "" {
  1243  			continue
  1244  		}
  1245  		parts := strings.Fields(line)
  1246  		if len(parts) < 1 {
  1247  			panic(fmt.Sprintf("go.sum malformed: unexpected line %s", line))
  1248  		}
  1249  		if parts[0] == modPathToIgnore {
  1250  			continue
  1251  		}
  1252  
  1253  		if !lines2[line] {
  1254  			return false
  1255  		}
  1256  	}
  1257  
  1258  	return true
  1259  }
  1260  
  1261  // collectImportPaths visits the given root and traverses its directories
  1262  // recursively, collecting the import paths of all importable packages in each
  1263  // directory along the way.
  1264  //
  1265  // modPath is the module path.
  1266  // root is the root directory of the module to collect imports for (the root
  1267  // of the modPath module).
  1268  //
  1269  // Note: the returned importPaths will include main if it exists in root.
  1270  func collectImportPaths(modPath, root string) (importPaths []string, _ error) {
  1271  	err := filepath.Walk(root, func(walkPath string, fi os.FileInfo, err error) error {
  1272  		if err != nil {
  1273  			return err
  1274  		}
  1275  
  1276  		// Avoid .foo, _foo, and testdata subdirectory trees.
  1277  		if !fi.IsDir() {
  1278  			return nil
  1279  		}
  1280  		base := filepath.Base(walkPath)
  1281  		if strings.HasPrefix(base, ".") || strings.HasPrefix(base, "_") || base == "testdata" || base == "internal" {
  1282  			return filepath.SkipDir
  1283  		}
  1284  
  1285  		p, err := build.Default.ImportDir(walkPath, 0)
  1286  		if err != nil {
  1287  			if nogoErr := (*build.NoGoError)(nil); errors.As(err, &nogoErr) {
  1288  				// No .go files found in directory. That's ok, we'll keep
  1289  				// searching.
  1290  				return nil
  1291  			}
  1292  			return err
  1293  		}
  1294  
  1295  		// Construct the import path.
  1296  		importPath := path.Join(modPath, filepath.ToSlash(trimFilePathPrefix(p.Dir, root)))
  1297  		importPaths = append(importPaths, importPath)
  1298  
  1299  		return nil
  1300  	})
  1301  	if err != nil {
  1302  		return nil, fmt.Errorf("listing packages in %s: %v", root, err)
  1303  	}
  1304  
  1305  	return importPaths, nil
  1306  }
  1307  
  1308  // loadPackages returns a list of all packages in the module modPath, sorted by
  1309  // package path. modRoot is the module root directory, but packages are loaded
  1310  // from loadDir, which must contain go.mod and go.sum containing goModData and
  1311  // goSumData.
  1312  //
  1313  // We load packages from a temporary external module so that replace and exclude
  1314  // directives are not applied. The loading process may also modify go.mod and
  1315  // go.sum, and we want to detect and report differences.
  1316  //
  1317  // Package loading errors will be returned in the Errors field of each package.
  1318  // Other diagnostics (such as the go.sum file being incomplete) will be
  1319  // returned through diagnostics.
  1320  // err will be non-nil in case of a fatal error that prevented packages
  1321  // from being loaded.
  1322  func loadPackages(ctx context.Context, modPath, modRoot, loadDir string, goModData, goSumData []byte, pkgPaths []string) (pkgs []*packages.Package, diagnostics []string, err error) {
  1323  	// Load packages.
  1324  	// TODO(jayconrod): if there are errors loading packages in the release
  1325  	// version, try loading in the release directory. Errors there would imply
  1326  	// that packages don't load without replace / exclude directives.
  1327  	cfg := &packages.Config{
  1328  		Mode:    packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
  1329  		Dir:     loadDir,
  1330  		Context: ctx,
  1331  	}
  1332  	cfg.Env = copyEnv(ctx, cfg.Env)
  1333  	if len(pkgPaths) > 0 {
  1334  		pkgs, err = packages.Load(cfg, pkgPaths...)
  1335  		if err != nil {
  1336  			return nil, nil, err
  1337  		}
  1338  	}
  1339  
  1340  	// Sort the returned packages by path.
  1341  	// packages.Load makes no guarantee about the order of returned packages.
  1342  	sort.Slice(pkgs, func(i, j int) bool {
  1343  		return pkgs[i].PkgPath < pkgs[j].PkgPath
  1344  	})
  1345  
  1346  	// Trim modRoot from file paths in errors.
  1347  	prefix := modRoot + string(os.PathSeparator)
  1348  	for _, pkg := range pkgs {
  1349  		for i := range pkg.Errors {
  1350  			pkg.Errors[i].Pos = strings.TrimPrefix(pkg.Errors[i].Pos, prefix)
  1351  		}
  1352  	}
  1353  
  1354  	return pkgs, diagnostics, nil
  1355  }
  1356  
  1357  type packagePair struct {
  1358  	base, release *packages.Package
  1359  }
  1360  
  1361  // zipPackages combines two lists of packages, sorted by package path,
  1362  // and returns a sorted list of pairs of packages with matching paths.
  1363  // If a package is in one list but not the other (because it was added or
  1364  // removed between releases), a pair will be returned with a nil
  1365  // base or release field.
  1366  func zipPackages(baseModPath string, basePkgs []*packages.Package, releaseModPath string, releasePkgs []*packages.Package) []packagePair {
  1367  	baseIndex, releaseIndex := 0, 0
  1368  	var pairs []packagePair
  1369  	for baseIndex < len(basePkgs) || releaseIndex < len(releasePkgs) {
  1370  		var basePkg, releasePkg *packages.Package
  1371  		var baseSuffix, releaseSuffix string
  1372  		if baseIndex < len(basePkgs) {
  1373  			basePkg = basePkgs[baseIndex]
  1374  			baseSuffix = trimPathPrefix(basePkg.PkgPath, baseModPath)
  1375  		}
  1376  		if releaseIndex < len(releasePkgs) {
  1377  			releasePkg = releasePkgs[releaseIndex]
  1378  			releaseSuffix = trimPathPrefix(releasePkg.PkgPath, releaseModPath)
  1379  		}
  1380  
  1381  		var pair packagePair
  1382  		if basePkg != nil && (releasePkg == nil || baseSuffix < releaseSuffix) {
  1383  			// Package removed
  1384  			pair = packagePair{basePkg, nil}
  1385  			baseIndex++
  1386  		} else if releasePkg != nil && (basePkg == nil || releaseSuffix < baseSuffix) {
  1387  			// Package added
  1388  			pair = packagePair{nil, releasePkg}
  1389  			releaseIndex++
  1390  		} else {
  1391  			// Matched packages.
  1392  			pair = packagePair{basePkg, releasePkg}
  1393  			baseIndex++
  1394  			releaseIndex++
  1395  		}
  1396  		pairs = append(pairs, pair)
  1397  	}
  1398  	return pairs
  1399  }
  1400  
  1401  // findSelectedVersion returns the highest version of the given modPath at
  1402  // modDir, if a module cycle exists. modDir should be a writable directory
  1403  // containing the go.mod for modPath.
  1404  //
  1405  // If no module cycle exists, it returns empty string.
  1406  func findSelectedVersion(ctx context.Context, modDir, modPath string) (latestVersion string, err error) {
  1407  	defer func() {
  1408  		if err != nil {
  1409  			err = fmt.Errorf("could not find selected version for %s: %v", modPath, err)
  1410  		}
  1411  	}()
  1412  
  1413  	cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", modPath)
  1414  	cmd.Env = copyEnv(ctx, cmd.Env)
  1415  	cmd.Dir = modDir
  1416  	out, err := cmd.Output()
  1417  	if err != nil {
  1418  		return "", cleanCmdError(err)
  1419  	}
  1420  	return strings.TrimSpace(string(out)), nil
  1421  }
  1422  
  1423  func copyEnv(ctx context.Context, current []string) []string {
  1424  	env, ok := ctx.Value("env").([]string)
  1425  	if !ok {
  1426  		return current
  1427  	}
  1428  	clone := make([]string, len(env))
  1429  	copy(clone, env)
  1430  	return clone
  1431  }
  1432  
  1433  // loadRetractions lists all retracted deps found at the modRoot.
  1434  func loadRetractions(ctx context.Context, modRoot string) ([]string, error) {
  1435  	cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-u", "all")
  1436  	if env, ok := ctx.Value("env").([]string); ok {
  1437  		cmd.Env = env
  1438  	}
  1439  	cmd.Dir = modRoot
  1440  	out, err := cmd.Output()
  1441  	if err != nil {
  1442  		return nil, cleanCmdError(err)
  1443  	}
  1444  
  1445  	var retracted []string
  1446  	type message struct {
  1447  		Path      string
  1448  		Version   string
  1449  		Retracted []string
  1450  	}
  1451  
  1452  	dec := json.NewDecoder(bytes.NewBuffer(out))
  1453  	for {
  1454  		var m message
  1455  		if err := dec.Decode(&m); err == io.EOF {
  1456  			break
  1457  		} else if err != nil {
  1458  			return nil, err
  1459  		}
  1460  		if len(m.Retracted) == 0 {
  1461  			continue
  1462  		}
  1463  		rationale, ok := shortRetractionRationale(m.Retracted)
  1464  		if ok {
  1465  			retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author: %s", m.Path, m.Version, rationale))
  1466  		} else {
  1467  			retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author", m.Path, m.Version))
  1468  		}
  1469  	}
  1470  
  1471  	return retracted, nil
  1472  }
  1473  
  1474  // shortRetractionRationale returns a retraction rationale string that is safe
  1475  // to print in a terminal. It returns hard-coded strings if the rationale
  1476  // is empty, too long, or contains non-printable characters.
  1477  //
  1478  // It returns true if the rationale was printable, and false if it was not (too
  1479  // long, contains graphics, etc).
  1480  func shortRetractionRationale(rationales []string) (string, bool) {
  1481  	if len(rationales) == 0 {
  1482  		return "", false
  1483  	}
  1484  	rationale := rationales[0]
  1485  
  1486  	const maxRationaleBytes = 500
  1487  	if i := strings.Index(rationale, "\n"); i >= 0 {
  1488  		rationale = rationale[:i]
  1489  	}
  1490  	rationale = strings.TrimSpace(rationale)
  1491  	if rationale == "" || rationale == "retracted by module author" {
  1492  		return "", false
  1493  	}
  1494  	if len(rationale) > maxRationaleBytes {
  1495  		return "", false
  1496  	}
  1497  	for _, r := range rationale {
  1498  		if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
  1499  			return "", false
  1500  		}
  1501  	}
  1502  	// NOTE: the go.mod parser rejects invalid UTF-8, so we don't check that here.
  1503  	return rationale, true
  1504  }
  1505  
  1506  // hasGitUncommittedChanges checks if the given directory has uncommitteed git
  1507  // changes.
  1508  func hasGitUncommittedChanges(dir string) (bool, error) {
  1509  	stdout := &bytes.Buffer{}
  1510  	cmd := exec.Command("git", "status", "--porcelain")
  1511  	cmd.Dir = dir
  1512  	cmd.Stdout = stdout
  1513  	if err := cmd.Run(); err != nil {
  1514  		return false, cleanCmdError(err)
  1515  	}
  1516  	return stdout.Len() != 0, nil
  1517  }