cmd/go/internal/modload: drop requirements on excluded versions

Previously, when we encountered an excluded version in any module's
requirements, we would resolve it to the next higher version.
Unfortunately, the meaning of “the next higher version” can change
over time.

Moreover, users who use 'exclude' directives normally either already
require some higher version (using the 'exclude' directive to prune
out invalid requirements from some intermediate version), or already
require some lower version (using the 'exclude' directive to prevent
'go get -u' from upgrading to a known-bad version). In both of these
cases, resolving an upgrade for the excluded version is needless work
even in the best case: it adds work for the 'go' command when there is
already a perfectly usable selected version of the module in the
requirement graph.

Instead, we now interpret the 'exclude' directive as dropping all
references to the excluded version.

This implements the approach described in
https://golang.org/issue/36465#issuecomment-572694990.

Fixes #36465
Updates #36460

Change-Id: Ibf0187daced417b4cc23b97125826778658e4b0f
Reviewed-on: https://go-review.googlesource.com/c/go/+/244773
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/src/cmd/go/internal/modload/init.go b/src/cmd/go/internal/modload/init.go
index 71c7b15..7f49310 100644
--- a/src/cmd/go/internal/modload/init.go
+++ b/src/cmd/go/internal/modload/init.go
@@ -383,8 +383,8 @@
 		legacyModInit()
 	}
 
-	modFileToBuildList()
 	setDefaultBuildMod()
+	modFileToBuildList()
 	if cfg.BuildMod == "vendor" {
 		readVendorList()
 		checkVendorConsistency()
@@ -459,7 +459,15 @@
 
 	list := []module.Version{Target}
 	for _, r := range modFile.Require {
-		list = append(list, r.Mod)
+		if index != nil && index.exclude[r.Mod] {
+			if cfg.BuildMod == "mod" {
+				fmt.Fprintf(os.Stderr, "go: dropping requirement on excluded version %s %s\n", r.Mod.Path, r.Mod.Version)
+			} else {
+				fmt.Fprintf(os.Stderr, "go: ignoring requirement on excluded version %s %s\n", r.Mod.Path, r.Mod.Version)
+			}
+		} else {
+			list = append(list, r.Mod)
+		}
 	}
 	buildList = list
 }
diff --git a/src/cmd/go/internal/modload/mvs.go b/src/cmd/go/internal/modload/mvs.go
index 67eb2c2..39d0d69 100644
--- a/src/cmd/go/internal/modload/mvs.go
+++ b/src/cmd/go/internal/modload/mvs.go
@@ -54,20 +54,15 @@
 		if err != nil {
 			return cached{nil, err}
 		}
-		for i, mv := range list {
-			if index != nil {
-				for index.exclude[mv] {
-					mv1, err := r.next(mv)
-					if err != nil {
-						return cached{nil, err}
-					}
-					if mv1.Version == "none" {
-						return cached{nil, fmt.Errorf("%s(%s) depends on excluded %s(%s) with no newer version available", mod.Path, mod.Version, mv.Path, mv.Version)}
-					}
-					mv = mv1
+		if index != nil && len(index.exclude) > 0 {
+			// Drop requirements on excluded versions.
+			nonExcluded := list[:0]
+			for _, r := range list {
+				if !index.exclude[r] {
+					nonExcluded = append(nonExcluded, r)
 				}
 			}
-			list[i] = mv
+			list = nonExcluded
 		}
 
 		return cached{list, nil}
diff --git a/src/cmd/go/testdata/script/mod_require_exclude.txt b/src/cmd/go/testdata/script/mod_require_exclude.txt
index 60f7e3f..1a0fc30 100644
--- a/src/cmd/go/testdata/script/mod_require_exclude.txt
+++ b/src/cmd/go/testdata/script/mod_require_exclude.txt
@@ -1,16 +1,51 @@
 # build with no newer version to satisfy exclude
 env GO111MODULE=on
-! go list -m all
-stderr 'no newer version available'
+cp go.mod go.mod.orig
+
+# With the selected version excluded, commands that query that version without
+# updating go.mod should fail.
+
+! go list -mod=readonly -m all
+stderr '^go: ignoring requirement on excluded version rsc.io/sampler v1\.99\.99$'
+stderr '^go: updates to go.mod needed, disabled by -mod=readonly$'
+! stdout '^rsc.io/sampler v1.99.99'
+cmp go.mod go.mod.orig
+
+! go list -mod=vendor -m rsc.io/sampler
+stderr '^go: ignoring requirement on excluded version rsc.io/sampler v1\.99\.99$'
+stderr '^go list -m: module rsc.io/sampler: can''t resolve module using the vendor directory\n\t\(Use -mod=mod or -mod=readonly to bypass\.\)$'
+! stdout '^rsc.io/sampler v1.99.99'
+cmp go.mod go.mod.orig
+
+# With the selected version excluded, commands that load only modules should
+# drop the excluded module.
+
+go list -m all
+stderr '^go: dropping requirement on excluded version rsc.io/sampler v1\.99\.99$'
+stdout '^x$'
+! stdout '^rsc.io/sampler'
+cmp go.mod go.moddrop
+
+# With the latest version excluded, 'go list' should resolve needed packages
+# from the next-highest version.
+
+cp go.mod.orig go.mod
+go list -f '{{with .Module}}{{.Path}} {{.Version}}{{end}}' all
+stderr '^go: dropping requirement on excluded version rsc.io/sampler v1\.99\.99$'
+stdout '^x $'
+! stdout '^rsc.io/sampler v1.99.99'
+stdout '^rsc.io/sampler v1.3.0'
 
 # build with newer version available
 cp go.mod2 go.mod
-go list -m all
+go list -f '{{with .Module}}{{.Path}} {{.Version}}{{end}}' all
+stderr '^go: dropping requirement on excluded version rsc.io/quote v1\.5\.1$'
 stdout 'rsc.io/quote v1.5.2'
 
 # build with excluded newer version
 cp go.mod3 go.mod
-go list -m all
+go list -f '{{with .Module}}{{.Path}} {{.Version}}{{end}}' all
+! stderr '^go: dropping requirement'
 stdout 'rsc.io/quote v1.5.1'
 
 -- x.go --
@@ -19,15 +54,28 @@
 
 -- go.mod --
 module x
-exclude rsc.io/sampler latest
-require rsc.io/sampler latest
 
+go 1.13
+
+exclude rsc.io/sampler v1.99.99
+require rsc.io/sampler v1.99.99
+-- go.moddrop --
+module x
+
+go 1.13
+
+exclude rsc.io/sampler v1.99.99
 -- go.mod2 --
 module x
+
+go 1.13
+
 exclude rsc.io/quote v1.5.1
 require rsc.io/quote v1.5.1
-
 -- go.mod3 --
 module x
+
+go 1.13
+
 exclude rsc.io/quote v1.5.2
 require rsc.io/quote v1.5.1