Repository: GoogleContainerTools/kpt
Pull request #3328 (2022-06-24 23:27:55)

use semver library for `rpkg copy` default revision
Fixes https://github.com/GoogleContainerTools/kpt/issues/3200

The logic to compute the "latest" tag in porch and the logic to check if upstream updates are available uses the `semver` library. This PR aligns `rpkg copy` to use the same library and support the same semver formats when computing the default next revision number. 




Patch:
diff --git a/internal/cmdrpkgcopy/command.go b/internal/cmdrpkgcopy/command.go
index 2edba583..a32ee480 100644
--- a/internal/cmdrpkgcopy/command.go
+++ b/internal/cmdrpkgcopy/command.go
@@ -17,14 +17,15 @@ package cmdrpkgcopy
 import (
 	"context"
 	"fmt"
-	"regexp"
 	"strconv"
+	"strings"
 
 	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/util/porch"
 	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	"github.com/spf13/cobra"
+	"golang.org/x/mod/semver"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -146,8 +147,7 @@ func (r *runner) getPackageRevisionSpec() (*porchapi.PackageRevisionSpec, error)
 }
 
 // defaultPackageRevision attempts to return a default package revision number
-// of "latest + 1" given a package name, repository, and namespace. It only
-// understands revisions following `v[0-9]+` formats.
+// of "latest + 1" given a package name, repository, and namespace.
 func (r *runner) defaultPackageRevision(packageName, repository string) (string, error) {
 	// get all package revisions
 	packageRevisionList := porchapi.PackageRevisionList{}
@@ -187,18 +187,18 @@ func (r *runner) defaultPackageRevision(packageName, repository string) (string,
 }
 
 func nextRevisionNumber(latestRevision string) (string, error) {
-	match, err := regexp.MatchString("^v[0-9]+$", latestRevision)
-	if err != nil {
-		return "", err
+	if !semver.IsValid(latestRevision) {
+		return "", fmt.Errorf("invalid revision format %s; explicit --revision flag is required", latestRevision)
 	}
-	if !match {
-		return "", fmt.Errorf("could not understand format of latest revision %q; explicit --revision flag is required", latestRevision)
-	}
-	i, err := strconv.Atoi(latestRevision[1:])
+
+	parts := strings.Split(latestRevision[1:], ".")
+	lastIndex := len(parts) - 1
+	i, err := strconv.Atoi(parts[lastIndex])
 	if err != nil {
 		return "", err
 	}
 	i++
-	next := "v" + strconv.Itoa(i)
+	parts[lastIndex] = strconv.Itoa(i)
+	next := "v" + strings.Join(parts, ".")
 	return next, nil
 }


Test patch:
diff --git a/internal/cmdrpkgcopy/command_test.go b/internal/cmdrpkgcopy/command_test.go
new file mode 100644
index 00000000..2a42987b
--- /dev/null
+++ b/internal/cmdrpkgcopy/command_test.go
@@ -0,0 +1,59 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmdrpkgcopy
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestNextRevisionNumber(t *testing.T) {
+	testcases := map[string]struct {
+		input    string
+		expected string
+		err      string
+	}{
+		"invalid": {
+			input: "alskdfj",
+			err:   "invalid revision format alskdfj; explicit --revision flag is required",
+		},
+		"no dots": {
+			input:    "v4",
+			expected: "v5",
+		},
+		"one dot": {
+			input:    "v3.1",
+			expected: "v3.2",
+		},
+		"two dots": {
+			input:    "v1.2.3",
+			expected: "v1.2.4",
+		},
+	}
+
+	for name, tc := range testcases {
+		t.Run(name, func(t *testing.T) {
+			output, err := nextRevisionNumber(tc.input)
+			if tc.err != "" {
+				require.Error(t, err)
+				require.Equal(t, tc.err, err.Error())
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tc.expected, output)
+			}
+		})
+	}
+}