]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go/internal/modload: support go.mod retract directive
authorJay Conrod <jayconrod@google.com>
Wed, 15 Apr 2020 17:56:09 +0000 (13:56 -0400)
committerJay Conrod <jayconrod@google.com>
Wed, 26 Aug 2020 21:12:37 +0000 (21:12 +0000)
The go command now recognizes 'retract' directives in go.mod. A
retract directive may be used by a module author to indicate a
version should not be used. The go command will not automatically
upgrade to a retracted version. Retracted versions will not be
considered when resolving version queries like "latest" that don't
refer to a specific version.

Internally, when the go command resolves a version query, it will find
the highest release version (or pre-release if no release is
available), then it will load retractions from the go.mod file for
that version. Comments on retractions are treated as a rationale and
may appear in error messages. Retractions are only loaded when a query
is resolved, so this should have no impact on performance for most
builds, except when go.mod is incomplete.

For #24031

Change-Id: I17d643b9e03a3445676dbf1a5a351090c6ff6914
Reviewed-on: https://go-review.googlesource.com/c/go/+/228380
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
15 files changed:
src/cmd/go/alldocs.go
src/cmd/go/internal/modinfo/info.go
src/cmd/go/internal/modload/help.go
src/cmd/go/internal/modload/list.go
src/cmd/go/internal/modload/modfile.go
src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.0.0-bad.txt [new file with mode: 0644]
src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.1.0.txt [new file with mode: 0644]
src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.9.0.txt [new file with mode: 0644]
src/cmd/go/testdata/mod/example.com_retract_v1.0.0-bad.txt [new file with mode: 0644]
src/cmd/go/testdata/mod/example.com_retract_v1.0.0-good.txt [new file with mode: 0644]
src/cmd/go/testdata/mod/example.com_retract_v1.0.0-unused.txt [new file with mode: 0644]
src/cmd/go/testdata/mod/example.com_retract_v1.1.0.txt [new file with mode: 0644]
src/cmd/go/testdata/script/mod_download.txt
src/cmd/go/testdata/script/mod_retract.txt [new file with mode: 0644]
src/cmd/go/testdata/script/mod_sumdb_golang.txt

index 68bad3cff1b812d355b3647069d659597e12304d..f50529c4f2abeb76a0de5b4774620d594014d837 100644 (file)
 //     require new/thing/v2 v2.3.4
 //     exclude old/thing v1.2.3
 //     replace bad/thing v1.4.5 => good/thing v1.4.5
+//     retract v1.5.6
 //
 // The verbs are
 //     module, to define the module path;
 //     go, to set the expected language version;
 //     require, to require a particular module at a given version or later;
-//     exclude, to exclude a particular module version from use; and
-//     replace, to replace a module version with a different module version.
+//     exclude, to exclude a particular module version from use;
+//     replace, to replace a module version with a different module version; and
+//     retract, to indicate a previously released version should not be used.
 // Exclude and replace apply only in the main module's go.mod and are ignored
-// in dependencies.  See https://research.swtch.com/vgo-mvs for details.
+// in dependencies.  See https://golang.org/ref/mod for details.
 //
 // The leading verb can be factored out of adjacent lines to create a block,
 // like in Go imports:
index 07248d1a61cb837be4d1cc7e6574407f6eb651a1..897be56397d144f4da6ab296dc636e19c8a82f07 100644 (file)
@@ -21,6 +21,7 @@ type ModulePublic struct {
        Dir       string        `json:",omitempty"` // directory holding local copy of files, if any
        GoMod     string        `json:",omitempty"` // path to go.mod file describing module, if any
        GoVersion string        `json:",omitempty"` // go version used in module
+       Retracted []string      `json:",omitempty"` // retraction information, if any (with -retracted or -u)
        Error     *ModuleError  `json:",omitempty"` // error loading module
 }
 
