maintner/maintnerd/maintapi: recognize golang.org/x internal branches

Fixes golang/go#46154.
Updates golang/go#36882.

Change-Id: I6369d88298e0e75f4aa960f3de453dd71a794579
Reviewed-on: https://go-review.googlesource.com/c/build/+/319790
Reviewed-by: Carlos Amedee <carlos@golang.org>
Run-TryBot: Carlos Amedee <carlos@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/maintner/maintnerd/maintapi/api.go b/maintner/maintnerd/maintapi/api.go
index 08512cd..46e1daa 100644
--- a/maintner/maintnerd/maintapi/api.go
+++ b/maintner/maintnerd/maintapi/api.go
@@ -181,13 +181,8 @@
 		}
 	} else {
 		// TryBot on a subrepo.
-		trimBundleSuffix := func(branch string) string {
-			// There was only one branch with a suffix, release-branch.go1.15-bundle in x/net, so just hardcode it.
-			// TODO: This special case can be removed when Go 1.17 is out and 1.15 is no longer supported.
-			return strings.TrimSuffix(branch, "-bundle")
-		}
-		if major, minor, ok := parseReleaseBranchVersion(trimBundleSuffix(w.Branch)); ok {
-			// An release-branch.goX.Y (or one with a -suffix) branch is used for internal needs
+		if major, minor, ok := parseInternalBranchVersion(w.Branch); ok {
+			// An internal-branch.goX.Y-suffix branch is used for internal needs
 			// of goX.Y only, so no reason to test it on other Go versions.
 			goBranch := fmt.Sprintf("release-branch.go%d.%d", major, minor)
 			goCommit := goProj.Ref("refs/heads/" + goBranch)
@@ -209,7 +204,7 @@
 				w.GoVersion = append(w.GoVersion, &apipb.MajorMinor{r.Major, r.Minor})
 			}
 		} else {
-			// A branch that is neither release-branch.goX.Y nor "master":
+			// A branch that is neither internal-branch.goX.Y-suffix nor "master":
 			// maybe some custom branch like "dev.go2go".
 			// Test it against Go tip only until we want to do more.
 			w.GoCommit = []string{goProj.Ref("refs/heads/master").String()}
@@ -385,6 +380,42 @@
 	return int32(maj), int32(min), ok
 }
 
+// parseInternalBranchVersion parses the major-minor version pair
+// from internal-branch.goX-suffix or internal-branch.goX.Y-suffix internal branch names,
+// and reports whether the internal branch name is valid.
+//
+// Before Go 1.16, golang.org/x repositories used release-branch.go1.n as internal
+// branch names, so this function also accepts those branch names. See issue 36882.
+//
+// For example, "internal-branch.go1-vendor" is parsed as version 1.0,
+// and "internal-branch.go1.2-vendor" is parsed as version 1.2.
+func parseInternalBranchVersion(branchName string) (major, minor int32, ok bool) {
+	// Accept release branches as internal branches, since Go 1.15 still uses them.
+	// There was only one branch with a suffix, release-branch.go1.15-bundle in x/net, so just hardcode it.
+	// TODO: This special case can be removed when Go 1.17 is out and 1.15 is no longer supported.
+	if maj, min, ok := version.ParseReleaseBranch(strings.TrimSuffix(branchName, "-bundle")); ok {
+		return int32(maj), int32(min), ok
+	}
+
+	const prefix = "internal-branch."
+	if !strings.HasPrefix(branchName, prefix) {
+		return 0, 0, false
+	}
+	tagAndSuffix := branchName[len(prefix):] // "go1.16-vendor".
+	i := strings.Index(tagAndSuffix, "-")
+	if i == -1 || i == len(tagAndSuffix)-1 {
+		// No "-suffix" at all, or empty suffix. Reject.
+		return 0, 0, false
+	}
+	tag := tagAndSuffix[:i] // "go1.16".
+	maj, min, pat, ok := version.ParseTag(tag)
+	if !ok || pat != 0 {
+		// Not a major Go release tag. Reject.
+		return 0, 0, false
+	}
+	return int32(maj), int32(min), true
+}
+
 // ListGoReleases lists Go releases. A release is considered to exist
 // if a tag for it exists.
 func (s apiService) ListGoReleases(ctx context.Context, req *apipb.ListGoReleasesRequest) (*apipb.ListGoReleasesResponse, error) {
diff --git a/maintner/maintnerd/maintapi/api_test.go b/maintner/maintnerd/maintapi/api_test.go
index fb62e0e..1c3ca73 100644
--- a/maintner/maintnerd/maintapi/api_test.go
+++ b/maintner/maintnerd/maintapi/api_test.go
@@ -168,11 +168,11 @@
 			`go_version:<major:1 minor:17 > go_version:<major:1 minor:16 > go_version:<major:1 minor:15 > `},
 
 		// Test that a golang.org/x repo TryBot on a branch like
-		// "release-branch.go1.N" or "release-branch.go1.N-suffix"
+		// "internal-branch.go1.N-suffix", "release-branch.go1.N", or "release-branch.go1.N-suffix"
 		// tests with Go 1.N (rather than tip + two supported releases).
-		// See issues 28891 and 42127.
+		// See issues 28891, 42127, and 36882.
 		{"net", 314649, &gerrit.ChangeInfo{}, nil, `project:"net" branch:"internal-branch.go1.16-vendor" change_id:"I2c54ce3b2acf1c5efdea66db0595b93a3f5ae5f3" commit:"3f4a416c7d3b3b41375d159f71ff0a801fc0102b" ` +
-			`go_commit:"9995c6b50aa55c1cc1236d1d688929df512dad53" go_branch:"master" go_version:<major:1 minor:17 > `}, // TODO(golang.org/issue/46154): This should be tested with Go 1.16, not tip.
+			`go_commit:"e67a58b7cb2b228e04477dfdb1aacd8348e63534" go_branch:"release-branch.go1.16" go_version:<major:1 minor:16 > `},
 		{"net", 258478, &gerrit.ChangeInfo{}, nil, `project:"net" branch:"release-branch.go1.15" change_id:"I546597cedf3715e6617babcb3b62140bf1857a27" commit:"a5fa9d4b7c91aa1c3fecbeb6358ec1127b910dd6" ` +
 			`go_commit:"72ccabc99449b2cb5bb1438eb90244d55f7b02f5" go_branch:"release-branch.go1.15" go_version:<major:1 minor:15 > `},
 		{"net", 264058, &gerrit.ChangeInfo{}, nil, `project:"net" branch:"release-branch.go1.15-bundle" change_id:"I546597cedf3715e6617babcb3b62140bf1857a27" commit:"abf26a14a65b111d492067f407f32455c5b1048c" ` +
@@ -257,6 +257,36 @@
 	}
 }
 
