cmd/go/internal/modfetch: make gopkg.in support more seamless

Gopkg.in did semantic import versioning first.
We should make it as easy as possible for users
of gopkg.in to migrate to vgo.

Before this CL, gopkg.in was supported as a special
kind of code fetcher that rewrote tags to add a funny
prefix, so that gopkg.in/russross/blackfriday.v2 v2.0.0
showed up in go.mod as

	gopkg.in/russross/blackfriday.v2 v1.0.0-gopkgin-v2.0.0

This was needed because by the usual module path syntax
rules, gopkg.in/russross/blackfriday.v2 should be a v1 of
a package. But it's rather mysterious.

Instead, as a nod to gopkg.in being here first, this CL changes
the module path syntax rules to understand the version ranges
for a given gopkg.in path, allowing the expected:

	gopkg.in/russross/blackfriday.v2 v2.0.0

It even supports .v1 paths correctly.

The version fixer recognizes the old v1.0.0-gopkgin-v2.0.0
and turns it into plain v2.0.0.

Fixes golang/go#23991.
Fixes golang/go#24099.

Change-Id: I579863c2d36780bd1e9f1b786f41560bd774d8e3
Reviewed-on: https://go-review.googlesource.com/107658
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/internal/modfetch/coderepo.go b/vendor/cmd/go/internal/modfetch/coderepo.go
index 466c405..787cb34 100644
--- a/vendor/cmd/go/internal/modfetch/coderepo.go
+++ b/vendor/cmd/go/internal/modfetch/coderepo.go
@@ -53,11 +53,33 @@
 		pseudoMajor = pathMajor[1:]
 	}
 