@@ -30,18 +31,26 @@ type ModuleError struct {
 
 func (m *ModulePublic) String() string {
        s := m.Path
+       versionString := func(mm *ModulePublic) string {
+               v := mm.Version
+               if len(mm.Retracted) == 0 {
+                       return v
+               }
+               return v + " (retracted)"
+       }
+
        if m.Version != "" {
-               s += " " + m.Version
+               s += " " + versionString(m)
                if m.Update != nil {
-                       s += " [" + m.Update.Version + "]"
+                       s += " [" + versionString(m.Update) + "]"
                }
        }
        if m.Replace != nil {
                s += " => " + m.Replace.Path
                if m.Replace.Version != "" {
-                       s += " " + m.Replace.Version
+                       s += " " + versionString(m.Replace)
                        if m.Replace.Update != nil {
-                               s += " [" + m.Replace.Update.Version + "]"
+                               s += " [" + versionString(m.Replace.Update) + "]"
                        }
                }
        }
index d80206b19482b0a9ecfe72302ee23e63fdf99da4..37f23d967ff89264003c288fcfaae1e5e204c136 100644 (file)
@@ -432,15 +432,17 @@ verb followed by arguments. For example:
        require new/thing/v2 v2.3.4
        exclude old/thing v1.2.3
        replace bad/thing v1.4.5 => good/thing v1.4.5
+       retract v1.5.6
 
 The verbs are
        module, to define the module path;
        go, to set the expected language version;
        require, to require a particular module at a given version or later;
-       exclude, to exclude a particular module version from use; and
-       replace, to replace a module version with a different module version.
+       exclude, to exclude a particular module version from use;
+       replace, to replace a module version with a different module version; and
+       retract, to indicate a previously released version should not be used.
 Exclude and replace apply only in the main module's go.mod and are ignored
-in dependencies.  See https://research.swtch.com/vgo-mvs for details.
+in dependencies.  See https://golang.org/ref/mod for details.
 
 The leading verb can be factored out of adjacent lines to create a block,
 like in Go imports:
index 2f549540a687088a6518c49ec8b7ee9c58c47639..a3461eea26f3f89868f7ea532a138805719afd82 100644 (file)
@@ -85,7 +85,8 @@ func listModules(ctx context.Context, args []string, listVersions bool) []*modin
 
                        allowed := CheckAllowed
                        if IsRevisionQuery(vers) {
-                               // Allow excluded versions if the user asked for a specific revision.
+                               // Allow excluded and retracted versions if the user asked for a
+                               // specific revision.
                                allowed = nil
                        }
                        info, err := Query(ctx, path, vers, current, allowed)
index aed1f0a36bebea19c4c93659e4610b114051ece5..0b135c5fb515a5fe90b8611ef4b8e0252a88b2d1 100644 (file)
@@ -9,13 +9,16 @@ import (
        "errors"
        "fmt"
        "path/filepath"
+       "strings"
        "sync"
+       "unicode"
 
        "cmd/go/internal/base"
        "cmd/go/internal/cfg"
        "cmd/go/internal/lockedfile"
        "cmd/go/internal/modfetch"
        "cmd/go/internal/par"
+       "cmd/go/internal/trace"
 
        "golang.org/x/mod/modfile"
        "golang.org/x/mod/module"
@@ -44,10 +47,16 @@ type requireMeta struct {
 }
 
 // CheckAllowed returns an error equivalent to ErrDisallowed if m is excluded by
-// the main module's go.mod. Most version queries use this to filter out
-// versions that should not be used.
+// the main module's go.mod or retracted by its author. Most version queries use
+// this to filter out versions that should not be used.
 func CheckAllowed(ctx context.Context, m module.Version) error {
-       return CheckExclusions(ctx, m)
+       if err := CheckExclusions(ctx, m); err != nil {
+               return err
+       }
+       if err := checkRetractions(ctx, m); err != nil {
+               return err
+       }
+       return nil
 }
 
 // ErrDisallowed is returned by version predicates passed to Query and similar
@@ -70,6 +79,120 @@ type excludedError struct{}
 func (e *excludedError) Error() string     { return "excluded by go.mod" }
 func (e *excludedError) Is(err error) bool { return err == ErrDisallowed }
 
+// checkRetractions returns an error if module m has been retracted by
+// its author.
+func checkRetractions(ctx context.Context, m module.Version) error {
+       if m.Version == "" {
+               // Main module, standard library, or file replacement module.
+               // Cannot be retracted.
+               return nil
+       }
+
+       // Look up retraction information from the latest available version of
+       // the module. Cache retraction information so we don't parse the go.mod
+       // file repeatedly.
+       type entry struct {
+               retract []retraction
+               err     error
+       }
+       path := m.Path
+       e := retractCache.Do(path, func() (v interface{}) {
+               ctx, span := trace.StartSpan(ctx, "checkRetractions "+path)
+               defer span.Done()
+
+               if repl := Replacement(module.Version{Path: m.Path}); repl.Path != "" {
+                       // All versions of the module were replaced with a local directory.
+                       // Don't load retractions.
+                       return &entry{nil, nil}
+               }
+
+               // Find the latest version of the module.
+               // Ignore exclusions from the main module's go.mod.
+               // We may need to account for the current version: for example,
+               // v2.0.0+incompatible is not "latest" if v1.0.0 is current.
+               rev, err := Query(ctx, path, "latest", findCurrentVersion(path), nil)
+               if err != nil {
+                       return &entry{err: err}
+               }
+
+               // Load go.mod for that version.
+               // If the version is replaced, we'll load retractions from the replacement.
+               // If there's an error loading the go.mod, we'll return it here.
+               // These errors should generally be ignored by callers of checkRetractions,
+               // since they happen frequently when we're offline. These errors are not
+               // equivalent to ErrDisallowed, so they may be distinguished from
+               // retraction errors.
+               summary, err := goModSummary(module.Version{Path: path, Version: rev.Version})
+               if err != nil {
+                       return &entry{err: err}
+               }
+               return &entry{retract: summary.retract}
+       }).(*entry)
+
+       if e.err != nil {
+               return fmt.Errorf("loading module retractions: %v", e.err)
+       }
+
+       var rationale []string
+       isRetracted := false
+       for _, r := range e.retract {
+               if semver.Compare(r.Low, m.Version) <= 0 && semver.Compare(m.Version, r.High) <= 0 {
+                       isRetracted = true
+                       if r.Rationale != "" {
+                               rationale = append(rationale, r.Rationale)
+                       }
+               }
+       }
+       if isRetracted {
+               return &retractedError{rationale: rationale}
+       }
+       return nil
+}
+
+var retractCache par.Cache
+
+type retractedError struct {
+       rationale []string
+}
+
+func (e *retractedError) Error() string {
+       msg := "retracted by module author"
+       if len(e.rationale) > 0 {
+               // This is meant to be a short error printed on a terminal, so just
+               // print the first rationale.
+               msg += ": " + ShortRetractionRationale(e.rationale[0])
+       }
+       return msg
+}
+
+func (e *retractedError) Is(err error) bool {
+       return err == ErrDisallowed
+}
+
+// ShortRetractionRationale returns a retraction rationale string that is safe
+// to print in a terminal. It returns hard-coded strings if the rationale
+// is empty, too long, or contains non-printable characters.
+func ShortRetractionRationale(rationale string) string {
+       const maxRationaleBytes = 500
+       if i := strings.Index(rationale, "\n"); i >= 0 {
+               rationale = rationale[:i]
+       }
+       rationale = strings.TrimSpace(rationale)
+       if rationale == "" {
+               return "retracted by module author"
+       }
+       if len(rationale) > maxRationaleBytes {
+               return "(rationale omitted: too long)"
+       }
+       for _, r := range rationale {
+               if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
+                       return "(rationale omitted: contains non-printable characters)"
+               }
+       }
+       // NOTE: the go.mod parser rejects invalid UTF-8, so we don't check that here.
+       return rationale
+}
+
 // Replacement returns the replacement for mod, if any, from go.mod.
 // If there is no replacement for mod, Replacement returns
 // a module.Version with Path == "".
@@ -210,6 +333,14 @@ type modFileSummary struct {
        module     module.Version
        goVersionV string // GoVersion with "v" prefix
        require    []module.Version
+       retract    []retraction
+}
+
+// A retraction consists of a retracted version interval and rationale.
+// retraction is like modfile.Retract, but it doesn't point to the syntax tree.
+type retraction struct {
+       modfile.VersionInterval
+       Rationale string
 }
 
 // goModSummary returns a summary of the go.mod file for module m,
@@ -363,6 +494,15 @@ func rawGoModSummary(m module.Version) (*modFileSummary, error) {
                        summary.require = append(summary.require, req.Mod)
                }
        }
+       if len(f.Retract) > 0 {
+               summary.retract = make([]retraction, 0, len(f.Retract))
+               for _, ret := range f.Retract {
+                       summary.retract = append(summary.retract, retraction{
+                               VersionInterval: ret.VersionInterval,
+                               Rationale:       ret.Rationale,
+                       })
+               }
+       }
 
        return summary, nil
 }
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.0.0-bad.txt b/src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.0.0-bad.txt
new file mode 100644 (file)
index 0000000..095063d
--- /dev/null
@@ -0,0 +1,14 @@
+See example.com_retract_self_prev_v1.9.0.txt.
+
+This version is retracted.
+
+-- .mod --
+module example.com/retract/self/prev
+
+go 1.15
+
+-- .info --
+{"Version":"v1.0.0-bad"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.1.0.txt b/src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.1.0.txt
new file mode 100644 (file)
index 0000000..27c3a39
--- /dev/null
@@ -0,0 +1,14 @@
+See example.com_retract_self_pref_v1.9.0.txt.
+
+This version is the latest (only) non-retracted version.
+
+-- .mod --
+module example.com/retract/self/prev
+
+go 1.15
+
+-- .info --
+{"Version":"v1.1.0"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.9.0.txt b/src/cmd/go/testdata/mod/example.com_retract_self_prev_v1.9.0.txt
new file mode 100644 (file)
index 0000000..03d6168
--- /dev/null
@@ -0,0 +1,18 @@
+Module example.com/retract/self/prev is a module that retracts its own
+latest version, as well as an earlier version.
+
+A previous unretracted release version, v1.1.0, is still available.
+
+-- .mod --
+module example.com/retract/self/prev
+
+go 1.15
+
+retract v1.0.0-bad // bad
+retract v1.9.0 // self
+
+-- .info --
+{"Version":"v1.9.0"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_v1.0.0-bad.txt b/src/cmd/go/testdata/mod/example.com_retract_v1.0.0-bad.txt
new file mode 100644 (file)
index 0000000..2f996cf
--- /dev/null
@@ -0,0 +1,10 @@
+-- .mod --
+module example.com/retract
+
+go 1.15
+
+-- .info --
+{"Version":"v1.0.0-bad"}
+
+-- retract.go --
+package retract
diff --git a/src/cmd/go/testdata/mod/example.com_retract_v1.0.0-good.txt b/src/cmd/go/testdata/mod/example.com_retract_v1.0.0-good.txt
new file mode 100644 (file)
index 0000000..78152bb
--- /dev/null
@@ -0,0 +1,10 @@
+-- .mod --
+module example.com/retract
+
+go 1.15
+
+-- .info --
+{"Version":"v1.0.0-good"}
+
+-- retract.go --
+package retract
diff --git a/src/cmd/go/testdata/mod/example.com_retract_v1.0.0-unused.txt b/src/cmd/go/testdata/mod/example.com_retract_v1.0.0-unused.txt
new file mode 100644 (file)
index 0000000..3bc9e35
--- /dev/null
@@ -0,0 +1,10 @@
+-- .mod --
+module example.com/retract
+
+go 1.15
+
+-- .info --
+{"Version":"v1.0.0-unused"}
+
+-- retract.go --
+package retract
diff --git a/src/cmd/go/testdata/mod/example.com_retract_v1.1.0.txt b/src/cmd/go/testdata/mod/example.com_retract_v1.1.0.txt
new file mode 100644 (file)
index 0000000..18d6d83
--- /dev/null
@@ -0,0 +1,13 @@
+-- .mod --
+module example.com/retract
+
+go 1.15
+
+retract v1.0.0-bad // bad
+retract v1.0.0-unused // bad
+
+-- .info --
+{"Version":"v1.1.0"}
+
+-- retract.go --
+package retract
index bb5c4627db697668697587344fecf743c5b74467..5acb83266bb4f448406036e5b31cca587d4f1e6a 100644 (file)
@@ -1,13 +1,15 @@
 env GO111MODULE=on
 
-# download with version should print nothing
+# download with version should print nothing.
+# It should not load retractions from the .mod file from the latest version.
 go mod download rsc.io/quote@v1.5.0
 ! stdout .
 ! stderr .
-
 exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.0.info
 exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.0.mod
 exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.0.zip
+! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
+! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
 
 # download of an invalid path should report the error
 [short] skip
@@ -31,53 +33,59 @@ stdout '^\t"GoModSum": "h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe\+TKr0="'
 go list -m all
 ! stdout rsc.io
 
-# add to go.mod so we can test non-query downloads
-go mod edit -require rsc.io/quote@v1.5.2
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
+# download query should have downloaded go.mod for the highest release version
+# in order to find retractions when resolving the query '@<=v1.5.0'.
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
 ! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
 
+# add to go.mod so we can test non-query downloads
+go mod edit -require rsc.io/quote@v1.5.3-pre1
+! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.info
+! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.mod
+! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.zip
+
 # module loading will page in the info and mod files
 go list -m all
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
-! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.info
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.mod
+! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.zip
 
 # download will fetch and unpack the zip file
 go mod download
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
-exists $GOPATH/pkg/mod/rsc.io/quote@v1.5.2
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.info
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.mod
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.zip
+exists $GOPATH/pkg/mod/rsc.io/quote@v1.5.3-pre1
 
 # download repopulates deleted files and directories independently.
-rm $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
+rm $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.info
 go mod download
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.info
-rm $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.info
+rm $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.mod
 go mod download
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
-rm $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.mod
+rm $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.zip
 go mod download
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
-rm -r $GOPATH/pkg/mod/rsc.io/quote@v1.5.2
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.zip
+rm -r $GOPATH/pkg/mod/rsc.io/quote@v1.5.3-pre1
 go mod download
-exists $GOPATH/pkg/mod/rsc.io/quote@v1.5.2
+exists $GOPATH/pkg/mod/rsc.io/quote@v1.5.3-pre1
 
 # download reports the locations of downloaded files
 go mod download -json
 stdout '^\t"Path": "rsc.io/quote"'
-stdout '^\t"Version": "v1.5.2"'
-stdout '^\t"Info": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)cache(\\\\|/)download(\\\\|/)rsc.io(\\\\|/)quote(\\\\|/)@v(\\\\|/)v1.5.2.info"'
-stdout '^\t"GoMod": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)cache(\\\\|/)download(\\\\|/)rsc.io(\\\\|/)quote(\\\\|/)@v(\\\\|/)v1.5.2.mod"'
-stdout '^\t"Zip": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)cache(\\\\|/)download(\\\\|/)rsc.io(\\\\|/)quote(\\\\|/)@v(\\\\|/)v1.5.2.zip"'
-stdout '^\t"Dir": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)rsc.io(\\\\|/)quote@v1.5.2"'
+stdout '^\t"Version": "v1.5.3-pre1"'
+stdout '^\t"Info": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)cache(\\\\|/)download(\\\\|/)rsc.io(\\\\|/)quote(\\\\|/)@v(\\\\|/)v1.5.3-pre1.info"'
+stdout '^\t"GoMod": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)cache(\\\\|/)download(\\\\|/)rsc.io(\\\\|/)quote(\\\\|/)@v(\\\\|/)v1.5.3-pre1.mod"'
+stdout '^\t"Zip": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)cache(\\\\|/)download(\\\\|/)rsc.io(\\\\|/)quote(\\\\|/)@v(\\\\|/)v1.5.3-pre1.zip"'
+stdout '^\t"Dir": ".*(\\\\|/)pkg(\\\\|/)mod(\\\\|/)rsc.io(\\\\|/)quote@v1.5.3-pre1"'
 
 # download will follow replacements
-go mod edit -require rsc.io/quote@v1.5.1 -replace rsc.io/quote@v1.5.1=rsc.io/quote@v1.5.3-pre1
+go mod edit -require rsc.io/quote@v1.5.1 -replace rsc.io/quote@v1.5.1=rsc.io/quote@v1.5.2
 go mod download
 ! exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.1.zip
-exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-pre1.zip
+exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
 
 # download will not follow replacements for explicit module queries
 go mod download -json rsc.io/quote@v1.5.1
diff --git a/src/cmd/go/testdata/script/mod_retract.txt b/src/cmd/go/testdata/script/mod_retract.txt
new file mode 100644 (file)
index 0000000..5d21902
--- /dev/null
@@ -0,0 +1,40 @@
+cp go.mod go.mod.orig
+
+# 'go list pkg' does not report an error when a retracted version is used.
+go list -e -f '{{if .Error}}{{.Error}}{{end}}' ./use
+! stdout .
+cmp go.mod go.mod.orig
+
+# Nor does 'go build'.
+[!short] go build ./use
+[!short] ! stderr .
+[!short] cmp go.mod go.mod.orig
+
+# Neither 'go list' nor 'go build' should download go.mod from the version
+# that would list retractions.
+exists $GOPATH/pkg/mod/cache/download/example.com/retract/@v/v1.0.0-bad.mod
+! exists $GOPATH/pkg/mod/cache/download/example.com/retract/@v/v1.1.0.mod
+
+# Importing a package from a module with a retracted latest version will
+# select the latest non-retracted version.
+go list ./use_self_prev
+go list -m example.com/retract/self/prev
+stdout '^example.com/retract/self/prev v1.1.0$'
+exists $GOPATH/pkg/mod/cache/download/example.com/retract/self/prev/@v/v1.9.0.mod
+
+-- go.mod --
+module example.com/use
+
+go 1.15
+
+require example.com/retract v1.0.0-bad
+
+-- use/use.go --
+package use
+
+import _ "example.com/retract"
+
+-- use_self_prev/use.go --
+package use_self_prev
+
+import _ "example.com/retract/self/prev"
index 40a07fc7e9fe702094f0d3a46cb2e7595b22d6d3..d9fb63acb03cce494562f0eae7e4ed27b1d3c432 100644 (file)
@@ -9,7 +9,7 @@ env GOPROXY=https://proxy.golang.org
 go env GOSUMDB
 stdout '^sum.golang.org$'
 
-# download direct from github
+# Download direct from github.
 [!net] skip
 [!exec:git] skip
 env GOSUMDB=sum.golang.org
@@ -17,11 +17,13 @@ env GOPROXY=direct
 go get -d rsc.io/quote@v1.5.2
 cp go.sum saved.sum
 
-# download from proxy.golang.org with go.sum entry already
+# Download from proxy.golang.org with go.sum entry already.
+# Use 'go list' instead of 'go get' since the latter may download extra go.mod
+# files not listed in go.sum.
 go clean -modcache
 env GOSUMDB=
 env GOPROXY=
-go get -x -d rsc.io/quote@v1.5.2
+go list -x -deps rsc.io/quote
 ! stderr github
 stderr proxy.golang.org/rsc.io/quote
 ! stderr sum.golang.org/tile
@@ -32,7 +34,7 @@ cmp go.sum saved.sum
 # Should use the checksum database to validate new go.sum lines,
 # but not need to fetch any new data from the proxy.
 rm go.sum
-go get -x -d rsc.io/quote@v1.5.2
+go list -x rsc.io/quote
 ! stderr github
 ! stderr proxy.golang.org/rsc.io/quote
 stderr sum.golang.org/tile
@@ -43,7 +45,7 @@ cmp go.sum saved.sum
 env TESTGOPROXY404=1
 go clean -modcache
 rm go.sum
-go get -x -d rsc.io/quote@v1.5.2
+go list -x rsc.io/quote
 stderr 'proxy.golang.org.*404 testing'
 stderr github.com/rsc
 cmp go.sum saved.sum