internal/task: add GetNextVersions

Add a function that gets the next version number for each of the
possible release types. Maybe it should be parameterized by the Kind
enum I added in CL 408295, at least for single releases like
major/beta/rc. That will become clearer when it's wired up.

For golang/go#51797.

Change-Id: Iccb94333d967ee620a9d2ae8ceacc87c7ea7007d
Reviewed-on: https://go-review.googlesource.com/c/build/+/408955
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alex Rakoczy <alex@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Heschi Kreinick <heschi@google.com>
diff --git a/internal/task/gerrit.go b/internal/task/gerrit.go
index ccf5fa4..7874e2b 100644
--- a/internal/task/gerrit.go
+++ b/internal/task/gerrit.go
@@ -19,6 +19,8 @@
 	AwaitSubmit(ctx context.Context, changeID string) (string, error)
 	// Tag creates a tag on project at the specified commit.
 	Tag(ctx context.Context, project, tag, commit string) error
+	// ListTags returns all the tags on project.
+	ListTags(ctx context.Context, project string) ([]string, error)
 }
 
 type realGerritClient struct {
@@ -89,6 +91,18 @@
 	return err
 }
 
+func (c *realGerritClient) ListTags(ctx context.Context, project string) ([]string, error) {
+	tags, err := c.client.GetProjectTags(ctx, project)
+	if err != nil {
+		return nil, err
+	}
+	var tagNames []string
+	for _, tag := range tags {
+		tagNames = append(tagNames, strings.TrimPrefix(tag.Ref, "refs/tags/"))
+	}
+	return tagNames, nil
+}
+
 // changeLink returns a link to the review page for the CL with the specified
 // change ID. The change ID must be in the project~cl# form.
 func changeLink(changeID string) string {
diff --git a/internal/task/milestones.go b/internal/task/milestones.go
index 4601186..f8ac385 100644
--- a/internal/task/milestones.go
+++ b/internal/task/milestones.go
@@ -4,7 +4,6 @@
 	"context"
 	"fmt"
 	"sort"
-	"strconv"
 	"strings"
 
 	"github.com/google/go-github/github"
@@ -76,16 +75,6 @@
 	return ReleaseMilestones{Current: currentMilestone, Next: nextMilestone}, nil
 }
 
-func nextVersion(version string) (string, error) {
-	parts := strings.Split(version, ".")
-	n, err := strconv.Atoi(parts[len(parts)-1])
-	if err != nil {
-		return "", err
-	}
-	parts[len(parts)-1] = strconv.Itoa(n + 1)
-	return strings.Join(parts, "."), nil
-}
-
 func uppercaseVersion(version string) string {
 	return strings.Replace(version, "go", "Go", 1)
 }
diff --git a/internal/task/version.go b/internal/task/version.go
index e83c687..d9f9a20 100644
--- a/internal/task/version.go
+++ b/internal/task/version.go
@@ -2,6 +2,8 @@
 
 import (
 	"fmt"
+	"strconv"
+	"strings"
 
 	"golang.org/x/build/gerrit"
 	"golang.org/x/build/internal/workflow"
@@ -13,6 +15,70 @@
 	Project string
 }
 