+func TestParseInternalBranchVersion(t *testing.T) {
+	tests := []struct {
+		name    string
+		wantMaj int32
+		wantMin int32
+		wantOK  bool
+	}{
+		{"internal-branch.go1.16-vendor", 1, 16, true},
+		{"internal-branch.go1.16-", 0, 0, false}, // Empty suffix is rejected.
+		{"internal-branch.go1.16", 0, 0, false},  // No suffix is rejected.
+		{"not-internal-branch", 0, 0, false},
+		{"internal-branch.go1.16.2", 0, 0, false},
+		{"internal-branch.go42-suffix", 42, 0, true}, // Be ready in case Go 42 is released after 7.5 million years.
+
+		// Old naming scheme for internal branches. See issue 36882.
+		{"release-branch.go1.15", 1, 15, true},
+		{"release-branch.go1.15-bundle", 1, 15, true},
+		{"release-branch.go1.15-other", 0, 0, false}, // There are no other branches that need to be supported for now.
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			maj, min, ok := parseInternalBranchVersion(tt.name)
+			if ok != tt.wantOK || maj != tt.wantMaj || min != tt.wantMin {
+				t.Errorf("parseInternalBranchVersion(%q) = Go %v.%v ok=%v; want Go %v.%v ok=%v", tt.name,
+					maj, min, ok, tt.wantMaj, tt.wantMin, tt.wantOK)
+			}
+		})
+	}
+}
+
 var (
 	corpusMu    sync.Mutex
 	corpusCache *maintner.Corpus