...

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

View as plain text