internal/postgres: fix GetLatestMajorPathForV1Path for gopkg.in

GetLatestMajorPathForV1Path was not handling the special
case of gopkg.in modules correctly.

Also, split the logic for extracting the major version of a module
path to a separate function.

Change-Id: Ie7758330a14ebaab6360fafcc4bf2d3ae46f6479
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/307470
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/postgres/path.go b/internal/postgres/path.go
index 61df19d..239a0be 100644
--- a/internal/postgres/path.go
+++ b/internal/postgres/path.go
@@ -13,6 +13,7 @@
 	"strings"
 
 	"github.com/lib/pq"
+	"golang.org/x/mod/module"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/database"
 	"golang.org/x/pkgsite/internal/derrors"
@@ -35,7 +36,7 @@
 			ORDER BY p.path DESC
 			LIMIT 1
 		);`
-	paths := map[string]string{}
+	paths := map[string]string{} // from unit path to series path
 	err = db.db.RunQuery(ctx, q, func(rows *sql.Rows) error {
 		var p, sp string
 		if err := rows.Scan(&p, &sp); err != nil {
@@ -56,14 +57,10 @@
 		// Trim the series path and suffix from the unit path.
 		// Keep only the N following vN.
 		suffix := internal.Suffix(v1path, sp)
-		v := strings.TrimSuffix(strings.TrimPrefix(
-			strings.TrimSuffix(strings.TrimPrefix(p, sp), suffix), "/v"), "/")
-		var i int
-		if v != "" {
-			i, err = strconv.Atoi(v)
-			if err != nil {
-				return "", 0, fmt.Errorf("strconv.Atoi(%q): %v", v, err)
-			}
+		modPath := strings.TrimSuffix(p, "/"+suffix)
+		i, err := modulePathMajorVersion(modPath)
+		if err != nil {
+			return "", 0, err
 		}
 		if maj <= i {
 			maj = i
@@ -77,6 +74,23 @@
 	return majPath, maj, nil
 }
 
+// modulePathMajorVersion returns the numeric major version for the given module path.
+// If the path has no "vN" suffix, it returns 1.
+func modulePathMajorVersion(modulePath string) (int, error) {
+	_, m, ok := module.SplitPathVersion(modulePath)
+	if !ok {
+		return 0, fmt.Errorf("bad module path %q", modulePath)
+	}
+	if m == "" {
+		return 1, nil
+	}
+	// First two characters are either ".v" or "/v".
+	if len(m) < 3 {
+		return 0, fmt.Errorf("bad version %q from module.SplitPathVersion(%q)", m, modulePath)
+	}
+	return strconv.Atoi(m[2:])
+}
+
 // upsertPath adds path into the paths table if it does not exist, and returns
 // its ID either way.
 // It assumes it is running inside a transaction.
diff --git a/internal/postgres/path_test.go b/internal/postgres/path_test.go
index 9600f29..61f0cfd 100644
--- a/internal/postgres/path_test.go
+++ b/internal/postgres/path_test.go
@@ -44,6 +44,12 @@
 			[]string{"m.com/v4@v4.0.0"},
 			"m.com/v4", 4,
 		},
+		{
+			"gopkg.in",
+			"gopkg.in/yaml",
+			[]string{"gopkg.in/yaml.v1@v1.0.0", "gopkg.in/yaml.v2@v2.0.0"},
+			"gopkg.in/yaml.v2", 2,
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			testDB, release := acquire(t)
@@ -78,6 +84,26 @@
 	}
 }
 
+func TestModulePathMajorVersion(t *testing.T) {
+	for _, test := range []struct {
+		in   string
+		want int
+	}{
+		{"m.com", 1},
+		{"m.com/v123", 123},
+		{"gopkg.in/m.v1", 1},
+		{"gopkg.in/m.v35", 35},
+	} {
+		got, err := modulePathMajorVersion(test.in)
+		if err != nil {
+			t.Fatalf("%s: %v", test.in, err)
+		}
+		if got != test.want {
+			t.Errorf("%s: got %d, want %d", test.in, got, test.want)
+		}
+	}
+}
+
 func TestUpsertPathConcurrently(t *testing.T) {
 	// Verify that we get no constraint violations or other errors when
 	// the same path is upserted multiple times concurrently.