internal/fetch: support latest version in modcache getter

If the fsProxyModuleGetter is asked for the latest version, it finds
it by looking at the versions of all the cached zips for the module.

For golang/go#47780

Change-Id: I6e0b1f51c994e99bbcf3e66f40ef4c504fe48ce8
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/345273
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/pkgsite/main_test.go b/cmd/pkgsite/main_test.go
index ce0356f..afce39e 100644
--- a/cmd/pkgsite/main_test.go
+++ b/cmd/pkgsite/main_test.go
@@ -41,6 +41,7 @@
 	}{
 		{"local", "example.com/testmod", "There is no documentation for this package."},
 		{"modcache", "modcache.com@v1.0.0", "var V = 1"},
+		{"modcache", "modcache.com", "var V = 1"},
 		{"proxy", "example.com/single/pkg", "G is new in v1.1.0"},
 	} {
 		t.Run(test.name, func(t *testing.T) {
diff --git a/internal/fetch/getters.go b/internal/fetch/getters.go
index 75618dc..4a78b25 100644
--- a/internal/fetch/getters.go
+++ b/internal/fetch/getters.go
@@ -17,12 +17,14 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 	"time"
 
 	"golang.org/x/mod/modfile"
 	"golang.org/x/mod/module"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/proxy"
+	"golang.org/x/pkgsite/internal/version"
 )
 
 // ModuleGetter gets module data.
@@ -170,16 +172,23 @@
 }
 
 // Info returns basic information about the module.
-func (g *fsProxyModuleGetter) Info(ctx context.Context, path, version string) (_ *proxy.VersionInfo, err error) {
-	defer derrors.Wrap(&err, "fsProxyModuleGetter.Info(%q, %q)", path, version)
+func (g *fsProxyModuleGetter) Info(ctx context.Context, path, vers string) (_ *proxy.VersionInfo, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.Info(%q, %q)", path, vers)
+
+	if vers == version.Latest {
+		vers, err = g.latestVersion(path)
+		if err != nil {
+			return nil, err
+		}
+	}
 
 	// Check for a .zip file. Some directories in the download cache have .info and .mod files but no .zip.
-	f, err := g.openFile(path, version, "zip")
+	f, err := g.openFile(path, vers, "zip")
 	if err != nil {
 		return nil, err
 	}
 	f.Close()
-	data, err := g.readFile(path, version, "info")
+	data, err := g.readFile(path, vers, "info")
 	if err != nil {
 		return nil, err
 	}
@@ -191,23 +200,36 @@
 }
 
 // Mod returns the contents of the module's go.mod file.
-func (g *fsProxyModuleGetter) Mod(ctx context.Context, path, version string) (_ []byte, err error) {
-	defer derrors.Wrap(&err, "fsProxyModuleGetter.Mod(%q, %q)", path, version)
+func (g *fsProxyModuleGetter) Mod(ctx context.Context, path, vers string) (_ []byte, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.Mod(%q, %q)", path, vers)
+
+	if vers == version.Latest {
+		vers, err = g.latestVersion(path)
+		if err != nil {
+			return nil, err
+		}
+	}
 
 	// Check that .zip is readable first.
-	f, err := g.openFile(path, version, "zip")
+	f, err := g.openFile(path, vers, "zip")
 	if err != nil {
 		return nil, err
 	}
 	f.Close()
-	return g.readFile(path, version, "mod")
+	return g.readFile(path, vers, "mod")
 }
 
 // ContentDir returns an fs.FS for the module's contents.
-func (g *fsProxyModuleGetter) ContentDir(ctx context.Context, path, version string) (_ fs.FS, err error) {
-	defer derrors.Wrap(&err, "fsProxyModuleGetter.ContentDir(%q, %q)", path, version)
+func (g *fsProxyModuleGetter) ContentDir(ctx context.Context, path, vers string) (_ fs.FS, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.ContentDir(%q, %q)", path, vers)
 
-	data, err := g.readFile(path, version, "zip")
+	if vers == version.Latest {
+		vers, err = g.latestVersion(path)
+		if err != nil {
+			return nil, err
+		}
+	}
+	data, err := g.readFile(path, vers, "zip")
 	if err != nil {
 		return nil, err
 	}
@@ -215,7 +237,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return fs.Sub(zr, path+"@"+version)
+	return fs.Sub(zr, path+"@"+vers)
 }
 
 // ZipSize returns the approximate size of the zip file in bytes.
@@ -223,6 +245,28 @@
 	return 0, errors.New("fsProxyModuleGetter.ZipSize unimplemented")
 }
 
+// latestVersion gets the latest version that is in the directory.
+func (g *fsProxyModuleGetter) latestVersion(modulePath string) (_ string, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.latestVersion(%q)", modulePath)
+
+	dir, err := g.moduleDir(modulePath)
+	if err != nil {
+		return "", err
+	}
+	zips, err := filepath.Glob(filepath.Join(dir, "*.zip"))
+	if err != nil {
+		return "", err
+	}
+	if len(zips) == 0 {
+		return "", fmt.Errorf("no zips in %q for module %q: %w", g.dir, modulePath, derrors.NotFound)
+	}
+	var versions []string
+	for _, z := range zips {
+		versions = append(versions, strings.TrimSuffix(filepath.Base(z), ".zip"))
+	}
+	return version.LatestOf(versions), nil
+}
+
 func (g *fsProxyModuleGetter) readFile(path, version, suffix string) (_ []byte, err error) {
 	defer derrors.Wrap(&err, "fsProxyModuleGetter.readFile(%q, %q, %q)", path, version, suffix)
 
@@ -250,13 +294,21 @@
 }
 
 func (g *fsProxyModuleGetter) escapedPath(modulePath, version, suffix string) (string, error) {
-	ep, err := module.EscapePath(modulePath)
+	dir, err := g.moduleDir(modulePath)
 	if err != nil {
-		return "", fmt.Errorf("path: %v: %w", err, derrors.InvalidArgument)
+		return "", err
 	}
 	ev, err := module.EscapeVersion(version)
 	if err != nil {
 		return "", fmt.Errorf("version: %v: %w", err, derrors.InvalidArgument)
 	}
-	return filepath.Join(g.dir, ep, "@v", fmt.Sprintf("%s.%s", ev, suffix)), nil
+	return filepath.Join(dir, fmt.Sprintf("%s.%s", ev, suffix)), nil
+}
+
+func (g *fsProxyModuleGetter) moduleDir(modulePath string) (string, error) {
+	ep, err := module.EscapePath(modulePath)
+	if err != nil {
+		return "", fmt.Errorf("path: %v: %w", err, derrors.InvalidArgument)
+	}
+	return filepath.Join(g.dir, ep, "@v"), nil
 }
diff --git a/internal/fetch/getters_test.go b/internal/fetch/getters_test.go
index 811843e..aa99ae2 100644
--- a/internal/fetch/getters_test.go
+++ b/internal/fetch/getters_test.go
@@ -14,6 +14,7 @@
 	"github.com/google/go-cmp/cmp"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/proxy"
+	"golang.org/x/pkgsite/internal/version"
 )
 
 func TestDirectoryModuleGetterEmpty(t *testing.T) {
@@ -60,7 +61,7 @@
 	ctx := context.Background()
 	const (
 		modulePath = "github.com/jackc/pgio"
-		version    = "v1.0.0"
+		vers       = "v1.0.0"
 		goMod      = "module github.com/jackc/pgio\n\ngo 1.12\n"
 	)
 	ts, err := time.Parse(time.RFC3339, "2019-03-30T17:04:38Z")
@@ -69,21 +70,30 @@
 	}
 	g := NewFSProxyModuleGetter("testdata/modcache")
 	t.Run("info", func(t *testing.T) {
-		got, err := g.Info(ctx, modulePath, version)
+		got, err := g.Info(ctx, modulePath, vers)
 		if err != nil {
 			t.Fatal(err)
 		}
-		want := &proxy.VersionInfo{Version: version, Time: ts}
+		want := &proxy.VersionInfo{Version: vers, Time: ts}
 		if !cmp.Equal(got, want) {
 			t.Errorf("got %+v, want %+v", got, want)
 		}
 
-		if _, err := g.Info(ctx, "nozip.com", version); !errors.Is(err, derrors.NotFound) {
+		// Asking for latest should give the same version.
+		got, err = g.Info(ctx, modulePath, version.Latest)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !cmp.Equal(got, want) {
+			t.Errorf("got %+v, want %+v", got, want)
+		}
+
+		if _, err := g.Info(ctx, "nozip.com", vers); !errors.Is(err, derrors.NotFound) {
 			t.Errorf("got %v, want NotFound", err)
 		}
 	})
 	t.Run("mod", func(t *testing.T) {
-		got, err := g.Mod(ctx, modulePath, version)
+		got, err := g.Mod(ctx, modulePath, vers)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -92,12 +102,12 @@
 			t.Errorf("got %q, want %q", got, want)
 		}
 
-		if _, err := g.Mod(ctx, "nozip.com", version); !errors.Is(err, derrors.NotFound) {
+		if _, err := g.Mod(ctx, "nozip.com", vers); !errors.Is(err, derrors.NotFound) {
 			t.Errorf("got %v, want NotFound", err)
 		}
 	})
 	t.Run("contentdir", func(t *testing.T) {
-		fsys, err := g.ContentDir(ctx, modulePath, version)
+		fsys, err := g.ContentDir(ctx, modulePath, vers)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -116,7 +126,7 @@
 			t.Errorf("got %q, want %q", got, want)
 		}
 
-		if _, err := g.ContentDir(ctx, "nozip.com", version); !errors.Is(err, derrors.NotFound) {
+		if _, err := g.ContentDir(ctx, "nozip.com", vers); !errors.Is(err, derrors.NotFound) {
 			t.Errorf("got %v, want NotFound", err)
 		}
 	})