Source file src/cmd/go/internal/modfetch/codehost/git.go

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

View as plain text