cmd/go/internal/modfetch/gitrepo: simplify and cache git access

This CL drops the Root method from codehost.Repo,
since a codehost.Repo is really just about access to a source tree.
It has no real knowledge of anything related to modules or
import paths, so the Root method was out of place.

Dropping Root and then caching gitrepo by remote
should guarantee that there is at most one *gitrepo.repo
for a particular vcswork checkout directory.

Add a lock to codehost.Run to guarantee that at most one
command runs in a directory at a time. This should not
be strictly necessary now, but it lets bugs be just inefficiencies
instead of crashes.

Change-Id: I983c7a4691de3a0aa809c1af61679b48a77667e0
Reviewed-on: https://go-review.googlesource.com/120041
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/internal/modfetch/bitbucket/fetch.go b/vendor/cmd/go/internal/modfetch/bitbucket/fetch.go
index c077a3e..58e74a5 100644
--- a/vendor/cmd/go/internal/modfetch/bitbucket/fetch.go
+++ b/vendor/cmd/go/internal/modfetch/bitbucket/fetch.go
@@ -12,11 +12,12 @@
 	"cmd/go/internal/modfetch/gitrepo"
 )
 
-func Lookup(path string) (codehost.Repo, error) {
+func Lookup(path string) (codehost.Repo, string, error) {
 	f := strings.Split(path, "/")
 	if len(f) < 3 || f[0] != "bitbucket.org" {
-		return nil, fmt.Errorf("bitbucket repo must be bitbucket.org/org/project")
+		return nil, "", fmt.Errorf("bitbucket repo must be bitbucket.org/org/project")
 	}
 	path = f[0] + "/" + f[1] + "/" + f[2]
-	return gitrepo.Repo("https://"+path, path)
+	repo, err := gitrepo.Repo("https://" + path)
+	return repo, path, err
 }
diff --git a/vendor/cmd/go/internal/modfetch/codehost/codehost.go b/vendor/cmd/go/internal/modfetch/codehost/codehost.go
index 177be3a..85c0fe7 100644
--- a/vendor/cmd/go/internal/modfetch/codehost/codehost.go
+++ b/vendor/cmd/go/internal/modfetch/codehost/codehost.go
@@ -16,6 +16,7 @@
 	"os/exec"
 	"path/filepath"
 	"strings"
+	"sync"
 	"time"
 
 	"cmd/go/internal/cfg"
@@ -34,9 +35,6 @@
 // remote version control servers, and code hosting sites.
 // A Repo must be safe for simultaneous use by multiple goroutines.
 type Repo interface {
-	// Root returns the import path of the root directory of the repository.
-	Root() string
-
 	// List lists all tags with the given prefix.
 	Tags(prefix string) (tags []string, err error)
 
@@ -161,19 +159,36 @@
 	return text
 }
 
+var dirLock sync.Map
+
 // Run runs the command line in the given directory
 // (an empty dir means the current directory).
 // It returns the standard output and, for a non-zero exit,
 // a *RunError indicating the command, exit status, and standard error.
 // Standard error is unavailable for commands that exit successfully.
 func Run(dir string, cmdline ...interface{}) ([]byte, error) {
+	if dir != "" {
+		muIface, ok := dirLock.Load(dir)
+		if !ok {
+			muIface, _ = dirLock.LoadOrStore(dir, new(sync.Mutex))
+		}
+		mu := muIface.(*sync.Mutex)
+		mu.Lock()
+		defer mu.Unlock()
+	}
+
 	cmd := str.StringList(cmdline...)
 	if cfg.BuildX {
-		var cd string
+		var text string
 		if dir != "" {
-			cd = "cd " + dir + "; "
+			text = "cd " + dir + "; "
 		}
-		fmt.Fprintf(os.Stderr, "%s%s\n", cd, strings.Join(cmd, " "))
+		text += strings.Join(cmd, " ")
+		fmt.Fprintf(os.Stderr, "%s\n", text)
+		start := time.Now()
+		defer func() {
+			fmt.Fprintf(os.Stderr, "%.3fs # %s\n", time.Since(start).Seconds(), text)
+		}()
 	}
 	// TODO: Impose limits on command output size.
 	// TODO: Set environment to get English error messages.
diff --git a/vendor/cmd/go/internal/modfetch/coderepo.go b/vendor/cmd/go/internal/modfetch/coderepo.go
index ab2a052..17c8b92 100644
--- a/vendor/cmd/go/internal/modfetch/coderepo.go
+++ b/vendor/cmd/go/internal/modfetch/coderepo.go
@@ -38,10 +38,9 @@
 	pseudoMajor string
 }
 
-func newCodeRepo(code codehost.Repo, path string) (Repo, error) {
-	codeRoot := code.Root()
-	if !hasPathPrefix(path, codeRoot) {
-		return nil, fmt.Errorf("mismatched repo: found %s for %s", codeRoot, path)
+func newCodeRepo(code codehost.Repo, root, path string) (Repo, error) {
+	if !hasPathPrefix(path, root) {
+		return nil, fmt.Errorf("mismatched repo: found %s for %s", root, path)
 	}
 	pathPrefix, pathMajor, ok := module.SplitPathVersion(path)
 	if !ok {
@@ -61,7 +60,7 @@
 	//
 	// Compute codeDir = bar, the subdirectory within the repo
 	// corresponding to the module root.
-	codeDir := strings.Trim(strings.TrimPrefix(pathPrefix, codeRoot), "/")
+	codeDir := strings.Trim(strings.TrimPrefix(pathPrefix, root), "/")
 	if strings.HasPrefix(path, "gopkg.in/") {
 		// But gopkg.in is a special legacy case, in which pathPrefix does not start with codeRoot.
 		// For example we might have:
@@ -77,7 +76,7 @@
 	r := &codeRepo{
 		modPath:     path,
 		code:        code,
-		codeRoot:    codeRoot,
+		codeRoot:    root,
 		codeDir:     codeDir,
 		pathPrefix:  pathPrefix,
 		pathMajor:   pathMajor,
diff --git a/vendor/cmd/go/internal/modfetch/coderepo_test.go b/vendor/cmd/go/internal/modfetch/coderepo_test.go
index 4a622e3..c8233a5 100644
--- a/vendor/cmd/go/internal/modfetch/coderepo_test.go
+++ b/vendor/cmd/go/internal/modfetch/coderepo_test.go
@@ -19,10 +19,6 @@
 	"cmd/go/internal/modfetch/codehost"
 )
 
-func init() {
-	isTest = true
-}
-
 func TestMain(m *testing.M) {
 	os.Exit(testMain(m))
 }
@@ -90,7 +86,7 @@
 	},
 	{
 		path:    "github.com/rsc/vgotest1",
-		rev:     "80d85",
+		rev:     "80d85c5",
 		version: "v0.0.0-20180219231006-80d85c5d4d17",
 		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
 		short:   "80d85c5d4d17",
@@ -116,7 +112,7 @@
 	},
 	{
 		path:     "github.com/rsc/vgotest1/v2",
-		rev:      "80d85",
+		rev:      "80d85c5",
 		version:  "v2.0.0-20180219231006-80d85c5d4d17",
 		name:     "80d85c5d4d17598a0e9055e7c175a32b415d6128",
 		short:    "80d85c5d4d17",
@@ -126,7 +122,7 @@
 	},
 	{
 		path:    "github.com/rsc/vgotest1/v54321",
-		rev:     "80d85",
+		rev:     "80d85c5",
 		version: "v54321.0.0-20180219231006-80d85c5d4d17",
 		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
 		short:   "80d85c5d4d17",
@@ -136,12 +132,12 @@
 	{
 		path: "github.com/rsc/vgotest1/submod",
 		rev:  "v1.0.0",
-		err:  "unknown revision \"submod/v1.0.0\"",
+		err:  "unknown revision submod/v1.0.0",
 	},
 	{
 		path: "github.com/rsc/vgotest1/submod",
 		rev:  "v1.0.3",
-		err:  "unknown revision \"submod/v1.0.3\"",
+		err:  "unknown revision submod/v1.0.3",
 	},
 	{
 		path:    "github.com/rsc/vgotest1/submod",
@@ -209,33 +205,6 @@
 		gomod:   "module \"github.com/rsc/vgotest1/v2\" // v2/go.mod\n",
 	},
 	{
-		path:    "go.googlesource.com/scratch",
-		rev:     "0f302529858",
-		version: "v0.0.0-20180220024720-0f3025298580",
-		name:    "0f30252985809011f026b5a2d5cf456e021623da",
-		short:   "0f3025298580",
-		time:    time.Date(2018, 2, 20, 2, 47, 20, 0, time.UTC),
-		gomod:   "//vgo 0.0.4\n\nmodule go.googlesource.com/scratch\n",
-	},
-	{
-		path:    "go.googlesource.com/scratch/rsc",
-		rev:     "0f302529858",
-		version: "v0.0.0-20180220024720-0f3025298580",
-		name:    "0f30252985809011f026b5a2d5cf456e021623da",
-		short:   "0f3025298580",
-		time:    time.Date(2018, 2, 20, 2, 47, 20, 0, time.UTC),
-		gomod:   "",
-	},
-	{
-		path:     "go.googlesource.com/scratch/cbro",
-		rev:      "0f302529858",
-		version:  "v0.0.0-20180220024720-0f3025298580",
-		name:     "0f30252985809011f026b5a2d5cf456e021623da",
-		short:    "0f3025298580",
-		time:     time.Date(2018, 2, 20, 2, 47, 20, 0, time.UTC),
-		gomoderr: "missing go.mod",
-	},
-	{
 		// redirect to github
 		path:    "rsc.io/quote",
 		rev:     "v1.0.0",
@@ -610,12 +579,10 @@
 
 // fixedTagsRepo is a fake codehost.Repo that returns a fixed list of tags
 type fixedTagsRepo struct {
-	root string
 	tags []string
 }
 
 func (ch *fixedTagsRepo) Tags(string) ([]string, error)                  { return ch.tags, nil }
-func (ch *fixedTagsRepo) Root() string                                   { return ch.root }
 func (ch *fixedTagsRepo) Latest() (*codehost.RevInfo, error)             { panic("not impl") }
 func (ch *fixedTagsRepo) ReadFile(string, string, int64) ([]byte, error) { panic("not impl") }
 func (ch *fixedTagsRepo) ReadZip(string, string, int64) (io.ReadCloser, string, error) {
@@ -626,7 +593,6 @@
 func TestNonCanonicalSemver(t *testing.T) {
 	root := "golang.org/x/issue24476"
 	ch := &fixedTagsRepo{
-		root: root,
 		tags: []string{
 			"", "huh?", "1.0.1",
 			// what about "version 1 dot dogcow"?
@@ -637,7 +603,7 @@
 		},
 	}
 
-	cr, err := newCodeRepo(ch, root)
+	cr, err := newCodeRepo(ch, root, root)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/vendor/cmd/go/internal/modfetch/domain.go b/vendor/cmd/go/internal/modfetch/domain.go
index 2494f80..c1e6813 100644
--- a/vendor/cmd/go/internal/modfetch/domain.go
+++ b/vendor/cmd/go/internal/modfetch/domain.go
@@ -76,11 +76,11 @@
 				return nil, fmt.Errorf("invalid server URL %q: must be HTTPS", imp.RepoRoot)
 			}
 			if imp.VCS == "git" {
-				code, err := gitrepo.Repo(imp.RepoRoot, imp.Prefix)
+				code, err := gitrepo.Repo(imp.RepoRoot)
 				if err != nil {
 					return nil, err
 				}
-				return newCodeRepo(code, path)
+				return newCodeRepo(code, imp.Prefix, path)
 			}
 			return nil, fmt.Errorf("unknown VCS, Repo: %s, %s", imp.VCS, imp.RepoRoot)
 		}
diff --git a/vendor/cmd/go/internal/modfetch/github/fetch.go b/vendor/cmd/go/internal/modfetch/github/fetch.go
index a2a90f1..bf5c720 100644
--- a/vendor/cmd/go/internal/modfetch/github/fetch.go
+++ b/vendor/cmd/go/internal/modfetch/github/fetch.go
@@ -14,11 +14,12 @@
 
 // Lookup returns the code repository enclosing the given module path,
 // which must begin with github.com/.
-func Lookup(path string) (codehost.Repo, error) {
+func Lookup(path string) (codehost.Repo, string, error) {
 	f := strings.Split(path, "/")
 	if len(f) < 3 || f[0] != "github.com" {
-		return nil, fmt.Errorf("github repo must be github.com/org/project")
+		return nil, "", fmt.Errorf("github repo must be github.com/org/project")
 	}
 	path = f[0] + "/" + f[1] + "/" + f[2]
-	return gitrepo.Repo("https://"+path, path)
+	repo, err := gitrepo.Repo("https://" + path)
+	return repo, path, err
 }
diff --git a/vendor/cmd/go/internal/modfetch/gitrepo/fetch.go b/vendor/cmd/go/internal/modfetch/gitrepo/fetch.go
index e078189..a2f58c0 100644
--- a/vendor/cmd/go/internal/modfetch/gitrepo/fetch.go
+++ b/vendor/cmd/go/internal/modfetch/gitrepo/fetch.go
@@ -6,7 +6,6 @@
 package gitrepo
 
 import (
-	"archive/zip"
 	"bytes"
 	"fmt"
 	"io"
@@ -24,26 +23,23 @@
 )
 
 // Repo returns the code repository at the given Git remote reference.
-// The returned repo reports the given root as its module root.
-func Repo(remote, root string) (codehost.Repo, error) {
-	return newRepo(remote, root, false)
+func Repo(remote string) (codehost.Repo, error) {
+	return newRepoCached(remote, false)
 }
 
 // LocalRepo is like Repo but accepts both Git remote references
 // and paths to repositories on the local file system.
-// The returned repo reports the given root as its module root.
-func LocalRepo(remote, root string) (codehost.Repo, error) {
-	return newRepo(remote, root, true)
+func LocalRepo(remote string) (codehost.Repo, error) {
+	return newRepoCached(remote, true)
 }
 
 const workDirType = "git2"
 
 var repoCache par.Cache
 
-func newRepoCached(remote, root string, localOK bool) (codehost.Repo, error) {
+func newRepoCached(remote string, localOK bool) (codehost.Repo, error) {
 	type key struct {
 		remote  string
-		root    string
 		localOK bool
 	}
 	type cached struct {
@@ -51,16 +47,16 @@
 		err  error
 	}
 
-	c := repoCache.Do(key{remote, root, localOK}, func() interface{} {
-		repo, err := newRepo(remote, root, localOK)
+	c := repoCache.Do(key{remote, localOK}, func() interface{} {
+		repo, err := newRepo(remote, localOK)
 		return cached{repo, err}
 	}).(cached)
 
 	return c.repo, c.err
 }
 
-func newRepo(remote, root string, localOK bool) (codehost.Repo, error) {
-	r := &repo{remote: remote, root: root, canArchive: true}
+func newRepo(remote string, localOK bool) (codehost.Repo, error) {
+	r := &repo{remote: remote}
 	if strings.Contains(remote, "://") {
 		// This is a remote path.
 		dir, err := codehost.WorkDir(workDirType, r.remote)
@@ -110,11 +106,12 @@
 type repo struct {
 	remote string
 	local  bool
-	root   string
 	dir    string
 
-	mu         sync.Mutex // protects canArchive, some git repo state
-	canArchive bool
+	mu         sync.Mutex // protects fetchLevel, some git repo state
+	fetchLevel int
+
+	statCache par.Cache
 
 	refsOnce sync.Once
 	refs     map[string]string
@@ -124,9 +121,12 @@
 	localTags     map[string]bool
 }
 
-func (r *repo) Root() string {
-	return r.root
-}
+const (
+	// How much have we fetched into the git repo (in this process)?
+	fetchNone = iota // nothing yet
+	fetchSome        // shallow fetches of individual hashes
+	fetchAll         // "fetch -t origin": get all remote branches and tags
+)
 
 // loadLocalTags loads tag references from the local git cache
 // into the map r.localTags.
@@ -231,229 +231,181 @@
 	return []string{}
 }
 
-// statOrArchive tries to stat the given rev in the local repository,
-// or else it tries to obtain an archive at the rev with the given arguments,
-// or else it falls back to aggressive fetching and then a local stat.
-// The archive step is an optimization for servers that support it
-// (most do not, but maybe that will change), to let us minimize
-// the amount of code downloaded.
-func (r *repo) statOrArchive(rev string, archiveArgs ...string) (info *codehost.RevInfo, archive []byte, err error) {
-	var (
-		hash      string
-		tag       string
-		out       []byte
-		triedHash string
-	)
+// minHashDigits is the minimum number of digits to require
+// before accepting a hex digit sequence as potentially identifying
+// a specific commit in a git repo. (Of course, users can always
+// specify more digits, and many will paste in all 40 digits,
+// but many of git's commands default to printing short hashes
+// as 7 digits.)
+const minHashDigits = 7
 
-	// Maybe rev is a hash we already have locally.
-	if len(rev) >= 12 && codehost.AllHex(rev) {
-		out, err = codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H", rev)
-		if err == nil {
-			hash = strings.TrimSpace(string(out))
-			goto Found
+// stat stats the given rev in the local repository,
+// or else it fetches more info from the remote repository and tries again.
+func (r *repo) stat(rev string) (*codehost.RevInfo, error) {
+	if r.local {
+		return r.statLocal(rev, rev)
+	}
+
+	// Fast path: maybe rev is a hash we already have locally.
+	if len(rev) >= minHashDigits && len(rev) <= 40 && codehost.AllHex(rev) {
+		if info, err := r.statLocal(rev, rev); err == nil {
+			return info, nil
 		}
-		triedHash = rev
 	}
 
 	// Maybe rev is a tag we already have locally.
+	// (Note that we're excluding branches, which can be stale.)
 	r.localTagsOnce.Do(r.loadLocalTags)
-	if r.localTags["refs/tags/"+rev] {
-		out, err = codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H", "refs/tags/"+rev)
-		if err == nil {
-			hash = strings.TrimSpace(string(out))
-			goto Found
-		}
+	if r.localTags[rev] {
+		return r.statLocal(rev, "refs/tags/"+rev)
 	}
 
-	// Maybe rev is a ref from the server.
+	// Maybe rev is the name of a tag or branch on the remote server.
+	// Or maybe it's the prefix of a hash of a named ref.
+	// Try to resolve to both a ref (git name) and full (40-hex-digit) commit hash.
 	r.refsOnce.Do(r.loadRefs)
-	if k := "refs/tags/" + rev; r.refs[k] != "" {
-		hash = r.refs[k]
-		tag = k
-	} else if k := "refs/heads/" + rev; r.refs[k] != "" {
-		hash = r.refs[k]
-		rev = hash
+	var ref, hash string
+	if r.refs["refs/tags/"+rev] != "" {
+		ref = "refs/tags/" + rev
+		hash = r.refs[ref]
+		// Keep rev as is: tags are assumed not to change meaning.
+	} else if r.refs["refs/heads/"+rev] != "" {
+		ref = "refs/heads/" + rev
+		hash = r.refs[ref]
+		rev = hash // Replace rev, because meaning of refs/heads/foo can change.
 	} else if rev == "HEAD" && r.refs["HEAD"] != "" {
-		hash = r.refs["HEAD"]
-		rev = hash
-	} else if len(rev) >= 5 && len(rev) <= 40 && codehost.AllHex(rev) {
-		hash = rev
-	} else {
-		return nil, nil, fmt.Errorf("unknown revision %q", rev)
-	}
-
-	if hash != triedHash {
-		out, err = codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H", hash)
-		if err == nil {
-			hash = strings.TrimSpace(string(out))
-			goto Found
+		ref = "HEAD"
+		hash = r.refs[ref]
+		rev = hash // Replace rev, because meaning of HEAD can change.
+	} else if len(rev) >= minHashDigits && len(rev) <= 40 && codehost.AllHex(rev) {
+		// At the least, we have a hash prefix we can look up after the fetch below.
+		// Maybe we can map it to a full hash using the known refs.
+		prefix := rev
+		// Check whether rev is prefix of known ref hash.
+		for k, h := range r.refs {
+			if strings.HasPrefix(h, prefix) {
+				if hash != "" && hash != h {
+					// Hash is an ambiguous hash prefix.
+					// More information will not change that.
+					return nil, fmt.Errorf("ambiguous revision %s", rev)
+				}
+				if ref == "" || ref > k { // Break ties deterministically when multiple refs point at same hash.
+					ref = k
+				}
+				rev = h
+				hash = h
+			}
 		}
+		if hash == "" && len(rev) == 40 { // Didn't find a ref, but rev is a full hash.
+			hash = rev
+		}
+	} else {
+		return nil, fmt.Errorf("unknown revision %s", rev)
 	}
 
-	// We don't have the rev. Can we fetch it?
-	if r.local {
-		return nil, nil, fmt.Errorf("unknown revision %q", rev)
-	}
-
-	// Protect r.canArchive.
-	//
-	// Also protects against multiple goroutines running through the
-	// "progressively fetch more and more" sequence at the same time.
+	// Protect r.fetchLevel and the "fetch more and more" sequence.
 	// TODO(rsc): Add codehost.LockDir and use it for protecting that
-	// sequence, so that multiple processes don't collide.
+	// sequence, so that multiple processes don't collide in their
+	// git commands.
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
-	if r.canArchive {
-		// git archive with --remote requires a ref, not a hash.
-		// Proceed only if we know a ref for this hash.
-		if ref, ok := r.findRef(hash); ok {
-			out, err := codehost.Run(r.dir, "git", "archive", "--format=zip", "--remote="+r.remote, "--prefix=prefix/", ref, archiveArgs)
-			if err == nil {
-				return &codehost.RevInfo{Version: rev}, out, nil
-			}
-			if bytes.Contains(err.(*codehost.RunError).Stderr, []byte("did not match any files")) {
-				return nil, nil, os.ErrNotExist
-			}
-			if bytes.Contains(err.(*codehost.RunError).Stderr, []byte("Operation not supported by protocol")) {
-				r.canArchive = false
-			}
-		}
-	}
-
-	// Maybe it's a prefix of a ref we know.
-	// Iterating through all the refs is faster than doing unnecessary fetches.
-	// This is not strictly correct, in that the short ref might be ambiguous
-	// in the git repo as a whole, but not ambiguous in the list of named refs,
-	// so that we will resolve it where the git server would not.
-	// But this check avoids great expense, and preferring a known ref does
-	// not seem like such a bad failure mode.
-	if len(hash) >= 5 && len(hash) < 40 {
-		var full string
-		for _, h := range r.refs {
-			if strings.HasPrefix(h, hash) {
-				if full != "" {
-					// Prefix is ambiguous even in the ref list!
-					full = ""
-					break
-				}
-				full = h
-			}
-		}
-		if full != "" {
-			hash = full
-		}
-	}
-
-	// Fetch it.
-	if len(hash) == 40 {
-		name := hash
-		if ref, ok := r.findRef(hash); ok {
-			name = ref
-		}
-		if tag != "" {
-			_, err = codehost.Run(r.dir, "git", "fetch", "--depth=1", r.remote, tag+":"+tag)
+	// If we know a specific commit we need, fetch it.
+	if r.fetchLevel <= fetchSome && hash != "" {
+		r.fetchLevel = fetchSome
+		var refspec string
+		if ref != "" {
+			// If we do know the ref name, save the mapping locally
+			// so that (if it is a tag) it can show up in localTags
+			// on a future call. Also, some servers refuse to allow
+			// full hashes in ref specs, so prefer a ref name if known.
+			refspec = ref + ":" + ref
 		} else {
-			_, err = codehost.Run(r.dir, "git", "fetch", "--depth=1", r.remote, name)
+			ref = hash
+			refspec = hash
 		}
+		_, err := codehost.Run(r.dir, "git", "fetch", "-f", "--depth=1", r.remote, refspec)
 		if err == nil {
-			goto Found
+			return r.statLocal(rev, ref)
 		}
 		if !strings.Contains(err.Error(), "unadvertised object") && !strings.Contains(err.Error(), "no such remote ref") && !strings.Contains(err.Error(), "does not support shallow") {
-			return nil, nil, err
+			return nil, err
 		}
 	}
 
-	// It's a prefix, and we don't have a way to make the server resolve the prefix for us,
-	// or it's a full hash but also an unadvertised object.
-	// Download progressively more of the repo to look for it.
+	// Last resort.
+	// Fetch all heads and tags and hope the hash we want is in the history.
+	if r.fetchLevel < fetchAll {
+		r.fetchLevel = fetchAll
 
-	// Fetch the main branch (non-shallow).
-	if _, err := codehost.Run(r.dir, "git", "fetch", unshallow(r.dir), r.remote); err != nil {
-		return nil, nil, err
-	}
-	if out, err := codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H", hash); err == nil {
-		hash = strings.TrimSpace(string(out))
-		goto Found
+		// To work around a protocol version 2 bug that breaks --unshallow,
+		// add -c protocol.version=0.
+		// TODO(rsc): The bug is believed to be server-side, meaning only
+		// on Google's Git servers. Once the servers are fixed, drop the
+		// protocol.version=0. See Google-internal bug b/110495752.
+		var protoFlag []string
+		unshallowFlag := unshallow(r.dir)
+		if len(unshallowFlag) > 0 {
+			protoFlag = []string{"-c", "protocol.version=0"}
+		}
+		if _, err := codehost.Run(r.dir, "git", protoFlag, "fetch", unshallowFlag, "-f", "-t", r.remote, "refs/heads/*:refs/heads/*"); err != nil {
+			return nil, err
+		}
 	}
 
-	// Fetch all tags (non-shallow).
-	if _, err := codehost.Run(r.dir, "git", "fetch", unshallow(r.dir), "-f", "--tags", r.remote); err != nil {
-		return nil, nil, err
-	}
-	if out, err := codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H", hash); err == nil {
-		hash = strings.TrimSpace(string(out))
-		goto Found
-	}
+	return r.statLocal(rev, rev)
+}
 
-	// Fetch all branches (non-shallow).
-	if _, err := codehost.Run(r.dir, "git", "fetch", unshallow(r.dir), "-f", r.remote, "refs/heads/*:refs/heads/*"); err != nil {
-		return nil, nil, err
-	}
-	if out, err := codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H", hash); err == nil {
-		hash = strings.TrimSpace(string(out))
-		goto Found
-	}
-
-	// Fetch all refs (non-shallow).
-	if _, err := codehost.Run(r.dir, "git", "fetch", unshallow(r.dir), "-f", r.remote, "refs/*:refs/*"); err != nil {
-		return nil, nil, err
-	}
-	if out, err := codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H", hash); err == nil {
-		hash = strings.TrimSpace(string(out))
-		goto Found
-	}
-	return nil, nil, fmt.Errorf("cannot find hash %s", hash)
-Found:
-
-	if strings.HasPrefix(hash, rev) {
-		rev = hash
-	}
-
-	out, err = codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%ct", hash)
+// statLocal returns a codehost.RevInfo describing rev in the local git repository.
+// It uses version as info.Version.
+func (r *repo) statLocal(version, rev string) (*codehost.RevInfo, error) {
+	out, err := codehost.Run(r.dir, "git", "log", "-n1", "--format=format:%H %ct", rev)
 	if err != nil {
-		return nil, nil, err
+		if codehost.AllHex(rev) {
+			return nil, fmt.Errorf("unknown hash %s", rev)
+		}
+		return nil, fmt.Errorf("unknown revision %s", rev)
 	}
-	t, err := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64)
+	f := strings.Fields(string(out))
+	if len(f) != 2 {
+		return nil, fmt.Errorf("unexpected response from git log: %q", out)
+	}
+	hash := f[0]
+	if strings.HasPrefix(hash, version) {
+		version = hash // extend to full hash
+	}
+	t, err := strconv.ParseInt(f[1], 10, 64)
 	if err != nil {
-		return nil, nil, fmt.Errorf("invalid time from git log: %q", out)
+		return nil, fmt.Errorf("invalid time from git log: %q", out)
 	}
 
-	info = &codehost.RevInfo{
+	info := &codehost.RevInfo{
 		Name:    hash,
 		Short:   codehost.ShortenSHA1(hash),
 		Time:    time.Unix(t, 0).UTC(),
-		Version: rev,
-	}
-	return info, nil, nil
-}
-
-func (r *repo) Stat(rev string) (*codehost.RevInfo, error) {
-	// If the server will give us a git archive, we can pull the
-	// commit ID and the commit time out of the archive.
-	// We want an archive as small as possible (for speed),
-	// but we have to specify a pattern that matches at least one file name.
-	// The pattern here matches README, .gitignore, .gitattributes,
-	// and go.mod (and some other incidental file names);
-	// hopefully most repos will have at least one of these.
-	info, archive, err := r.statOrArchive(rev, "[Rg.][Ego][A.i][Dmt][Miao][Edgt]*")
-	if err != nil {
-		return nil, err
-	}
-	if archive != nil {
-		return zip2info(archive, info.Version)
+		Version: version,
 	}
 	return info, nil
 }
 
+func (r *repo) Stat(rev string) (*codehost.RevInfo, error) {
+	type cached struct {
+		info *codehost.RevInfo
+		err  error
+	}
+	c := r.statCache.Do(rev, func() interface{} {
+		info, err := r.stat(rev)
+		return cached{info, err}
+	}).(cached)
+	return c.info, c.err
+}
+
 func (r *repo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
-	info, archive, err := r.statOrArchive(rev, file)
+	// TODO: Could use git cat-file --batch.
+	info, err := r.Stat(rev) // download rev into local git repo
 	if err != nil {
 		return nil, err
 	}
-	if archive != nil {
-		return zip2file(archive, file, maxSize)
-	}
 	out, err := codehost.Run(r.dir, "git", "cat-file", "blob", info.Name+":"+file)
 	if err != nil {
 		return nil, os.ErrNotExist
@@ -467,70 +419,17 @@
 	if subdir != "" {
 		args = append(args, "--", subdir)
 	}
-	info, archive, err := r.statOrArchive(rev, args...)
+	info, err := r.Stat(rev) // download rev into local git repo
 	if err != nil {
 		return nil, "", err
 	}
-	if archive == nil {
-		archive, err = codehost.Run(r.dir, "git", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
-		if err != nil {
-			if bytes.Contains(err.(*codehost.RunError).Stderr, []byte("did not match any files")) {
-				return nil, "", os.ErrNotExist
-			}
-			return nil, "", err
+	archive, err := codehost.Run(r.dir, "git", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
+	if err != nil {
+		if bytes.Contains(err.(*codehost.RunError).Stderr, []byte("did not match any files")) {
+			return nil, "", os.ErrNotExist
 		}
+		return nil, "", err
 	}
 
 	return ioutil.NopCloser(bytes.NewReader(archive)), "", nil
 }
-
-func zip2info(archive []byte, rev string) (*codehost.RevInfo, error) {
-	r, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
-	if err != nil {
-		return nil, err
-	}
-	if r.Comment == "" {
-		return nil, fmt.Errorf("missing commit ID in git zip comment")
-	}
-	hash := r.Comment
-	if len(hash) != 40 || !codehost.AllHex(hash) {
-		return nil, fmt.Errorf("invalid commit ID in git zip comment")
-	}
-	if len(r.File) == 0 {
-		return nil, fmt.Errorf("git zip has no files")
-	}
-	info := &codehost.RevInfo{
-		Name:    hash,
-		Short:   codehost.ShortenSHA1(hash),
-		Time:    r.File[0].Modified.UTC(),
-		Version: rev,
-	}
-	return info, nil
-}
-
-func zip2file(archive []byte, file string, maxSize int64) ([]byte, error) {
-	r, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
-	if err != nil {
-		return nil, err
-	}
-	for _, f := range r.File {
-		if f.Name != "prefix/"+file {
-			continue
-		}
-		rc, err := f.Open()
-		if err != nil {
-			return nil, err
-		}
-		defer rc.Close()
-		l := &io.LimitedReader{R: rc, N: maxSize + 1}
-		data, err := ioutil.ReadAll(l)
-		if err != nil {
-			return nil, err
-		}
-		if l.N <= 0 {
-			return nil, fmt.Errorf("file %s too large", file)
-		}
-		return data, nil
-	}
-	return nil, fmt.Errorf("incomplete git zip archive: cannot find %s", file)
-}
diff --git a/vendor/cmd/go/internal/modfetch/gitrepo/fetch_test.go b/vendor/cmd/go/internal/modfetch/gitrepo/fetch_test.go
index b2d2524..99af883 100644
--- a/vendor/cmd/go/internal/modfetch/gitrepo/fetch_test.go
+++ b/vendor/cmd/go/internal/modfetch/gitrepo/fetch_test.go
@@ -66,9 +66,7 @@
 	if remote == "localGitRepo" {
 		remote = "file://" + filepath.ToSlash(localGitRepo)
 	}
-	// Re ?root: nothing should care about the second argument,
-	// so use a string that will be distinctive if it does show up.
-	return LocalRepo(remote, "?root")
+	return LocalRepo(remote)
 }
 
 var tagsTests = []struct {
@@ -286,7 +284,7 @@
 		repo:   gitrepo1,
 		rev:    "aaaaaaaaab",
 		subdir: "",
-		err:    "cannot find hash",
+		err:    "unknown hash",
 	},
 	{
 		repo:   "https://github.com/rsc/vgotest1",
@@ -460,7 +458,7 @@
 	{
 		repo: gitrepo1,
 		rev:  "aaaaaaaaab",
-		err:  "cannot find hash",
+		err:  "unknown hash",
 	},
 }
 
diff --git a/vendor/cmd/go/internal/modfetch/googlesource/fetch.go b/vendor/cmd/go/internal/modfetch/googlesource/fetch.go
index 8317ac3..a22a7bd 100644
--- a/vendor/cmd/go/internal/modfetch/googlesource/fetch.go
+++ b/vendor/cmd/go/internal/modfetch/googlesource/fetch.go
@@ -12,14 +12,15 @@
 	"cmd/go/internal/modfetch/gitrepo"
 )
 
-func Lookup(path string) (codehost.Repo, error) {
+func Lookup(path string) (codehost.Repo, string, error) {
 	i := strings.Index(path, "/")
 	if i+1 == len(path) || !strings.HasSuffix(path[:i+1], ".googlesource.com/") {
-		return nil, fmt.Errorf("not *.googlesource.com/*")
+		return nil, "", fmt.Errorf("not *.googlesource.com/*")
 	}
 	j := strings.Index(path[i+1:], "/")
 	if j >= 0 {
 		path = path[:i+1+j]
 	}
-	return gitrepo.Repo("https://"+path, path)
+	repo, err := gitrepo.Repo("https://" + path)
+	return repo, path, err
 }
diff --git a/vendor/cmd/go/internal/modfetch/gopkgin.go b/vendor/cmd/go/internal/modfetch/gopkgin.go
index d49b60b..f79edc0 100644
--- a/vendor/cmd/go/internal/modfetch/gopkgin.go
+++ b/vendor/cmd/go/internal/modfetch/gopkgin.go
@@ -13,10 +13,11 @@
 	"fmt"
 )
 
-func gopkginLookup(path string) (codehost.Repo, error) {
+func gopkginLookup(path string) (codehost.Repo, string, error) {
 	root, _, _, _, ok := modfile.ParseGopkgIn(path)
 	if !ok {
-		return nil, fmt.Errorf("invalid gopkg.in/ path: %q", path)
+		return nil, "", fmt.Errorf("invalid gopkg.in/ path: %q", path)
 	}
-	return gitrepo.Repo("https://"+root, root)
+	repo, err := gitrepo.Repo("https://" + root)
+	return repo, root, err
 }
diff --git a/vendor/cmd/go/internal/modfetch/repo.go b/vendor/cmd/go/internal/modfetch/repo.go
index 072bbbc..3542dcb 100644
--- a/vendor/cmd/go/internal/modfetch/repo.go
+++ b/vendor/cmd/go/internal/modfetch/repo.go
@@ -99,11 +99,11 @@
 	if proxyURL != "" {
 		return lookupProxy(path)
 	}
-	if code, err := lookupCodeHost(path, false); err != errNotHosted {
+	if code, root, err := lookupCodeHost(path, false); err != errNotHosted {
 		if err != nil {
 			return nil, err
 		}
-		return newCodeRepo(code, path)
+		return newCodeRepo(code, root, path)
 	}
 	return lookupCustomDomain(path)
 }
@@ -148,21 +148,18 @@
 
 var errNotHosted = errors.New("not hosted")
 
-var isTest bool
-
-func lookupCodeHost(path string, customDomain bool) (codehost.Repo, error) {
+func lookupCodeHost(path string, customDomain bool) (codehost.Repo, string, error) {
 	switch {
 	case strings.HasPrefix(path, "github.com/"):
 		return github.Lookup(path)
 	case strings.HasPrefix(path, "bitbucket.org/"):
 		return bitbucket.Lookup(path)
-	case customDomain && strings.HasSuffix(path[:strings.Index(path, "/")+1], ".googlesource.com/") ||
-		isTest && strings.HasPrefix(path, "go.googlesource.com/scratch"):
+	case customDomain && strings.HasSuffix(path[:strings.Index(path, "/")+1], ".googlesource.com/"):
 		return googlesource.Lookup(path)
 	case strings.HasPrefix(path, "gopkg.in/"):
 		return gopkginLookup(path)
 	}
-	return nil, errNotHosted
+	return nil, "", errNotHosted
 }
 
 func SortVersions(list []string) {