github.com/bir3/gocompiler@v0.3.205/src/cmd/gocmd/internal/modfetch/codehost/git.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package codehost
     6  
     7  import (
     8  	"bytes"
     9  	"crypto/sha256"
    10  	"encoding/base64"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"io/fs"
    15  	"net/url"
    16  	"os"
    17  	"os/exec"
    18  	"path/filepath"
    19  	"sort"
    20  	"strconv"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/lockedfile"
    26  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/par"
    27  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/web"
    28  
    29  	"github.com/bir3/gocompiler/src/xvendor/golang.org/x/mod/semver"
    30  )
    31  
    32  // LocalGitRepo is like Repo but accepts both Git remote references
    33  // and paths to repositories on the local file system.
    34  func LocalGitRepo(remote string) (Repo, error) {
    35  	return newGitRepoCached(remote, true)
    36  }
    37  
    38  // A notExistError wraps another error to retain its original text
    39  // but makes it opaquely equivalent to fs.ErrNotExist.
    40  type notExistError struct {
    41  	err error
    42  }
    43  
    44  func (e notExistError) Error() string   { return e.err.Error() }
    45  func (notExistError) Is(err error) bool { return err == fs.ErrNotExist }
    46  
    47  const gitWorkDirType = "git3"
    48  
    49  var gitRepoCache par.Cache
    50  
    51  func newGitRepoCached(remote string, localOK bool) (Repo, error) {
    52  	type key struct {
    53  		remote  string
    54  		localOK bool
    55  	}
    56  	type cached struct {
    57  		repo Repo
    58  		err  error
    59  	}
    60  
    61  	c := gitRepoCache.Do(key{remote, localOK}, func() any {
    62  		repo, err := newGitRepo(remote, localOK)
    63  		return cached{repo, err}
    64  	}).(cached)
    65  
    66  	return c.repo, c.err
    67  }
    68  
    69  func newGitRepo(remote string, localOK bool) (Repo, error) {
    70  	r := &gitRepo{remote: remote}
    71  	if strings.Contains(remote, "://") {
    72  		// This is a remote path.
    73  		var err error
    74  		r.dir, r.mu.Path, err = WorkDir(gitWorkDirType, r.remote)
    75  		if err != nil {
    76  			return nil, err
    77  		}
    78  
    79  		unlock, err := r.mu.Lock()
    80  		if err != nil {
    81  			return nil, err
    82  		}
    83  		defer unlock()
    84  
    85  		if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
    86  			if _, err := Run(r.dir, "git", "init", "--bare"); err != nil {
    87  				os.RemoveAll(r.dir)
    88  				return nil, err
    89  			}
    90  			// We could just say git fetch https://whatever later,
    91  			// but this lets us say git fetch origin instead, which
    92  			// is a little nicer. More importantly, using a named remote
    93  			// avoids a problem with Git LFS. See golang.org/issue/25605.
    94  			if _, err := Run(r.dir, "git", "remote", "add", "origin", "--", r.remote); err != nil {
    95  				os.RemoveAll(r.dir)
    96  				return nil, err
    97  			}
    98  		}
    99  		r.remoteURL = r.remote
   100  		r.remote = "origin"
   101  	} else {
   102  		// Local path.
   103  		// Disallow colon (not in ://) because sometimes
   104  		// that's rcp-style host:path syntax and sometimes it's not (c:\work).
   105  		// The go command has always insisted on URL syntax for ssh.
   106  		if strings.Contains(remote, ":") {
   107  			return nil, fmt.Errorf("git remote cannot use host:path syntax")
   108  		}
   109  		if !localOK {
   110  			return nil, fmt.Errorf("git remote must not be local directory")
   111  		}
   112  		r.local = true
   113  		info, err := os.Stat(remote)
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  		if !info.IsDir() {
   118  			return nil, fmt.Errorf("%s exists but is not a directory", remote)
   119  		}
   120  		r.dir = remote
   121  		r.mu.Path = r.dir + ".lock"
   122  	}
   123  	return r, nil
   124  }
   125  
   126  type gitRepo struct {
   127  	remote, remoteURL string
   128  	local             bool
   129  	dir               string
   130  
   131  	mu lockedfile.Mutex // protects fetchLevel and git repo state
   132  
   133  	fetchLevel int
   134  
   135  	statCache par.Cache
   136  
   137  	refsOnce sync.Once
   138  	// refs maps branch and tag refs (e.g., "HEAD", "refs/heads/master")
   139  	// to commits (e.g., "37ffd2e798afde829a34e8955b716ab730b2a6d6")
   140  	refs    map[string]string
   141  	refsErr error
   142  
   143  	localTagsOnce sync.Once
   144  	localTags     map[string]bool
   145  }
   146  
   147  const (
   148  	// How much have we fetched into the git repo (in this process)?
   149  	fetchNone = iota // nothing yet
   150  	fetchSome        // shallow fetches of individual hashes
   151  	fetchAll         // "fetch -t origin": get all remote branches and tags
   152  )
   153  
   154  // loadLocalTags loads tag references from the local git cache
   155  // into the map r.localTags.
   156  // Should only be called as r.localTagsOnce.Do(r.loadLocalTags).
   157  func (r *gitRepo) loadLocalTags() {
   158  	// The git protocol sends all known refs and ls-remote filters them on the client side,
   159  	// so we might as well record both heads and tags in one shot.
   160  	// Most of the time we only care about tags but sometimes we care about heads too.
   161  	out, err := Run(r.dir, "git", "tag", "-l")
   162  	if err != nil {
   163  		return
   164  	}
   165  
   166  	r.localTags = make(map[string]bool)
   167  	for _, line := range strings.Split(string(out), "\n") {
   168  		if line != "" {
   169  			r.localTags[line] = true
   170  		}
   171  	}
   172  }
   173  
   174  func (r *gitRepo) CheckReuse(old *Origin, subdir string) error {
   175  	if old == nil {
   176  		return fmt.Errorf("missing origin")
   177  	}
   178  	if old.VCS != "git" || old.URL != r.remoteURL {
   179  		return fmt.Errorf("origin moved from %v %q to %v %q", old.VCS, old.URL, "git", r.remoteURL)
   180  	}
   181  	if old.Subdir != subdir {
   182  		return fmt.Errorf("origin moved from %v %q %q to %v %q %q", old.VCS, old.URL, old.Subdir, "git", r.remoteURL, subdir)
   183  	}
   184  
   185  	// Note: Can have Hash with no Ref and no TagSum and no RepoSum,
   186  	// meaning the Hash simply has to remain in the repo.
   187  	// In that case we assume it does in the absence of any real way to check.
   188  	// But if neither Hash nor TagSum is present, we have nothing to check,
   189  	// which we take to mean we didn't record enough information to be sure.
   190  	if old.Hash == "" && old.TagSum == "" && old.RepoSum == "" {
   191  		return fmt.Errorf("non-specific origin")
   192  	}
   193  
   194  	r.loadRefs()
   195  	if r.refsErr != nil {
   196  		return r.refsErr
   197  	}
   198  
   199  	if old.Ref != "" {
   200  		hash, ok := r.refs[old.Ref]
   201  		if !ok {
   202  			return fmt.Errorf("ref %q deleted", old.Ref)
   203  		}
   204  		if hash != old.Hash {
   205  			return fmt.Errorf("ref %q moved from %s to %s", old.Ref, old.Hash, hash)
   206  		}
   207  	}
   208  	if old.TagSum != "" {
   209  		tags, err := r.Tags(old.TagPrefix)
   210  		if err != nil {
   211  			return err
   212  		}
   213  		if tags.Origin.TagSum != old.TagSum {
   214  			return fmt.Errorf("tags changed")
   215  		}
   216  	}
   217  	if old.RepoSum != "" {
   218  		if r.repoSum(r.refs) != old.RepoSum {
   219  			return fmt.Errorf("refs changed")
   220  		}
   221  	}
   222  	return nil
   223  }
   224  
   225  // loadRefs loads heads and tags references from the remote into the map r.refs.
   226  // The result is cached in memory.
   227  func (r *gitRepo) loadRefs() (map[string]string, error) {
   228  	r.refsOnce.Do(func() {
   229  		// The git protocol sends all known refs and ls-remote filters them on the client side,
   230  		// so we might as well record both heads and tags in one shot.
   231  		// Most of the time we only care about tags but sometimes we care about heads too.
   232  		out, gitErr := Run(r.dir, "git", "ls-remote", "-q", r.remote)
   233  		if gitErr != nil {
   234  			if rerr, ok := gitErr.(*RunError); ok {
   235  				if bytes.Contains(rerr.Stderr, []byte("fatal: could not read Username")) {
   236  					rerr.HelpText = "Confirm the import path was entered correctly.\nIf this is a private repository, see https://golang.org/doc/faq#git_https for additional information."
   237  				}
   238  			}
   239  
   240  			// If the remote URL doesn't exist at all, ideally we should treat the whole
   241  			// repository as nonexistent by wrapping the error in a notExistError.
   242  			// For HTTP and HTTPS, that's easy to detect: we'll try to fetch the URL
   243  			// ourselves and see what code it serves.
   244  			if u, err := url.Parse(r.remoteURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
   245  				if _, err := web.GetBytes(u); errors.Is(err, fs.ErrNotExist) {
   246  					gitErr = notExistError{gitErr}
   247  				}
   248  			}
   249  
   250  			r.refsErr = gitErr
   251  			return
   252  		}
   253  
   254  		refs := make(map[string]string)
   255  		for _, line := range strings.Split(string(out), "\n") {
   256  			f := strings.Fields(line)
   257  			if len(f) != 2 {
   258  				continue
   259  			}
   260  			if f[1] == "HEAD" || strings.HasPrefix(f[1], "refs/heads/") || strings.HasPrefix(f[1], "refs/tags/") {
   261  				refs[f[1]] = f[0]
   262  			}
   263  		}
   264  		for ref, hash := range refs {
   265  			if k, found := strings.CutSuffix(ref, "^{}"); found { // record unwrapped annotated tag as value of tag
   266  				refs[k] = hash
   267  				delete(refs, ref)
   268  			}
   269  		}
   270  		r.refs = refs
   271  	})
   272  	return r.refs, r.refsErr
   273  }
   274  
   275  func (r *gitRepo) Tags(prefix string) (*Tags, error) {
   276  	refs, err := r.loadRefs()
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	tags := &Tags{
   282  		Origin: &Origin{
   283  			VCS:       "git",
   284  			URL:       r.remoteURL,
   285  			TagPrefix: prefix,
   286  		},
   287  		List: []Tag{},
   288  	}
   289  	for ref, hash := range refs {
   290  		if !strings.HasPrefix(ref, "refs/tags/") {
   291  			continue
   292  		}
   293  		tag := ref[len("refs/tags/"):]
   294  		if !strings.HasPrefix(tag, prefix) {
   295  			continue
   296  		}
   297  		tags.List = append(tags.List, Tag{tag, hash})
   298  	}
   299  	sort.Slice(tags.List, func(i, j int) bool {
   300  		return tags.List[i].Name < tags.List[j].Name
   301  	})
   302  
   303  	dir := prefix[:strings.LastIndex(prefix, "/")+1]
   304  	h := sha256.New()
   305  	for _, tag := range tags.List {
   306  		if isOriginTag(strings.TrimPrefix(tag.Name, dir)) {
   307  			fmt.Fprintf(h, "%q %s\n", tag.Name, tag.Hash)
   308  		}
   309  	}
   310  	tags.Origin.TagSum = "t1:" + base64.StdEncoding.EncodeToString(h.Sum(nil))
   311  	return tags, nil
   312  }
   313  
   314  // repoSum returns a checksum of the entire repo state,
   315  // which can be checked (as Origin.RepoSum) to cache
   316  // the absence of a specific module version.
   317  // The caller must supply refs, the result of a successful r.loadRefs.
   318  func (r *gitRepo) repoSum(refs map[string]string) string {
   319  	var list []string
   320  	for ref := range refs {
   321  		list = append(list, ref)
   322  	}
   323  	sort.Strings(list)
   324  	h := sha256.New()
   325  	for _, ref := range list {
   326  		fmt.Fprintf(h, "%q %s\n", ref, refs[ref])
   327  	}
   328  	return "r1:" + base64.StdEncoding.EncodeToString(h.Sum(nil))
   329  }
   330  
   331  // unknownRevisionInfo returns a RevInfo containing an Origin containing a RepoSum of refs,
   332  // for use when returning an UnknownRevisionError.
   333  func (r *gitRepo) unknownRevisionInfo(refs map[string]string) *RevInfo {
   334  	return &RevInfo{
   335  		Origin: &Origin{
   336  			VCS:     "git",
   337  			URL:     r.remoteURL,
   338  			RepoSum: r.repoSum(refs),
   339  		},
   340  	}
   341  }
   342  
   343  func (r *gitRepo) Latest() (*RevInfo, error) {
   344  	refs, err := r.loadRefs()
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  	if refs["HEAD"] == "" {
   349  		return nil, ErrNoCommits
   350  	}
   351  	statInfo, err := r.Stat(refs["HEAD"])
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  
   356  	// Stat may return cached info, so make a copy to modify here.
   357  	info := new(RevInfo)
   358  	*info = *statInfo
   359  	info.Origin = new(Origin)
   360  	if statInfo.Origin != nil {
   361  		*info.Origin = *statInfo.Origin
   362  	}
   363  	info.Origin.Ref = "HEAD"
   364  	info.Origin.Hash = refs["HEAD"]
   365  
   366  	return info, nil
   367  }
   368  
   369  // findRef finds some ref name for the given hash,
   370  // for use when the server requires giving a ref instead of a hash.
   371  // There may be multiple ref names for a given hash,
   372  // in which case this returns some name - it doesn't matter which.
   373  func (r *gitRepo) findRef(hash string) (ref string, ok bool) {
   374  	refs, err := r.loadRefs()
   375  	if err != nil {
   376  		return "", false
   377  	}
   378  	for ref, h := range refs {
   379  		if h == hash {
   380  			return ref, true
   381  		}
   382  	}
   383  	return "", false
   384  }
   385  
   386  // minHashDigits is the minimum number of digits to require
   387  // before accepting a hex digit sequence as potentially identifying
   388  // a specific commit in a git repo. (Of course, users can always
   389  // specify more digits, and many will paste in all 40 digits,
   390  // but many of git's commands default to printing short hashes
   391  // as 7 digits.)
   392  const minHashDigits = 7
   393  
   394  // stat stats the given rev in the local repository,
   395  // or else it fetches more info from the remote repository and tries again.
   396  func (r *gitRepo) stat(rev string) (info *RevInfo, err error) {
   397  	if r.local {
   398  		return r.statLocal(rev, rev)
   399  	}
   400  
   401  	// Fast path: maybe rev is a hash we already have locally.
   402  	didStatLocal := false
   403  	if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) {
   404  		if info, err := r.statLocal(rev, rev); err == nil {
   405  			return info, nil
   406  		}
   407  		didStatLocal = true
   408  	}
   409  
   410  	// Maybe rev is a tag we already have locally.
   411  	// (Note that we're excluding branches, which can be stale.)
   412  	r.localTagsOnce.Do(r.loadLocalTags)
   413  	if r.localTags[rev] {
   414  		return r.statLocal(rev, "refs/tags/"+rev)
   415  	}
   416  
   417  	// Maybe rev is the name of a tag or branch on the remote server.
   418  	// Or maybe it's the prefix of a hash of a named ref.
   419  	// Try to resolve to both a ref (git name) and full (40-hex-digit) commit hash.
   420  	refs, err := r.loadRefs()
   421  	if err != nil {
   422  		return nil, err
   423  	}
   424  	// loadRefs may return an error if git fails, for example segfaults, or
   425  	// could not load a private repo, but defer checking to the else block
   426  	// below, in case we already have the rev in question in the local cache.
   427  	var ref, hash string
   428  	if refs["refs/tags/"+rev] != "" {
   429  		ref = "refs/tags/" + rev
   430  		hash = refs[ref]
   431  		// Keep rev as is: tags are assumed not to change meaning.
   432  	} else if refs["refs/heads/"+rev] != "" {
   433  		ref = "refs/heads/" + rev
   434  		hash = refs[ref]
   435  		rev = hash // Replace rev, because meaning of refs/heads/foo can change.
   436  	} else if rev == "HEAD" && refs["HEAD"] != "" {
   437  		ref = "HEAD"
   438  		hash = refs[ref]
   439  		rev = hash // Replace rev, because meaning of HEAD can change.
   440  	} else if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) {
   441  		// At the least, we have a hash prefix we can look up after the fetch below.
   442  		// Maybe we can map it to a full hash using the known refs.
   443  		prefix := rev
   444  		// Check whether rev is prefix of known ref hash.
   445  		for k, h := range refs {
   446  			if strings.HasPrefix(h, prefix) {
   447  				if hash != "" && hash != h {
   448  					// Hash is an ambiguous hash prefix.
   449  					// More information will not change that.
   450  					return nil, fmt.Errorf("ambiguous revision %s", rev)
   451  				}
   452  				if ref == "" || ref > k { // Break ties deterministically when multiple refs point at same hash.
   453  					ref = k
   454  				}
   455  				rev = h
   456  				hash = h
   457  			}
   458  		}
   459  		if hash == "" && len(rev) == 40 { // Didn't find a ref, but rev is a full hash.
   460  			hash = rev
   461  		}
   462  	} else {
   463  		return r.unknownRevisionInfo(refs), &UnknownRevisionError{Rev: rev}
   464  	}
   465  
   466  	defer func() {
   467  		if info != nil {
   468  			info.Origin.Hash = info.Name
   469  			// There's a ref = hash below; don't write that hash down as Origin.Ref.
   470  			if ref != info.Origin.Hash {
   471  				info.Origin.Ref = ref
   472  			}
   473  		}
   474  	}()
   475  
   476  	// Protect r.fetchLevel and the "fetch more and more" sequence.
   477  	unlock, err := r.mu.Lock()
   478  	if err != nil {
   479  		return nil, err
   480  	}
   481  	defer unlock()
   482  
   483  	// Perhaps r.localTags did not have the ref when we loaded local tags,
   484  	// but we've since done fetches that pulled down the hash we need
   485  	// (or already have the hash we need, just without its tag).
   486  	// Either way, try a local stat before falling back to network I/O.
   487  	if !didStatLocal {
   488  		if info, err := r.statLocal(rev, hash); err == nil {
   489  			if after, found := strings.CutPrefix(ref, "refs/tags/"); found {
   490  				// Make sure tag exists, so it will be in localTags next time the go command is run.
   491  				Run(r.dir, "git", "tag", after, hash)
   492  			}
   493  			return info, nil
   494  		}
   495  	}
   496  
   497  	// If we know a specific commit we need and its ref, fetch it.
   498  	// We do NOT fetch arbitrary hashes (when we don't know the ref)
   499  	// because we want to avoid ever importing a commit that isn't
   500  	// reachable from refs/tags/* or refs/heads/* or HEAD.
   501  	// Both Gerrit and GitHub expose every CL/PR as a named ref,
   502  	// and we don't want those commits masquerading as being real
   503  	// pseudo-versions in the main repo.
   504  	if r.fetchLevel <= fetchSome && ref != "" && hash != "" && !r.local {
   505  		r.fetchLevel = fetchSome
   506  		var refspec string
   507  		if ref != "" && ref != "HEAD" {
   508  			// If we do know the ref name, save the mapping locally
   509  			// so that (if it is a tag) it can show up in localTags
   510  			// on a future call. Also, some servers refuse to allow
   511  			// full hashes in ref specs, so prefer a ref name if known.
   512  			refspec = ref + ":" + ref
   513  		} else {
   514  			// Fetch the hash but give it a local name (refs/dummy),
   515  			// because that triggers the fetch behavior of creating any
   516  			// other known remote tags for the hash. We never use
   517  			// refs/dummy (it's not refs/tags/dummy) and it will be
   518  			// overwritten in the next command, and that's fine.
   519  			ref = hash
   520  			refspec = hash + ":refs/dummy"
   521  		}
   522  		_, err := Run(r.dir, "git", "fetch", "-f", "--depth=1", r.remote, refspec)
   523  		if err == nil {
   524  			return r.statLocal(rev, ref)
   525  		}
   526  		// Don't try to be smart about parsing the error.
   527  		// It's too complex and varies too much by git version.
   528  		// No matter what went wrong, fall back to a complete fetch.
   529  	}
   530  
   531  	// Last resort.
   532  	// Fetch all heads and tags and hope the hash we want is in the history.
   533  	if err := r.fetchRefsLocked(); err != nil {
   534  		return nil, err
   535  	}
   536  
   537  	return r.statLocal(rev, rev)
   538  }
   539  
   540  // fetchRefsLocked fetches all heads and tags from the origin, along with the
   541  // ancestors of those commits.
   542  //
   543  // We only fetch heads and tags, not arbitrary other commits: we don't want to
   544  // pull in off-branch commits (such as rejected GitHub pull requests) that the
   545  // server may be willing to provide. (See the comments within the stat method
   546  // for more detail.)
   547  //
   548  // fetchRefsLocked requires that r.mu remain locked for the duration of the call.
   549  func (r *gitRepo) fetchRefsLocked() error {
   550  	if r.fetchLevel < fetchAll {
   551  		// NOTE: To work around a bug affecting Git clients up to at least 2.23.0
   552  		// (2019-08-16), we must first expand the set of local refs, and only then
   553  		// unshallow the repository as a separate fetch operation. (See
   554  		// golang.org/issue/34266 and
   555  		// https://github.com/git/git/blob/4c86140027f4a0d2caaa3ab4bd8bfc5ce3c11c8a/transport.c#L1303-L1309.)
   556  
   557  		if _, err := Run(r.dir, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
   558  			return err
   559  		}
   560  
   561  		if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil {
   562  			if _, err := Run(r.dir, "git", "fetch", "--unshallow", "-f", r.remote); err != nil {
   563  				return err
   564  			}
   565  		}
   566  
   567  		r.fetchLevel = fetchAll
   568  	}
   569  	return nil
   570  }
   571  
   572  // statLocal returns a new RevInfo describing rev in the local git repository.
   573  // It uses version as info.Version.
   574  func (r *gitRepo) statLocal(version, rev string) (*RevInfo, error) {
   575  	out, err := Run(r.dir, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", rev, "--")
   576  	if err != nil {
   577  		// Return info with Origin.RepoSum if possible to allow caching of negative lookup.
   578  		var info *RevInfo
   579  		if refs, err := r.loadRefs(); err == nil {
   580  			info = r.unknownRevisionInfo(refs)
   581  		}
   582  		return info, &UnknownRevisionError{Rev: rev}
   583  	}
   584  	f := strings.Fields(string(out))
   585  	if len(f) < 2 {
   586  		return nil, fmt.Errorf("unexpected response from git log: %q", out)
   587  	}
   588  	hash := f[0]
   589  	if strings.HasPrefix(hash, version) {
   590  		version = hash // extend to full hash
   591  	}
   592  	t, err := strconv.ParseInt(f[1], 10, 64)
   593  	if err != nil {
   594  		return nil, fmt.Errorf("invalid time from git log: %q", out)
   595  	}
   596  
   597  	info := &RevInfo{
   598  		Origin: &Origin{
   599  			VCS:  "git",
   600  			URL:  r.remoteURL,
   601  			Hash: hash,
   602  		},
   603  		Name:    hash,
   604  		Short:   ShortenSHA1(hash),
   605  		Time:    time.Unix(t, 0).UTC(),
   606  		Version: hash,
   607  	}
   608  	if !strings.HasPrefix(hash, rev) {
   609  		info.Origin.Ref = rev
   610  	}
   611  
   612  	// Add tags. Output looks like:
   613  	//	ede458df7cd0fdca520df19a33158086a8a68e81 1523994202 HEAD -> master, tag: v1.2.4-annotated, tag: v1.2.3, origin/master, origin/HEAD
   614  	for i := 2; i < len(f); i++ {
   615  		if f[i] == "tag:" {
   616  			i++
   617  			if i < len(f) {
   618  				info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ","))
   619  			}
   620  		}
   621  	}
   622  	sort.Strings(info.Tags)
   623  
   624  	// Used hash as info.Version above.
   625  	// Use caller's suggested version if it appears in the tag list
   626  	// (filters out branch names, HEAD).
   627  	for _, tag := range info.Tags {
   628  		if version == tag {
   629  			info.Version = version
   630  		}
   631  	}
   632  
   633  	return info, nil
   634  }
   635  
   636  func (r *gitRepo) Stat(rev string) (*RevInfo, error) {
   637  	if rev == "latest" {
   638  		return r.Latest()
   639  	}
   640  	type cached struct {
   641  		info *RevInfo
   642  		err  error
   643  	}
   644  	c := r.statCache.Do(rev, func() any {
   645  		info, err := r.stat(rev)
   646  		return cached{info, err}
   647  	}).(cached)
   648  	return c.info, c.err
   649  }
   650  
   651  func (r *gitRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
   652  	// TODO: Could use git cat-file --batch.
   653  	info, err := r.Stat(rev) // download rev into local git repo
   654  	if err != nil {
   655  		return nil, err
   656  	}
   657  	out, err := Run(r.dir, "git", "cat-file", "blob", info.Name+":"+file)
   658  	if err != nil {
   659  		return nil, fs.ErrNotExist
   660  	}
   661  	return out, nil
   662  }
   663  
   664  func (r *gitRepo) RecentTag(rev, prefix string, allowed func(tag string) bool) (tag string, err error) {
   665  	info, err := r.Stat(rev)
   666  	if err != nil {
   667  		return "", err
   668  	}
   669  	rev = info.Name // expand hash prefixes
   670  
   671  	// describe sets tag and err using 'git for-each-ref' and reports whether the
   672  	// result is definitive.
   673  	describe := func() (definitive bool) {
   674  		var out []byte
   675  		out, err = Run(r.dir, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev)
   676  		if err != nil {
   677  			return true
   678  		}
   679  
   680  		// prefixed tags aren't valid semver tags so compare without prefix, but only tags with correct prefix
   681  		var highest string
   682  		for _, line := range strings.Split(string(out), "\n") {
   683  			line = strings.TrimSpace(line)
   684  			// git do support lstrip in for-each-ref format, but it was added in v2.13.0. Stripping here
   685  			// instead gives support for git v2.7.0.
   686  			if !strings.HasPrefix(line, "refs/tags/") {
   687  				continue
   688  			}
   689  			line = line[len("refs/tags/"):]
   690  
   691  			if !strings.HasPrefix(line, prefix) {
   692  				continue
   693  			}
   694  			if !allowed(line) {
   695  				continue
   696  			}
   697  
   698  			semtag := line[len(prefix):]
   699  			if semver.Compare(semtag, highest) > 0 {
   700  				highest = semtag
   701  			}
   702  		}
   703  
   704  		if highest != "" {
   705  			tag = prefix + highest
   706  		}
   707  
   708  		return tag != "" && !AllHex(tag)
   709  	}
   710  
   711  	if describe() {
   712  		return tag, err
   713  	}
   714  
   715  	// Git didn't find a version tag preceding the requested rev.
   716  	// See whether any plausible tag exists.
   717  	tags, err := r.Tags(prefix + "v")
   718  	if err != nil {
   719  		return "", err
   720  	}
   721  	if len(tags.List) == 0 {
   722  		return "", nil
   723  	}
   724  
   725  	// There are plausible tags, but we don't know if rev is a descendent of any of them.
   726  	// Fetch the history to find out.
   727  
   728  	unlock, err := r.mu.Lock()
   729  	if err != nil {
   730  		return "", err
   731  	}
   732  	defer unlock()
   733  
   734  	if err := r.fetchRefsLocked(); err != nil {
   735  		return "", err
   736  	}
   737  
   738  	// If we've reached this point, we have all of the commits that are reachable
   739  	// from all heads and tags.
   740  	//
   741  	// The only refs we should be missing are those that are no longer reachable
   742  	// (or never were reachable) from any branch or tag, including the master
   743  	// branch, and we don't want to resolve them anyway (they're probably
   744  	// unreachable for a reason).
   745  	//
   746  	// Try one last time in case some other goroutine fetched rev while we were
   747  	// waiting on the lock.
   748  	describe()
   749  	return tag, err
   750  }
   751  
   752  func (r *gitRepo) DescendsFrom(rev, tag string) (bool, error) {
   753  	// The "--is-ancestor" flag was added to "git merge-base" in version 1.8.0, so
   754  	// this won't work with Git 1.7.1. According to golang.org/issue/28550, cmd/go
   755  	// already doesn't work with Git 1.7.1, so at least it's not a regression.
   756  	//
   757  	// git merge-base --is-ancestor exits with status 0 if rev is an ancestor, or
   758  	// 1 if not.
   759  	_, err := Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
   760  
   761  	// Git reports "is an ancestor" with exit code 0 and "not an ancestor" with
   762  	// exit code 1.
   763  	// Unfortunately, if we've already fetched rev with a shallow history, git
   764  	// merge-base has been observed to report a false-negative, so don't stop yet
   765  	// even if the exit code is 1!
   766  	if err == nil {
   767  		return true, nil
   768  	}
   769  
   770  	// See whether the tag and rev even exist.
   771  	tags, err := r.Tags(tag)
   772  	if err != nil {
   773  		return false, err
   774  	}
   775  	if len(tags.List) == 0 {
   776  		return false, nil
   777  	}
   778  
   779  	// NOTE: r.stat is very careful not to fetch commits that we shouldn't know
   780  	// about, like rejected GitHub pull requests, so don't try to short-circuit
   781  	// that here.
   782  	if _, err = r.stat(rev); err != nil {
   783  		return false, err
   784  	}
   785  
   786  	// Now fetch history so that git can search for a path.
   787  	unlock, err := r.mu.Lock()
   788  	if err != nil {
   789  		return false, err
   790  	}
   791  	defer unlock()
   792  
   793  	if r.fetchLevel < fetchAll {
   794  		// Fetch the complete history for all refs and heads. It would be more
   795  		// efficient to only fetch the history from rev to tag, but that's much more
   796  		// complicated, and any kind of shallow fetch is fairly likely to trigger
   797  		// bugs in JGit servers and/or the go command anyway.
   798  		if err := r.fetchRefsLocked(); err != nil {
   799  			return false, err
   800  		}
   801  	}
   802  
   803  	_, err = Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
   804  	if err == nil {
   805  		return true, nil
   806  	}
   807  	if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
   808  		return false, nil
   809  	}
   810  	return false, err
   811  }
   812  
   813  func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
   814  	// TODO: Use maxSize or drop it.
   815  	args := []string{}
   816  	if subdir != "" {
   817  		args = append(args, "--", subdir)
   818  	}
   819  	info, err := r.Stat(rev) // download rev into local git repo
   820  	if err != nil {
   821  		return nil, err
   822  	}
   823  
   824  	unlock, err := r.mu.Lock()
   825  	if err != nil {
   826  		return nil, err
   827  	}
   828  	defer unlock()
   829  
   830  	if err := ensureGitAttributes(r.dir); err != nil {
   831  		return nil, err
   832  	}
   833  
   834  	// Incredibly, git produces different archives depending on whether
   835  	// it is running on a Windows system or not, in an attempt to normalize
   836  	// text file line endings. Setting -c core.autocrlf=input means only
   837  	// translate files on the way into the repo, not on the way out (archive).
   838  	// The -c core.eol=lf should be unnecessary but set it anyway.
   839  	archive, err := Run(r.dir, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
   840  	if err != nil {
   841  		if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) {
   842  			return nil, fs.ErrNotExist
   843  		}
   844  		return nil, err
   845  	}
   846  
   847  	return io.NopCloser(bytes.NewReader(archive)), nil
   848  }
   849  
   850  // ensureGitAttributes makes sure export-subst and export-ignore features are
   851  // disabled for this repo. This is intended to be run prior to running git
   852  // archive so that zip files are generated that produce consistent ziphashes
   853  // for a given revision, independent of variables such as git version and the
   854  // size of the repo.
   855  //
   856  // See: https://github.com/golang/go/issues/27153
   857  func ensureGitAttributes(repoDir string) (err error) {
   858  	const attr = "\n* -export-subst -export-ignore\n"
   859  
   860  	d := repoDir + "/info"
   861  	p := d + "/attributes"
   862  
   863  	if err := os.MkdirAll(d, 0755); err != nil {
   864  		return err
   865  	}
   866  
   867  	f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
   868  	if err != nil {
   869  		return err
   870  	}
   871  	defer func() {
   872  		closeErr := f.Close()
   873  		if closeErr != nil {
   874  			err = closeErr
   875  		}
   876  	}()
   877  
   878  	b, err := io.ReadAll(f)
   879  	if err != nil {
   880  		return err
   881  	}
   882  	if !bytes.HasSuffix(b, []byte(attr)) {
   883  		_, err := f.WriteString(attr)
   884  		return err
   885  	}
   886  
   887  	return nil
   888  }