cmd/go/internal/modfetch: maintain @v/list files

After this change, the $GOPATH/src/mod/cache/download file tree
is exactly the format that needs to be served from a Go package proxy.
The tree can be copied to static hosting or even accessed by
setting GOPROXY to a file:/// URL. Test that.

Fixes golang/go#26185.

Change-Id: I81f10fa42835a5d1909ab348fd1dfb7449089f5e
Reviewed-on: https://go-review.googlesource.com/122406
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/internal/modfetch/cache.go b/vendor/cmd/go/internal/modfetch/cache.go
index be0adc0..5503df1 100644
--- a/vendor/cmd/go/internal/modfetch/cache.go
+++ b/vendor/cmd/go/internal/modfetch/cache.go
@@ -13,6 +13,7 @@
 	"path/filepath"
 	"strings"
 
+	"cmd/go/internal/base"
 	"cmd/go/internal/modfetch/codehost"
 	"cmd/go/internal/par"
 	"cmd/go/internal/semver"
@@ -345,5 +346,59 @@
 	}
 	// Rename temp file onto cache file,
 	// so that the cache file is always a complete file.
-	return os.Rename(f.Name(), file)
+	if err := os.Rename(f.Name(), file); err != nil {
+		return err
+	}
+
+	if strings.HasSuffix(file, ".mod") {
+		rewriteVersionList(filepath.Dir(file))
+	}
+	return nil
+}
+
+// rewriteVersionList rewrites the version list in dir
+// after a new *.mod file has been written.
+func rewriteVersionList(dir string) {
+	if filepath.Base(dir) != "@v" {
+		base.Fatalf("go: internal error: misuse of rewriteVersionList")
+	}
+
+	// TODO(rsc): We should do some kind of directory locking here,
+	// to avoid lost updates.
+
+	infos, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return
+	}
+	var list []string
+	for _, info := range infos {
+		// We look for *.mod files on the theory that if we can't supply
+		// the .mod file then there's no point in listing that version,
+		// since it's unusable. (We can have *.info without *.mod.)
+		// We don't require *.zip files on the theory that for code only
+		// involved in module graph construction, many *.zip files
+		// will never be requested.
+		name := info.Name()
+		if strings.HasSuffix(name, ".mod") {
+			v := strings.TrimSuffix(name, ".mod")
+			if semver.IsValid(v) && semver.Canonical(v) == v {
+				list = append(list, v)
+			}
+		}
+	}
+	SortVersions(list)
+
+	var buf bytes.Buffer
+	for _, v := range list {
+		buf.WriteString(v)
+		buf.WriteString("\n")
+	}
+	listFile := filepath.Join(dir, "list")
+	old, _ := ioutil.ReadFile(listFile)
+	if bytes.Equal(buf.Bytes(), old) {
+		return
+	}
+	// TODO: Use rename to install file,
+	// so that readers never see an incomplete file.
+	ioutil.WriteFile(listFile, buf.Bytes(), 0666)
 }
diff --git a/vendor/cmd/go/mod_test.go b/vendor/cmd/go/mod_test.go
index 258066f..01e07d0 100644
--- a/vendor/cmd/go/mod_test.go
+++ b/vendor/cmd/go/mod_test.go
@@ -1155,6 +1155,45 @@
 	}
 }
 
+func TestModProxy(t *testing.T) {
+	tg := testgo(t)
+	tg.setenv("GO111MODULE", "on")
+	defer tg.cleanup()
+	tg.makeTempdir()
+
+	tg.setenv("GOPATH", tg.path("gp1"))
+
+	tg.must(os.MkdirAll(tg.path("x"), 0777))
+	tg.must(ioutil.WriteFile(tg.path("x/main.go"), []byte(`package x; import _ "rsc.io/quote"`), 0666))
+	tg.must(ioutil.WriteFile(tg.path("x/go.mod"), []byte(`module x
+		require rsc.io/quote v1.5.1`), 0666))
+	tg.cd(tg.path("x"))
+	tg.run("list", "all")
+	tg.run("list", "-getmode=local", "all")
+	tg.mustExist(tg.path("gp1/src/mod/cache/download/rsc.io/quote/@v/list"))
+
+	// @v/list should contain version list.
+	data, err := ioutil.ReadFile(tg.path("gp1/src/mod/cache/download/rsc.io/quote/@v/list"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !strings.Contains(string(data), "v1.5.1\n") {
+		t.Fatalf("cannot find v1.5.1 in @v/list:\n%s", data)
+	}
+
+	tg.setenv("GOPROXY", "file:///nonexist")
+	tg.run("list", "-getmode=local", "all")
+
+	tg.setenv("GOPATH", tg.path("gp2"))
+	tg.runFail("list", "-getmode=local", "all")
+	tg.runFail("list", "all") // because GOPROXY is bogus
+
+	tg.setenv("GOPROXY", "file://"+filepath.ToSlash(tg.path("gp1/src/mod/cache/download")))
+	tg.runFail("list", "-getmode=local", "all")
+	tg.run("list", "all")
+	tg.mustExist(tg.path("gp2/src/mod/cache/download/rsc.io/quote/@v/list"))
+}
+
 func TestModVendorNoDeps(t *testing.T) {
 	tg := testgo(t)
 	tg.setenv("GO111MODULE", "on")