+type NextVersions struct {
+	CurrentMinor  string
+	PreviousMinor string
+	Beta          string
+	RC            string
+	Major         string
+}
+
+// GetNextVersions returns the next version for each type of release.
+func (t *VersionTasks) GetNextVersions(ctx *workflow.TaskContext) (NextVersions, error) {
+	tags, err := t.Gerrit.ListTags(ctx, t.Project)
+	if err != nil {
+		return NextVersions{}, err
+	}
+	tagSet := map[string]bool{}
+	for _, tag := range tags {
+		tagSet[tag] = true
+	}
+	// Find the most recently released major version.
+	currentMajor := 0
+	for ; ; currentMajor++ {
+		if !tagSet[fmt.Sprintf("go1.%d", currentMajor+1)] {
+			break
+		}
+	}
+	var savedError error
+	findUnused := func(v string) string {
+		for {
+			if !tagSet[v] {
+				return v
+			}
+			var err error
+			v, err = nextVersion(v)
+			if err != nil {
+				savedError = err
+				return ""
+			}
+		}
+	}
+	// Find the next missing tag for each release type.
+	result := NextVersions{
+		CurrentMinor:  findUnused(fmt.Sprintf("go1.%d.1", currentMajor)),
+		PreviousMinor: findUnused(fmt.Sprintf("go1.%d.1", currentMajor-1)),
+		Beta:          findUnused(fmt.Sprintf("go1.%dbeta1", currentMajor+1)),
+		RC:            findUnused(fmt.Sprintf("go1.%drc1", currentMajor+1)),
+		Major:         fmt.Sprintf("go1.%d", currentMajor+1),
+	}
+	return result, savedError
+}
+
+func nextVersion(version string) (string, error) {
+	lastNonDigit := strings.LastIndexFunc(version, func(r rune) bool {
+		return r < '0' || r > '9'
+	})
+	if lastNonDigit == -1 || len(version) == lastNonDigit {
+		return "", fmt.Errorf("malformatted Go version %q", version)
+	}
+	n, err := strconv.Atoi(version[lastNonDigit+1:])
+	if err != nil {
+		return "", fmt.Errorf("malformatted Go version %q (%v)", version, err)
+	}
+	return fmt.Sprintf("%s%d", version[:lastNonDigit+1], n+1), nil
+}
+
 // CreateAutoSubmitVersionCL mails an auto-submit change to update VERSION on branch.
 func (t *VersionTasks) CreateAutoSubmitVersionCL(ctx *workflow.TaskContext, branch, version string) (string, error) {
 	return t.Gerrit.CreateAutoSubmitChange(ctx, gerrit.ChangeInput{
diff --git a/internal/task/version_test.go b/internal/task/version_test.go
index 0880983..6d1fd32 100644
--- a/internal/task/version_test.go
+++ b/internal/task/version_test.go
@@ -6,12 +6,77 @@
 	"strings"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"golang.org/x/build/gerrit"
 	"golang.org/x/build/internal/workflow"
 )
 
 var flagRunVersionTest = flag.Bool("run-version-test", false, "run version test, which will submit CLs to go.googlesource.com/scratch. Must have a Gerrit cookie in gitcookies.")
 
+func TestGetNextVersionsLive(t *testing.T) {
+	if !*flagRunVersionTest {
+		t.Skip("Not enabled by flags")
+	}
+
+	cl := gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookiesAuth())
+	tasks := &VersionTasks{
+		Gerrit:  &realGerritClient{client: cl},
+		Project: "go",
+	}
+	ctx := &workflow.TaskContext{
+		Context: context.Background(),
+		Logger:  &testLogger{t},
+	}
+
+	versions, err := tasks.GetNextVersions(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// It's hard to check correctness automatically.
+	t.Errorf("manually verify results: %#v", versions)
+}
+
+func TestGetNextVersions(t *testing.T) {
+	tasks := &VersionTasks{
+		Gerrit: &versionsClient{
+			tags: []string{
+				"go1.1", "go1.2",
+				"go1.3beta1", "go1.3beta2", "go1.3rc1", "go1.3", "go1.3.1", "go1.3.2", "go1.3.3",
+				"go1.4beta1", "go1.4beta2", "go1.4rc1", "go1.4", "go1.4.1",
+				"go1.5beta1", "go1.5rc1",
+			},
+		},
+		Project: "go",
+	}
+	ctx := &workflow.TaskContext{
+		Context: context.Background(),
+		Logger:  &testLogger{t},
+	}
+	versions, err := tasks.GetNextVersions(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := NextVersions{
+		CurrentMinor:  "go1.4.2",
+		PreviousMinor: "go1.3.4",
+		Beta:          "go1.5beta2",
+		RC:            "go1.5rc2",
+		Major:         "go1.5",
+	}
+	if diff := cmp.Diff(versions, want); diff != "" {
+		t.Fatalf("GetNextVersions mismatch (-want +got):\n%s", diff)
+	}
+}
+
+type versionsClient struct {
+	tags []string
+	GerritClient
+}
+
+func (c *versionsClient) ListTags(ctx context.Context, project string) ([]string, error) {
+	return c.tags, nil
+}
+
 func TestVersion(t *testing.T) {
 	if !*flagRunVersionTest {
 		t.Skip("Not enabled by flags")