internal/stdlib: update to account for .0 major version suffixes

Starting with go1.21.0, the first version of new major Go versions will
include the .0 suffix (compare go1.20).

Update pkgsite logic to account for this switch, and add
unit+integration test support.

Fixes golang/go#60373

Change-Id: Ibdac8a3413a48f925427a5aae366bed2f691cfa6
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/508937
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/internal/fetch/latest_test.go b/internal/fetch/latest_test.go
index ab7560c..a4c6113 100644
--- a/internal/fetch/latest_test.go
+++ b/internal/fetch/latest_test.go
@@ -30,7 +30,7 @@
 	}{
 		{"example.com/basic", "v1.1.0", "v1.1.0"},
 		{"example.com/retractions", "v1.2.0", "v1.0.0"},
-		{"std", "v1.14.6", "v1.14.6"},
+		{"std", "v1.21.0", "v1.21.0"},
 	} {
 		got, err := LatestModuleVersions(context.Background(), test.modulePath, prox, nil)
 		if err != nil {
diff --git a/internal/frontend/versions.go b/internal/frontend/versions.go
index 454e242..3ede9e9 100644
--- a/internal/frontend/versions.go
+++ b/internal/frontend/versions.go
@@ -396,10 +396,10 @@
 func linkVersion(modulePath, requestedVersion, resolvedVersion string) string {
 	if modulePath == stdlib.ModulePath {
 		if strings.HasPrefix(resolvedVersion, "go") {
-			return resolvedVersion
+			return resolvedVersion // already a go version
 		}
 		if stdlib.SupportedBranches[requestedVersion] {
-			return requestedVersion
+			return requestedVersion // branch, not version
 		}
 		return goTagForVersion(resolvedVersion)
 	}
diff --git a/internal/frontend/versions_test.go b/internal/frontend/versions_test.go
index 1ed90a5..31efad6 100644
--- a/internal/frontend/versions_test.go
+++ b/internal/frontend/versions_test.go
@@ -34,14 +34,18 @@
 	return m
 }
 
-func versionSummaries(path string, versions []string, linkify func(path, version string) string) []*VersionSummary {
+func versionSummaries(path string, versions []string, isStdlib bool, linkify func(path, version string) string) []*VersionSummary {
 	vs := make([]*VersionSummary, len(versions))
 	for i, version := range versions {
+		semver := version
+		if isStdlib {
+			semver = stdlib.VersionForTag(version)
+		}
 		vs[i] = &VersionSummary{
 			Version:    version,
 			Link:       linkify(path, version),
 			CommitTime: absoluteTime(sample.CommitTime),
-			IsMinor:    isMinor(version),
+			IsMinor:    isMinor(semver),
 		}
 	}
 	return vs
@@ -80,10 +84,10 @@
 			true),
 		Documentation: []*internal.Documentation{sample.Doc},
 	}
-	makeList := func(pkgPath, modulePath, major string, versions []string, incompatible bool) *VersionList {
+	makeList := func(pkgPath, modulePath, major string, versions []string, isStdlib, incompatible bool) *VersionList {
 		return &VersionList{
 			VersionListKey: VersionListKey{ModulePath: modulePath, Major: major, Incompatible: incompatible},
-			Versions: versionSummaries(pkgPath, versions, func(path, version string) string {
+			Versions: versionSummaries(pkgPath, versions, isStdlib, func(path, version string) string {
 				return constructUnitURL(pkgPath, modulePath, version)
 			}),
 		}
@@ -114,6 +118,12 @@
 		t.Fatal(err)
 	}
 
+	// Named bool values, for readability of call sites below.
+	const (
+		stdlib, notStdlib        = true, false
+		compatible, incompatible = false, true
+	)
+
 	for _, tc := range []struct {
 		name        string
 		pkg         *internal.Unit
@@ -124,12 +134,25 @@
 			name: "want stdlib versions",
 			pkg:  nethttpPkg,
 			modules: []*internal.Module{
+				sampleModule("std", "v1.22.0-rc.1", version.TypePseudo, nethttpPkg),
+				sampleModule("std", "v1.21.0", version.TypeRelease, nethttpPkg),
+				sampleModule("std", "v1.20.0", version.TypeRelease, nethttpPkg),
 				sampleModule("std", "v1.12.5", version.TypeRelease, nethttpPkg),
 				sampleModule("std", "v1.11.6", version.TypeRelease, nethttpPkg),
 			},
 			wantDetails: &VersionsDetails{
 				ThisModule: []*VersionList{
-					makeList("net/http", "std", "go1", []string{"go1.12.5", "go1.11.6"}, false),
+					makeList(
+						"net/http", "std", "go1",
+						[]string{
+							"go1.22rc1",
+							"go1.21.0",
+							"go1.20",
+							"go1.12.5",
+							"go1.11.6",
+						},
+						stdlib, compatible,
+					),
 				},
 			},
 		},
@@ -149,7 +172,7 @@
 			wantDetails: &VersionsDetails{
 				ThisModule: []*VersionList{
 					func() *VersionList {
-						vl := makeList(v1Path, modulePath1, "v1", []string{"v1.3.0", "v1.2.3", "v1.2.1"}, false)
+						vl := makeList(v1Path, modulePath1, "v1", []string{"v1.3.0", "v1.2.3", "v1.2.1"}, notStdlib, compatible)
 						vl.Versions[2].Vulns = []vuln.Vuln{{
 							ID:      vulnEntry.ID,
 							Details: vulnEntry.Summary,
@@ -158,7 +181,7 @@
 					}(),
 				},
 				IncompatibleModules: []*VersionList{
-					makeList(v1Path, modulePath1, "v2", []string{"v2.1.0+incompatible"}, true),
+					makeList(v1Path, modulePath1, "v2", []string{"v2.1.0+incompatible"}, notStdlib, incompatible),
 				},
 				OtherModules: []string{"test.com", modulePath2},
 			},
@@ -176,7 +199,7 @@
 			},
 			wantDetails: &VersionsDetails{
 				ThisModule: []*VersionList{
-					makeList(v2Path, modulePath2, "v2", []string{"v2.2.1-alpha.1", "v2.0.0"}, false),
+					makeList(v2Path, modulePath2, "v2", []string{"v2.2.1-alpha.1", "v2.0.0"}, notStdlib, compatible),
 				},
 				OtherModules: []string{modulePath1},
 			},
diff --git a/internal/stdlib/gorepo.go b/internal/stdlib/gorepo.go
index 6bb2077..09ee8ca 100644
--- a/internal/stdlib/gorepo.go
+++ b/internal/stdlib/gorepo.go
@@ -161,6 +161,7 @@
 	"refs/tags/go1.13",
 	"refs/tags/go1.13beta1",
 	"refs/tags/go1.14.6",
+	"refs/tags/go1.21.0",
 	"refs/heads/dev.fuzz",
 	"refs/heads/master",
 	// other tags
diff --git a/internal/stdlib/stdlib.go b/internal/stdlib/stdlib.go
index b7fa93b..b11a0a8 100644
--- a/internal/stdlib/stdlib.go
+++ b/internal/stdlib/stdlib.go
@@ -101,6 +101,9 @@
 // TagForVersion returns the Go standard library repository tag corresponding
 // to semver. The Go tags differ from standard semantic versions in a few ways,
 // such as beginning with "go" instead of "v".
+//
+// Starting with go1.21.0, the first patch release of major go versions include
+// the .0 suffix. Previously, the .0 suffix was elided (golang/go#57631).
 func TagForVersion(v string) (_ string, err error) {
 	defer derrors.Wrap(&err, "TagForVersion(%q)", v)
 
@@ -122,7 +125,9 @@
 	prerelease := semver.Prerelease(goVersion)
 	versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease)
 	patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".")
-	if patch == "0" {
+	if patch == "0" && (semver.Compare(v, "v1.21.0") < 0 || prerelease != "") {
+		// Starting with go1.21.0, the first patch version includes .0.
+		// Prereleases do not include .0 (we don't do prereleases for other patch releases).
 		versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0")
 	}
 	goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v"))
@@ -251,10 +256,11 @@
 	return plumbing.NewTagReferenceName(tag), nil
 }
 
-// Versions returns all the versions of Go that are relevant to the discovery
-// site. These are all release versions (tags of the forms "goN.N" and
-// "goN.N.N", where N is a number) and beta or rc versions (tags of the forms
-// "goN.NbetaN" and "goN.N.NbetaN", and similarly for "rc" replacing "beta").
+// Versions returns all the semantic versions of Go that are relevant to the
+// discovery site. These are all release versions (derived from tags of the
+// forms "goN.N" and "goN.N.N", where N is a number) and beta or rc versions
+// (derived from tags of the forms "goN.NbetaN" and "goN.N.NbetaN", and
+// similarly for "rc" replacing "beta").
 func Versions() (_ []string, err error) {
 	defer derrors.Wrap(&err, "stdlib.Versions()")
 
diff --git a/internal/stdlib/stdlib_test.go b/internal/stdlib/stdlib_test.go
index 8841b0a..7425150 100644
--- a/internal/stdlib/stdlib_test.go
+++ b/internal/stdlib/stdlib_test.go
@@ -64,6 +64,26 @@
 			want:    "go1.13",
 		},
 		{
+			name:    "version v1.20.0-rc.2",
+			version: "v1.20.0-rc.2",
+			want:    "go1.20rc2",
+		},
+		{
+			name:    "version v1.20.0",
+			version: "v1.20.0",
+			want:    "go1.20",
+		},
+		{
+			name:    "version v1.21.0-rc.2",
+			version: "v1.21.0-rc.2",
+			want:    "go1.21rc2",
+		},
+		{
+			name:    "version v1.21.0",
+			version: "v1.21.0",
+			want:    "go1.21.0",
+		},
+		{
 			name:    "master branch",
 			version: "master",
 			want:    "master",
@@ -96,7 +116,7 @@
 				return
 			}
 			if got != test.want {
-				t.Errorf("TagForVersion(%q) = %q, %v, wanted %q, %v", test.version, got, err, test.want, nil)
+				t.Fatalf("TagForVersion(%q) = %q, %v, wanted %q, %v", test.version, got, err, test.want, nil)
 			}
 		})
 	}
@@ -236,7 +256,7 @@
 	}{
 		{
 			requestedVersion: "latest",
-			want:             "v1.14.6",
+			want:             "v1.21.0",
 		},
 		{
 			requestedVersion: "master",
@@ -279,7 +299,8 @@
 	otherWants := append([]string{"v1.17.6"}, commonWants...)
 	t.Run("test", func(t *testing.T) {
 		defer WithTestData()()
-		testVersions(commonWants)
+		testWants := append([]string{"v1.21.0"}, commonWants...)
+		testVersions(testWants)
 	})
 	t.Run("local", func(t *testing.T) {
 		if *repoPath == "" {
@@ -316,6 +337,8 @@
 		{"go1.0", ""},
 		{"weekly.2012-02-14", ""},
 		{"latest", "latest"},
+		{"go1.21.0", "v1.21.0"},
+		{"go1.21", "v1.21.0"},
 	} {
 		got := VersionForTag(test.in)
 		if got != test.want {