cmd/pkgsite: fix case where we don't have runtime.GOROOT()

If pkgsite is built with -trimpath, runtime.GOROOT() returns an empty
string, so we don't know where the local go stdlib is. In that case,
don't try to serve a local stdlib and instead fall back to the
stdlibZipModuleGetter. Also fix two issues that show up when using the
stdlibZipModuleGetter. First, we try to prefetch fetch.LocalVersion of
the standard library when the server starts. Instead, prefetch latest
which will have the correct behavior with both the stdlib zips and
local stdlib. Second, when we're trying to fetch the stdlib using git
make sure that we're on another branch than the one we're trying to
fetch into because git won't let us fetch into the branch we're on.

Fixes golang/go#64903

Change-Id: I9dfb22d50b7738080490ce2682e0cf187c16d2d1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/554195
TryBot-Bypass: Michael Matloob <matloob@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/pkgsite/main.go b/cmd/pkgsite/main.go
index 7e6bd12..f9eae15 100644
--- a/cmd/pkgsite/main.go
+++ b/cmd/pkgsite/main.go
@@ -384,13 +384,15 @@
 	if cfg.useLocalStdlib {
 		goRepo := *goRepoPath
 		if goRepo == "" {
-			goRepo = runtime.GOROOT()
+			goRepo = getGOROOT()
 		}
-		mg, err := fetch.NewGoPackagesStdlibModuleGetter(ctx, goRepo)
-		if err != nil {
-			log.Errorf(ctx, "loading packages from stdlib: %v", err)
-		} else {
-			getters = append(getters, mg)
+		if goRepo != "" { // if goRepo == "" we didn't get a *goRepoPath and couldn't find GOROOT. Fall back to the zip files.
+			mg, err := fetch.NewGoPackagesStdlibModuleGetter(ctx, goRepo)
+			if err != nil {
+				log.Errorf(ctx, "loading packages from stdlib: %v", err)
+			} else {
+				getters = append(getters, mg)
+			}
 		}
 	}
 
@@ -404,6 +406,17 @@
 	return getters, nil
 }
 