+	// At this point we might have:
+	//	codeRoot = github.com/rsc/foo
+	//	path = github.com/rsc/foo/bar/v2
+	//	pathPrefix = github.com/rsc/foo/bar
+	//	pathMajor = /v2
+	//	pseudoMajor = v2
+	//
+	// Compute codeDir = bar, the subdirectory within the repo
+	// corresponding to the module root.
+	codeDir := strings.Trim(strings.TrimPrefix(pathPrefix, codeRoot), "/")
+	if strings.HasPrefix(path, "gopkg.in/") {
+		// But gopkg.in is a special legacy case, in which pathPrefix does not start with codeRoot.
+		// For example we might have:
+		//	codeRoot = gopkg.in/yaml.v2
+		//	pathPrefix = gopkg.in/yaml
+		//	pathMajor = .v2
+		//	pseudoMajor = v2
+		//	codeDir = pathPrefix (because codeRoot is not a prefix of pathPrefix)
+		// Clear codeDir - the module root is the repo root for gopkg.in repos.
+		codeDir = ""
+	}
+
 	r := &codeRepo{
 		modPath:     path,
 		code:        code,
 		codeRoot:    codeRoot,
-		codeDir:     strings.Trim(strings.TrimPrefix(pathPrefix, codeRoot), "/"),
+		codeDir:     codeDir,
 		pathPrefix:  pathPrefix,
 		pathMajor:   pathMajor,
 		pseudoMajor: pseudoMajor,
@@ -98,6 +120,9 @@
 }
 
 func (r *codeRepo) Stat(rev string) (*RevInfo, error) {
+	if rev == "latest" {
+		return r.Latest()
+	}
 	codeRev := r.revToRev(rev)
 	if semver.IsValid(codeRev) && r.codeDir != "" {
 		codeRev = r.codeDir + "/" + codeRev
@@ -118,14 +143,17 @@
 }
 
 func (r *codeRepo) convert(info *codehost.RevInfo) (*RevInfo, error) {
+	versionOK := func(v string) bool {
+		return semver.IsValid(v) && v == semver.Canonical(v) && !isPseudoVersion(v)
+	}
 	v := info.Version
 	if r.codeDir == "" {
-		if !semver.IsValid(v) || isPseudoVersion(v) {
+		if !versionOK(v) {
 			v = PseudoVersion(r.pseudoMajor, info.Time, info.Short)
 		}
 	} else {
 		p := r.codeDir + "/"
-		if strings.HasPrefix(v, p) && semver.IsValid(v[len(p):]) && !isPseudoVersion(v[len(p):]) {
+		if strings.HasPrefix(v, p) && versionOK(v[len(p):]) {
 			v = v[len(p):]
 		} else {
 			v = PseudoVersion(r.pseudoMajor, info.Time, info.Short)
@@ -168,7 +196,7 @@
 	if err != nil {
 		return "", "", nil, err
 	}
-	if r.pathMajor == "" {
+	if r.pathMajor == "" || strings.HasPrefix(r.pathMajor, ".") {
 		if r.codeDir == "" {
 			return rev, "", nil, nil
 		}
diff --git a/vendor/cmd/go/internal/modfetch/coderepo_test.go b/vendor/cmd/go/internal/modfetch/coderepo_test.go
index 7de4816..aba2ea3 100644
--- a/vendor/cmd/go/internal/modfetch/coderepo_test.go
+++ b/vendor/cmd/go/internal/modfetch/coderepo_test.go
@@ -289,7 +289,7 @@
 	{
 		path:    "gopkg.in/yaml.v2",
 		rev:     "d670f940",
-		version: "v0.0.0-20180109114331-d670f9405373",
+		version: "v2.0.0-20180109114331-d670f9405373",
 		name:    "d670f9405373e636a5a2765eea47fac0c9bc91a4",
 		short:   "d670f9405373",
 		time:    time.Date(2018, 1, 9, 11, 43, 31, 0, time.UTC),
@@ -298,7 +298,7 @@
 	{
 		path:    "gopkg.in/check.v1",
 		rev:     "20d25e280405",
-		version: "v0.0.0-20161208181325-20d25e280405",
+		version: "v1.0.0-20161208181325-20d25e280405",
 		name:    "20d25e2804050c1cd24a7eea1e7a6447dd0e74ec",
 		short:   "20d25e280405",
 		time:    time.Date(2016, 12, 8, 18, 13, 25, 0, time.UTC),
@@ -307,7 +307,7 @@
 	{
 		path:    "gopkg.in/yaml.v2",
 		rev:     "v2",
-		version: "v0.0.0-20180328195020-5420a8b6744d",
+		version: "v2.0.0-20180328195020-5420a8b6744d",
 		name:    "5420a8b6744d3b0345ab293f6fcba19c978f1183",
 		short:   "5420a8b6744d",
 		time:    time.Date(2018, 3, 28, 19, 50, 20, 0, time.UTC),
@@ -322,6 +322,30 @@
 		time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
 		gomod:   "//vgo 0.0.4\n\nmodule vcs-test.golang.org/go/mod/gitrepo1\n",
 	},
+	{
+		path:    "gopkg.in/natefinch/lumberjack.v2",
+		rev:     "latest",
+		version: "v2.0.0-20170531160350-a96e63847dc3",
+		name:    "a96e63847dc3c67d17befa69c303767e2f84e54f",
+		short:   "a96e63847dc3",
+		time:    time.Date(2017, 5, 31, 16, 3, 50, 0, time.UTC),
+		gomod:   "//vgo 0.0.4\n\nmodule gopkg.in/natefinch/lumberjack.v2\n",
+	},
+	{
+		path: "gopkg.in/natefinch/lumberjack.v2",
+		// This repo has a v2.1 tag.
+		// We only allow semver references to tags that are fully qualified, as in v2.1.0.
+		// Because we can't record v2.1.0 (the actual tag is v2.1), we record a pseudo-version
+		// instead, same as if the tag were any other non-version-looking string.
+		// We use a v2 pseudo-version here because of the .v2 in the path, not because
+		// of the v2 in the rev.
+		rev:     "v2.1", // non-canonical semantic version turns into pseudo-version
+		version: "v2.0.0-20170531160350-a96e63847dc3",
+		name:    "a96e63847dc3c67d17befa69c303767e2f84e54f",
+		short:   "a96e63847dc3",
+		time:    time.Date(2017, 5, 31, 16, 3, 50, 0, time.UTC),
+		gomod:   "//vgo 0.0.4\n\nmodule gopkg.in/natefinch/lumberjack.v2\n",
+	},
 }
 
 func TestCodeRepo(t *testing.T) {
@@ -503,7 +527,7 @@
 	},
 	{
 		path:     "gopkg.in/russross/blackfriday.v2",
-		versions: []string{"v1.0.0-gopkgin-v2.0.0"},
+		versions: []string{"v2.0.0"},
 	},
 	{
 		path:     "gopkg.in/natefinch/lumberjack.v2",
diff --git a/vendor/cmd/go/internal/modfetch/gopkgin.go b/vendor/cmd/go/internal/modfetch/gopkgin.go
index a3b8b6f..ec72dcd 100644
--- a/vendor/cmd/go/internal/modfetch/gopkgin.go
+++ b/vendor/cmd/go/internal/modfetch/gopkgin.go
@@ -8,14 +8,12 @@
 
 import (
 	"cmd/go/internal/modfetch/codehost"
-	"cmd/go/internal/modfetch/github"
-	"cmd/go/internal/semver"
+	"cmd/go/internal/modfetch/gitrepo"
 	"fmt"
-	"io"
 	"strings"
 )
 
-func ParseGopkgIn(path string) (root, repo, major, subdir string, ok bool) {
+func parseGopkgIn(path string) (root, repo, major, subdir string, ok bool) {
 	if !strings.HasPrefix(path, "gopkg.in/") {
 		return
 	}
@@ -53,100 +51,9 @@
 }
 
 func gopkginLookup(path string) (codehost.Repo, error) {
-	root, repo, major, subdir, ok := ParseGopkgIn(path)
+	root, _, _, _, ok := parseGopkgIn(path)
 	if !ok {
 		return nil, fmt.Errorf("invalid gopkg.in/ path: %q", path)
 	}
-	gh, err := github.Lookup(repo)
-	if err != nil {
-		return nil, err
-	}
-	return &gopkgin{gh, root, repo, major, subdir}, nil
-}
-
-type gopkgin struct {
-	gh     codehost.Repo
-	root   string
-	repo   string
-	major  string
-	subdir string
-}
-
-func (r *gopkgin) Root() string {
-	return r.root
-}
-
-func (r *gopkgin) Tags(prefix string) ([]string, error) {
-	p := r.major + "."
-	list, err := r.gh.Tags(p)
-	if err != nil {
-		return nil, err
-	}
-	var out []string
-	for _, v := range list {
-		if !strings.HasPrefix(v, p) || !semver.IsValid(v) {
-			continue
-		}
-		out = append(out, "v1"+v[len(r.major):]+"-gopkgin-"+v)
-	}
-	return out, nil
-}
-
-func (r *gopkgin) Stat(rev string) (*codehost.RevInfo, error) {
-	ghRev, err := r.unconvert(rev)
-	if err != nil {
-		return nil, err
-	}
-	return r.convert(r.gh.Stat(ghRev))
-}
-
-func (r *gopkgin) Latest() (*codehost.RevInfo, error) {
-	if r.major == "v0" {
-		return r.convert(r.gh.Stat("master"))
-	}
-	return r.convert(r.gh.Stat(r.major))
-}
-
-func (r *gopkgin) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
-	ghRev, err := r.unconvert(rev)
-	if err != nil {
-		return nil, err
-	}
-	return r.gh.ReadFile(ghRev, file, maxSize)
-}
-
-func (r *gopkgin) ReadZip(rev, subdir string, maxSize int64) (io.ReadCloser, string, error) {
-	ghRev, err := r.unconvert(rev)
-	if err != nil {
-		return nil, "", err
-	}
-	return r.gh.ReadZip(ghRev, subdir, maxSize)
-}
-
-func (r *gopkgin) convert(info *codehost.RevInfo, err error) (*codehost.RevInfo, error) {
-	if err != nil {
-		return nil, err
-	}
-	v := info.Version
-	if !semver.IsValid(v) {
-		return info, nil
-	}
-	if !strings.HasPrefix(v, r.major+".") {
-		info.Version = PseudoVersion("v0", info.Time, info.Short)
-		return info, nil
-	}
-	info.Version = "v1" + v[len(r.major):] + "-gopkgin-" + v
-	return info, nil
-}
-
-func (r *gopkgin) unconvert(rev string) (ghRev string, err error) {
-	i := strings.Index(rev, "-gopkgin-")
-	if i < 0 {
-		return rev, nil
-	}
-	fake, real := rev[:i], rev[i+len("-gopkgin-"):]
-	if strings.HasPrefix(real, r.major+".") && fake == "v1"+real[len(r.major):] {
-		return real, nil
-	}
-	return "", fmt.Errorf("malformed gopkgin tag")
+	return gitrepo.Repo("https://"+root, root)
 }
diff --git a/vendor/cmd/go/internal/module/module.go b/vendor/cmd/go/internal/module/module.go
index a5d28cc..1e8f74c 100644
--- a/vendor/cmd/go/internal/module/module.go
+++ b/vendor/cmd/go/internal/module/module.go
@@ -34,19 +34,31 @@
 	if !semver.IsValid(version) {
 		return fmt.Errorf("malformed semantic version %v", version)
 	}
-	_, pathVersion, _ := SplitPathVersion(path)
-	pathVersion = strings.TrimPrefix(pathVersion, "/")
 	vm := semver.Major(version)
-	if vm == "v0" || vm == "v1" {
-		vm = ""
-	}
-	if vm != pathVersion {
+	_, pathVersion, _ := SplitPathVersion(path)
+
+	if strings.HasPrefix(pathVersion, ".") {
+		// Special-case gopkg.in path requirements.
+		pathVersion = pathVersion[1:] // cut .
+		if vm == pathVersion {
+			return nil
+		}
+	} else {
+		// Standard path requirements.
+		if pathVersion != "" {
+			pathVersion = pathVersion[1:] // cut /
+		}
+		if vm == "v0" || vm == "v1" {
+			vm = ""
+		}
+		if vm == pathVersion {
+			return nil
+		}
 		if pathVersion == "" {
 			pathVersion = "v0 or v1"
 		}
-		return fmt.Errorf("mismatched module path %v and version %v (want %v)", path, version, pathVersion)
 	}
-	return nil
+	return fmt.Errorf("mismatched module path %v and version %v (want %v)", path, version, pathVersion)
 }
 
 // firstPathOK reports whether r can appear in the first element of a module path.
@@ -125,9 +137,15 @@
 	return nil
 }
 
-// SplitPathVersion returns pathPrefix and version such that pathPrefix+pathMajor == path
+// SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
 // and version is either empty or "/vN" for N >= 2.
-func SplitPathVersion(path string) (pathPrefix, pathMajor string, ok bool) {
+// As a special case, gopkg.in paths are recognized directly;
+// they require ".vN" instead of "/vN", and for all N, not just N >= 2.
+func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
+	if strings.HasPrefix(path, "gopkg.in/") {
+		return splitGopkgIn(path)
+	}
+
 	i := len(path)
 	dot := false
 	for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
@@ -139,17 +157,39 @@
 	if i <= 1 || path[i-1] != 'v' || path[i-2] != '/' {
 		return path, "", true
 	}
-	pathPrefix, pathMajor = path[:i-2], path[i-2:]
+	prefix, pathMajor = path[:i-2], path[i-2:]
 	if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
 		return path, "", false
 	}
-	return pathPrefix, pathMajor, true
+	return prefix, pathMajor, true
 }
 
+// splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
+func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
+	if !strings.HasPrefix(path, "gopkg.in/") {
+		return path, "", false
+	}
+	i := len(path)
+	for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
+		i--
+	}
+	if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
+		// All gopkg.in paths must end in vN for some N.
+		return path, "", false
+	}
+	prefix, pathMajor = path[:i-2], path[i-2:]
+	if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
+		return path, "", false
+	}
+	return prefix, pathMajor, true
+}
+
+// MatchPathMajor reports whether the semantic version v
+// matches the path major version pathMajor.
 func MatchPathMajor(v, pathMajor string) bool {
 	m := semver.Major(v)
 	if pathMajor == "" {
 		return m == "v0" || m == "v1"
 	}
-	return pathMajor[0] == '/' && m == pathMajor[1:]
+	return (pathMajor[0] == '/' || pathMajor[0] == '.') && m == pathMajor[1:]
 }
diff --git a/vendor/cmd/go/internal/module/module_test.go b/vendor/cmd/go/internal/module/module_test.go
index a507fa1..6142a9e 100644
--- a/vendor/cmd/go/internal/module/module_test.go
+++ b/vendor/cmd/go/internal/module/module_test.go
@@ -24,6 +24,23 @@
 	{"github.com/go-yaml/yaml/v2", "v2.0.0", true},
 	{"github.com/go-yaml/yaml/v2", "v2.1.5", true},
 	{"github.com/go-yaml/yaml/v2", "v3.0.0", false},
+
+	{"gopkg.in/yaml.v0", "v0.8.0", true},
+	{"gopkg.in/yaml.v0", "v1.0.0", false},
+	{"gopkg.in/yaml.v0", "v2.0.0", false},
+	{"gopkg.in/yaml.v0", "v2.1.5", false},
+	{"gopkg.in/yaml.v0", "v3.0.0", false},
+
+	{"gopkg.in/yaml.v1", "v0.8.0", false},
+	{"gopkg.in/yaml.v1", "v1.0.0", true},
+	{"gopkg.in/yaml.v1", "v2.0.0", false},
+	{"gopkg.in/yaml.v1", "v2.1.5", false},
+	{"gopkg.in/yaml.v1", "v3.0.0", false},
+
+	{"gopkg.in/yaml.v2", "v1.0.0", false},
+	{"gopkg.in/yaml.v2", "v2.0.0", true},
+	{"gopkg.in/yaml.v2", "v2.1.5", true},
+	{"gopkg.in/yaml.v2", "v3.0.0", false},
 }
 
 func TestCheck(t *testing.T) {
@@ -144,6 +161,10 @@
 	{"x.y/z", ""},
 	{"x.y/z", "/v2"},
 	{"x.y/z", "/v3"},
+	{"gopkg.in/yaml", ".v0"},
+	{"gopkg.in/yaml", ".v1"},
+	{"gopkg.in/yaml", ".v2"},
+	{"gopkg.in/yaml", ".v3"},
 }
 
 func TestSplitPathVersion(t *testing.T) {
diff --git a/vendor/cmd/go/internal/vgo/init.go b/vendor/cmd/go/internal/vgo/init.go
index b4272e8..269e425 100644
--- a/vendor/cmd/go/internal/vgo/init.go
+++ b/vendor/cmd/go/internal/vgo/init.go
@@ -341,6 +341,11 @@
 }
 
 func fixVersion(path, vers string) (string, error) {
+	// Special case: remove the old -gopkgin- hack.
+	if strings.HasPrefix(path, "gopkg.in/") && strings.Contains(vers, "-gopkgin-") {
+		vers = vers[strings.Index(vers, "-gopkgin-")+len("-gopkgin-"):]
+	}
+
 	// fixVersion is called speculatively on every
 	// module, version pair from every go.mod file.
 	// Avoid the query if it looks OK.