+func getGOROOT() string {
+	if rg := runtime.GOROOT(); rg != "" {
+		return rg
+	}
+	b, err := exec.Command("go", "env", "GOROOT").Output()
+	if err != nil {
+		return ""
+	}
+	return strings.TrimSpace(string(b))
+}
+
 func newServer(getters []fetch.ModuleGetter, localModules []frontend.LocalModule, prox *proxy.Client) (*frontend.Server, error) {
 	lds := fetchdatasource.Options{
 		Getters:              getters,
@@ -424,7 +437,7 @@
 	for _, lm := range localModules {
 		go lds.GetUnitMeta(context.Background(), "", lm.ModulePath, fetch.LocalVersion)
 	}
-	go lds.GetUnitMeta(context.Background(), "", "std", fetch.LocalVersion)
+	go lds.GetUnitMeta(context.Background(), "", "std", "latest")
 
 	server, err := frontend.NewServer(frontend.ServerConfig{
 		DataSourceGetter: func(context.Context) internal.DataSource { return lds },
diff --git a/internal/stdlib/gorepo.go b/internal/stdlib/gorepo.go
index 10c361e..bba520c 100644
--- a/internal/stdlib/gorepo.go
+++ b/internal/stdlib/gorepo.go
@@ -13,6 +13,7 @@
 	"os/exec"
 	"path/filepath"
 	"runtime"
+	"strings"
 
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/version"
@@ -21,7 +22,7 @@
 // A goRepo represents a git repo holding the Go standard library.
 type goRepo interface {
 	// Clone the repo at the given version to the directory.
-	clone(ctx context.Context, version string, toDirectory string) (refName string, err error)
+	clone(ctx context.Context, version string, toDirectory string) (hash string, err error)
 
 	// Return all the refs of the repo.
 	refs(ctx context.Context) ([]ref, error)
@@ -29,10 +30,10 @@
 
 type remoteGoRepo struct{}
 
-func (remoteGoRepo) clone(ctx context.Context, v, directory string) (refName string, err error) {
+func (remoteGoRepo) clone(ctx context.Context, v, directory string) (hash string, err error) {
 	defer derrors.Wrap(&err, "remoteGoRepo.clone(%q)", v)
 
-	refName, err = refNameForVersion(v)
+	refName, err := refNameForVersion(v)
 	if err != nil {
 		return "", err
 	}
@@ -44,17 +45,29 @@
 	if err := cmd.Run(); err != nil {
 		return "", err
 	}
-	cmd = exec.CommandContext(ctx, "git", "fetch", "-f", "--depth=1", "--", GoRepoURL, refName+":main")
+	cmd = exec.CommandContext(ctx, "git", "fetch", "-f", "--depth=1", "--", GoRepoURL, refName)
 	cmd.Dir = directory
 	if b, err := cmd.CombinedOutput(); err != nil {
 		return "", fmt.Errorf("running git fetch: %v: %s", err, b)
 	}
-	cmd = exec.CommandContext(ctx, "git", "checkout", "main")
+	cmd = exec.CommandContext(ctx, "git", "rev-parse", "FETCH_HEAD")
 	cmd.Dir = directory
-	if b, err := cmd.CombinedOutput(); err != nil {
-		return "", fmt.Errorf("running git checkout: %v: %s", err, b)
+	b, err := cmd.Output()
+	if err != nil {
+		if ee, ok := err.(*exec.ExitError); ok {
+			return "", fmt.Errorf("running git rev-parse: %v: %s", err, ee.Stderr)
+		}
+		return "", fmt.Errorf("running git rev-parse: %v", err)
 	}
-	return refName, nil
+	cmd = exec.CommandContext(ctx, "git", "checkout", "FETCH_HEAD")
+	cmd.Dir = directory
+	if err := cmd.Run(); err != nil {
+		if ee, ok := err.(*exec.ExitError); ok {
+			return "", fmt.Errorf("running git checkout: %v: %s", err, ee.Stderr)
+		}
+		return "", fmt.Errorf("running git checkout: %v", err)
+	}
+	return strings.TrimSpace(string(b)), nil
 }
 
 type ref struct {
@@ -104,19 +117,22 @@
 	cmd.Dir = g.path
 	b, err := cmd.Output()
 	if err != nil {
+		if ee, ok := err.(*exec.ExitError); ok {
+			return nil, fmt.Errorf("running git show-ref: %s", ee.Stderr)
+		}
 		return nil, fmt.Errorf("running git show-ref: %v", err)
 	}
 	return gitOutputToRefs(b)
 }
 
-func (g *localGoRepo) clone(ctx context.Context, v, directory string) (refName string, err error) {
+func (g *localGoRepo) clone(ctx context.Context, v, directory string) (hash string, err error) {
 	return "", nil
 }
 
 type testGoRepo struct {
 }
 
-func (t *testGoRepo) clone(ctx context.Context, v, directory string) (refName string, err error) {
+func (t *testGoRepo) clone(ctx context.Context, v, directory string) (hash string, err error) {
 	defer derrors.Wrap(&err, "testGoRepo.clone(%q)", v)
 	if v == TestMasterVersion {
 		v = version.Master
@@ -174,7 +190,16 @@
 		}
 		return "", fmt.Errorf("running git commit: %v", err)
 	}
-	return "HEAD", nil
+	cmd = exec.CommandContext(ctx, "git", "rev-parse", "HEAD")
+	cmd.Dir = directory
+	b, err := cmd.Output()
+	if err != nil {
+		if ee, ok := err.(*exec.ExitError); ok {
+			return "", fmt.Errorf("running git rev-parse: %v: %s", err, ee.Stderr)
+		}
+		return "", fmt.Errorf("running git rev-parse: %v", err)
+	}
+	return strings.TrimSpace(string(b)), nil
 }
 
 // testDataPath returns a path corresponding to a path relative to the calling
diff --git a/internal/stdlib/stdlib.go b/internal/stdlib/stdlib.go
index 4f16c5c..49fb833 100644
--- a/internal/stdlib/stdlib.go
+++ b/internal/stdlib/stdlib.go
@@ -324,21 +324,6 @@
 	return resolvedVersion, nil
 }
 
-func hashForRef(ctx context.Context, dir, tag string) (string, error) {
-	cmd := exec.CommandContext(ctx, "git", "show-ref", "--verify", "--", tag)
-	cmd.Dir = dir
-	b, err := cmd.Output()
-	if err != nil {
-		return "", fmt.Errorf("running git show-ref: %v", err)
-	}
-	b = bytes.TrimSpace(b)
-	f := bytes.Fields(b)
-	if len(f) != 2 {
-		return "", fmt.Errorf("invalid output from git show-ref: %q: expect two fields", b)
-	}
-	return string(f[0]), nil
-}
-
 func commiterTime(ctx context.Context, dir, object string) (time.Time, error) {
 	cmd := exec.CommandContext(ctx, "git", "show", "--no-patch", "--no-notes", "--format=%aI", object)
 	cmd.Dir = dir
@@ -370,17 +355,13 @@
 			err = rmallerr
 		}
 	}()
-	refName, err := getGoRepo().clone(ctx, requestedVersion, dir)
+	hash, err := getGoRepo().clone(ctx, requestedVersion, dir)
 	if err != nil {
 		return nil, "", time.Time{}, "", err
 	}
 	var buf bytes.Buffer
 	z := zip.NewWriter(&buf)
 
-	hash, err := hashForRef(ctx, dir, refName)
-	if err != nil {
-		return nil, "", time.Time{}, "", err
-	}
 	commitTime, err = commiterTime(ctx, dir, hash)
 	if err != nil {
 		return nil, "", time.Time{}, "", err