1  
     2  
     3  
     4  
     5  package codehost
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/base64"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"io/fs"
    16  	"net/url"
    17  	"os"
    18  	"os/exec"
    19  	"path/filepath"
    20  	"runtime"
    21  	"slices"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"cmd/go/internal/base"
    29  	"cmd/go/internal/lockedfile"
    30  	"cmd/go/internal/web"
    31  	"cmd/internal/par"
    32  
    33  	"golang.org/x/mod/semver"
    34  )
    35  
    36  
    37  
    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  func newGitRepo(ctx context.Context, remote string, local bool) (Repo, error) {
    48  	r := &gitRepo{remote: remote, local: local}
    49  	if local {
    50  		if strings.Contains(remote, "://") { 
    51  			return nil, fmt.Errorf("git remote (%s) lookup disabled", remote)
    52  		}
    53  		info, err := os.Stat(remote)
    54  		if err != nil {
    55  			return nil, err
    56  		}
    57  		if !info.IsDir() {
    58  			return nil, fmt.Errorf("%s exists but is not a directory", remote)
    59  		}
    60  		r.dir = remote
    61  		r.mu.Path = r.dir + ".lock"
    62  		r.sha256Hashes = r.checkConfigSHA256(ctx)
    63  		return r, nil
    64  	}
    65  	
    66  	if !strings.Contains(remote, "://") { 
    67  		if strings.Contains(remote, ":") {
    68  			return nil, fmt.Errorf("git remote (%s) must not be local directory (use URL syntax not host:path syntax)", remote)
    69  		}
    70  		return nil, fmt.Errorf("git remote (%s) must not be local directory", remote)
    71  	}
    72  	var err error
    73  	r.dir, r.mu.Path, err = WorkDir(ctx, gitWorkDirType, r.remote)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	unlock, err := r.mu.Lock()
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	defer unlock()
    83  
    84  	if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
    85  		repoSha256Hash := false
    86  		if refs, lrErr := r.loadRefs(ctx); lrErr == nil {
    87  			
    88  			
    89  			for _, refHash := range refs {
    90  				repoSha256Hash = len(refHash) == (256 / 4)
    91  				break
    92  			}
    93  		}
    94  		objFormatFlag := []string{}
    95  		if repoSha256Hash {
    96  			objFormatFlag = []string{"--object-format=sha256"}
    97  		}
    98  		if _, err := Run(ctx, r.dir, "git", "init", "--bare", objFormatFlag); err != nil {
    99  			os.RemoveAll(r.dir)
   100  			return nil, err
   101  		}
   102  		
   103  		
   104  		
   105  		
   106  		if _, err := r.runGit(ctx, "git", "remote", "add", "origin", "--", r.remote); err != nil {
   107  			os.RemoveAll(r.dir)
   108  			return nil, err
   109  		}
   110  		if runtime.GOOS == "windows" {
   111  			
   112  			
   113  			
   114  			
   115  			
   116  			
   117  			
   118  			
   119  			
   120  			if _, err := r.runGit(ctx, "git", "config", "core.longpaths", "true"); err != nil {
   121  				os.RemoveAll(r.dir)
   122  				return nil, err
   123  			}
   124  		}
   125  	}
   126  	r.sha256Hashes = r.checkConfigSHA256(ctx)
   127  	r.remoteURL = r.remote
   128  	r.remote = "origin"
   129  	return r, nil
   130  }
   131  
   132  type gitRepo struct {
   133  	ctx context.Context
   134  
   135  	remote, remoteURL string
   136  	local             bool 
   137  	dir               string
   138  
   139  	
   140  	sha256Hashes bool
   141  
   142  	mu lockedfile.Mutex 
   143  
   144  	fetchLevel int
   145  
   146  	statCache par.ErrCache[string, *RevInfo]
   147  
   148  	refsOnce sync.Once
   149  	
   150  	
   151  	refs    map[string]string
   152  	refsErr error
   153  
   154  	localTagsOnce sync.Once
   155  	localTags     sync.Map 
   156  }
   157  
   158  const (
   159  	
   160  	fetchNone = iota 
   161  	fetchSome        
   162  	fetchAll         
   163  )
   164  
   165  
   166  
   167  func (r *gitRepo) loadLocalTags(ctx context.Context) {
   168  	
   169  	
   170  	
   171  	out, err := r.runGit(ctx, "git", "tag", "-l")
   172  	if err != nil {
   173  		return
   174  	}
   175  
   176  	for _, line := range strings.Split(string(out), "\n") {
   177  		if line != "" {
   178  			r.localTags.Store(line, true)
   179  		}
   180  	}
   181  }
   182  
   183  func (r *gitRepo) CheckReuse(ctx context.Context, old *Origin, subdir string) error {
   184  	if old == nil {
   185  		return fmt.Errorf("missing origin")
   186  	}
   187  	if old.VCS != "git" || old.URL != r.remoteURL {
   188  		return fmt.Errorf("origin moved from %v %q to %v %q", old.VCS, old.URL, "git", r.remoteURL)
   189  	}
   190  	if old.Subdir != subdir {
   191  		return fmt.Errorf("origin moved from %v %q %q to %v %q %q", old.VCS, old.URL, old.Subdir, "git", r.remoteURL, subdir)
   192  	}
   193  
   194  	
   195  	
   196  	
   197  	
   198  	
   199  	if old.Hash == "" && old.TagSum == "" && old.RepoSum == "" {
   200  		return fmt.Errorf("non-specific origin")
   201  	}
   202  
   203  	r.loadRefs(ctx)
   204  	if r.refsErr != nil {
   205  		return r.refsErr
   206  	}
   207  
   208  	if old.Ref != "" {
   209  		hash, ok := r.refs[old.Ref]
   210  		if !ok {
   211  			return fmt.Errorf("ref %q deleted", old.Ref)
   212  		}
   213  		if hash != old.Hash {
   214  			return fmt.Errorf("ref %q moved from %s to %s", old.Ref, old.Hash, hash)
   215  		}
   216  	}
   217  	if old.TagSum != "" {
   218  		tags, err := r.Tags(ctx, old.TagPrefix)
   219  		if err != nil {
   220  			return err
   221  		}
   222  		if tags.Origin.TagSum != old.TagSum {
   223  			return fmt.Errorf("tags changed")
   224  		}
   225  	}
   226  	if old.RepoSum != "" {
   227  		if r.repoSum(r.refs) != old.RepoSum {
   228  			return fmt.Errorf("refs changed")
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  
   235  
   236  func (r *gitRepo) loadRefs(ctx context.Context) (map[string]string, error) {
   237  	if r.local { 
   238  		
   239  		
   240  		return nil, nil
   241  	}
   242  	r.refsOnce.Do(func() {
   243  		
   244  		
   245  		
   246  		release, err := base.AcquireNet()
   247  		if err != nil {
   248  			r.refsErr = err
   249  			return
   250  		}
   251  		out, gitErr := r.runGit(ctx, "git", "ls-remote", "-q", r.remote)
   252  		release()
   253  
   254  		if gitErr != nil {
   255  			if rerr, ok := gitErr.(*RunError); ok {
   256  				if bytes.Contains(rerr.Stderr, []byte("fatal: could not read Username")) {
   257  					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."
   258  				}
   259  			}
   260  
   261  			
   262  			
   263  			
   264  			
   265  			if u, err := url.Parse(r.remoteURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
   266  				if _, err := web.GetBytes(u); errors.Is(err, fs.ErrNotExist) {
   267  					gitErr = notExistError{gitErr}
   268  				}
   269  			}
   270  
   271  			r.refsErr = gitErr
   272  			return
   273  		}
   274  
   275  		refs := make(map[string]string)
   276  		for _, line := range strings.Split(string(out), "\n") {
   277  			f := strings.Fields(line)
   278  			if len(f) != 2 {
   279  				continue
   280  			}
   281  			if f[1] == "HEAD" || strings.HasPrefix(f[1], "refs/heads/") || strings.HasPrefix(f[1], "refs/tags/") {
   282  				refs[f[1]] = f[0]
   283  			}
   284  		}
   285  		for ref, hash := range refs {
   286  			if k, found := strings.CutSuffix(ref, "^{}"); found { 
   287  				refs[k] = hash
   288  				delete(refs, ref)
   289  			}
   290  		}
   291  		r.refs = refs
   292  	})
   293  	return r.refs, r.refsErr
   294  }
   295  
   296  func (r *gitRepo) Tags(ctx context.Context, prefix string) (*Tags, error) {
   297  	refs, err := r.loadRefs(ctx)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	tags := &Tags{
   303  		Origin: &Origin{
   304  			VCS:       "git",
   305  			URL:       r.remoteURL,
   306  			TagPrefix: prefix,
   307  		},
   308  		List: []Tag{},
   309  	}
   310  	for ref, hash := range refs {
   311  		if !strings.HasPrefix(ref, "refs/tags/") {
   312  			continue
   313  		}
   314  		tag := ref[len("refs/tags/"):]
   315  		if !strings.HasPrefix(tag, prefix) {
   316  			continue
   317  		}
   318  		tags.List = append(tags.List, Tag{tag, hash})
   319  	}
   320  	sort.Slice(tags.List, func(i, j int) bool {
   321  		return tags.List[i].Name < tags.List[j].Name
   322  	})
   323  
   324  	dir := prefix[:strings.LastIndex(prefix, "/")+1]
   325  	h := sha256.New()
   326  	for _, tag := range tags.List {
   327  		if isOriginTag(strings.TrimPrefix(tag.Name, dir)) {
   328  			fmt.Fprintf(h, "%q %s\n", tag.Name, tag.Hash)
   329  		}
   330  	}
   331  	tags.Origin.TagSum = "t1:" + base64.StdEncoding.EncodeToString(h.Sum(nil))
   332  	return tags, nil
   333  }
   334  
   335  
   336  
   337  
   338  
   339  func (r *gitRepo) repoSum(refs map[string]string) string {
   340  	list := make([]string, 0, len(refs))
   341  	for ref := range refs {
   342  		list = append(list, ref)
   343  	}
   344  	sort.Strings(list)
   345  	h := sha256.New()
   346  	for _, ref := range list {
   347  		fmt.Fprintf(h, "%q %s\n", ref, refs[ref])
   348  	}
   349  	return "r1:" + base64.StdEncoding.EncodeToString(h.Sum(nil))
   350  }
   351  
   352  
   353  
   354  func (r *gitRepo) unknownRevisionInfo(refs map[string]string) *RevInfo {
   355  	return &RevInfo{
   356  		Origin: &Origin{
   357  			VCS:     "git",
   358  			URL:     r.remoteURL,
   359  			RepoSum: r.repoSum(refs),
   360  		},
   361  	}
   362  }
   363  
   364  func (r *gitRepo) Latest(ctx context.Context) (*RevInfo, error) {
   365  	refs, err := r.loadRefs(ctx)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  	if refs["HEAD"] == "" {
   370  		return nil, ErrNoCommits
   371  	}
   372  	statInfo, err := r.Stat(ctx, refs["HEAD"])
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  
   377  	
   378  	info := new(RevInfo)
   379  	*info = *statInfo
   380  	info.Origin = new(Origin)
   381  	if statInfo.Origin != nil {
   382  		*info.Origin = *statInfo.Origin
   383  	}
   384  	info.Origin.Ref = "HEAD"
   385  	info.Origin.Hash = refs["HEAD"]
   386  
   387  	return info, nil
   388  }
   389  
   390  
   391  
   392  
   393  
   394  func (r *gitRepo) findRef(ctx context.Context, hash string) (ref string, ok bool) {
   395  	refs, err := r.loadRefs(ctx)
   396  	if err != nil {
   397  		return "", false
   398  	}
   399  	for ref, h := range refs {
   400  		if h == hash {
   401  			return ref, true
   402  		}
   403  	}
   404  	return "", false
   405  }
   406  
   407  func (r *gitRepo) checkConfigSHA256(ctx context.Context) bool {
   408  	if hashType, sha256CfgErr := r.runGit(ctx, "git", "config", "extensions.objectformat"); sha256CfgErr == nil {
   409  		return "sha256" == strings.TrimSpace(string(hashType))
   410  	}
   411  	return false
   412  }
   413  
   414  func (r *gitRepo) hexHashLen() int {
   415  	if !r.sha256Hashes {
   416  		return 160 / 4
   417  	}
   418  	return 256 / 4
   419  }
   420  
   421  
   422  
   423  func (r *gitRepo) shortenObjectHash(rev string) string {
   424  	if !r.sha256Hashes {
   425  		return ShortenSHA1(rev)
   426  	}
   427  	if AllHex(rev) && len(rev) == 256/4 {
   428  		return rev[:12]
   429  	}
   430  	return rev
   431  }
   432  
   433  
   434  
   435  
   436  
   437  
   438  
   439  const minHashDigits = 7
   440  
   441  
   442  
   443  func (r *gitRepo) stat(ctx context.Context, rev string) (info *RevInfo, err error) {
   444  	
   445  	didStatLocal := false
   446  	if len(rev) >= minHashDigits && len(rev) <= r.hexHashLen() && AllHex(rev) {
   447  		if info, err := r.statLocal(ctx, rev, rev); err == nil {
   448  			return info, nil
   449  		}
   450  		didStatLocal = true
   451  	}
   452  
   453  	
   454  	
   455  	r.localTagsOnce.Do(func() { r.loadLocalTags(ctx) })
   456  	if _, ok := r.localTags.Load(rev); ok {
   457  		return r.statLocal(ctx, rev, "refs/tags/"+rev)
   458  	}
   459  
   460  	
   461  	
   462  	
   463  	
   464  	refs, err := r.loadRefs(ctx)
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  	
   469  	
   470  	
   471  	var ref, hash string
   472  	if refs["refs/tags/"+rev] != "" {
   473  		ref = "refs/tags/" + rev
   474  		hash = refs[ref]
   475  		
   476  	} else if refs["refs/heads/"+rev] != "" {
   477  		ref = "refs/heads/" + rev
   478  		hash = refs[ref]
   479  		rev = hash 
   480  	} else if rev == "HEAD" && refs["HEAD"] != "" {
   481  		ref = "HEAD"
   482  		hash = refs[ref]
   483  		rev = hash 
   484  	} else if len(rev) >= minHashDigits && len(rev) <= r.hexHashLen() && AllHex(rev) {
   485  		
   486  		
   487  		prefix := rev
   488  		
   489  		for k, h := range refs {
   490  			if strings.HasPrefix(h, prefix) {
   491  				if hash != "" && hash != h {
   492  					
   493  					
   494  					return nil, fmt.Errorf("ambiguous revision %s", rev)
   495  				}
   496  				if ref == "" || ref > k { 
   497  					ref = k
   498  				}
   499  				rev = h
   500  				hash = h
   501  			}
   502  		}
   503  		if hash == "" && len(rev) == r.hexHashLen() { 
   504  			hash = rev
   505  		}
   506  	} else {
   507  		return r.unknownRevisionInfo(refs), &UnknownRevisionError{Rev: rev}
   508  	}
   509  
   510  	defer func() {
   511  		if info != nil {
   512  			info.Origin.Hash = info.Name
   513  			
   514  			if ref != info.Origin.Hash {
   515  				info.Origin.Ref = ref
   516  			}
   517  		}
   518  	}()
   519  
   520  	
   521  	unlock, err := r.mu.Lock()
   522  	if err != nil {
   523  		return nil, err
   524  	}
   525  	defer unlock()
   526  
   527  	
   528  	
   529  	
   530  	
   531  	if !didStatLocal {
   532  		if info, err := r.statLocal(ctx, rev, hash); err == nil {
   533  			tag, fromTag := strings.CutPrefix(ref, "refs/tags/")
   534  			if fromTag && !slices.Contains(info.Tags, tag) {
   535  				
   536  				
   537  				_, err := r.runGit(ctx, "git", "tag", tag, hash)
   538  				if err != nil {
   539  					return nil, err
   540  				}
   541  				r.localTags.Store(tag, true)
   542  				return r.statLocal(ctx, rev, ref)
   543  			}
   544  			return info, err
   545  		}
   546  	}
   547  
   548  	if r.local { 
   549  		return nil, fmt.Errorf("revision does not exist locally: %s", rev)
   550  	}
   551  
   552  	
   553  	
   554  	
   555  	
   556  	
   557  	
   558  	
   559  	if r.fetchLevel <= fetchSome && ref != "" && hash != "" {
   560  		r.fetchLevel = fetchSome
   561  		var refspec string
   562  		if ref == "HEAD" {
   563  			
   564  			
   565  			
   566  			
   567  			
   568  			ref = hash
   569  			refspec = hash + ":refs/dummy"
   570  		} else {
   571  			
   572  			
   573  			
   574  			
   575  			refspec = ref + ":" + ref
   576  		}
   577  
   578  		release, err := base.AcquireNet()
   579  		if err != nil {
   580  			return nil, err
   581  		}
   582  		
   583  		
   584  		
   585  		
   586  		_, err = r.runGit(ctx, "git", "-c", "protocol.version=2", "fetch", "-f", "--depth=1", r.remote, refspec)
   587  		release()
   588  
   589  		if err == nil {
   590  			return r.statLocal(ctx, rev, ref)
   591  		}
   592  		
   593  		
   594  		
   595  	}
   596  
   597  	
   598  	
   599  	if err := r.fetchRefsLocked(ctx); err != nil {
   600  		return nil, err
   601  	}
   602  
   603  	return r.statLocal(ctx, rev, rev)
   604  }
   605  
   606  
   607  
   608  
   609  
   610  
   611  
   612  
   613  
   614  
   615  func (r *gitRepo) fetchRefsLocked(ctx context.Context) error {
   616  	if r.local {
   617  		panic("go: fetchRefsLocked called in local only mode.")
   618  	}
   619  	if r.fetchLevel < fetchAll {
   620  		
   621  		
   622  		
   623  		
   624  		
   625  
   626  		release, err := base.AcquireNet()
   627  		if err != nil {
   628  			return err
   629  		}
   630  		defer release()
   631  
   632  		if _, err := r.runGit(ctx, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
   633  			return err
   634  		}
   635  
   636  		if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil {
   637  			if _, err := r.runGit(ctx, "git", "fetch", "--unshallow", "-f", r.remote); err != nil {
   638  				return err
   639  			}
   640  		}
   641  
   642  		r.fetchLevel = fetchAll
   643  	}
   644  	return nil
   645  }
   646  
   647  
   648  
   649  func (r *gitRepo) statLocal(ctx context.Context, version, rev string) (*RevInfo, error) {
   650  	out, err := r.runGit(ctx, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", rev, "--")
   651  	if err != nil {
   652  		
   653  		var info *RevInfo
   654  		if refs, err := r.loadRefs(ctx); err == nil {
   655  			info = r.unknownRevisionInfo(refs)
   656  		}
   657  		return info, &UnknownRevisionError{Rev: rev}
   658  	}
   659  	f := strings.Fields(string(out))
   660  	if len(f) < 2 {
   661  		return nil, fmt.Errorf("unexpected response from git log: %q", out)
   662  	}
   663  	hash := f[0]
   664  	if strings.HasPrefix(hash, version) {
   665  		version = hash 
   666  	}
   667  	t, err := strconv.ParseInt(f[1], 10, 64)
   668  	if err != nil {
   669  		return nil, fmt.Errorf("invalid time from git log: %q", out)
   670  	}
   671  
   672  	info := &RevInfo{
   673  		Origin: &Origin{
   674  			VCS:  "git",
   675  			URL:  r.remoteURL,
   676  			Hash: hash,
   677  		},
   678  		Name:    hash,
   679  		Short:   r.shortenObjectHash(hash),
   680  		Time:    time.Unix(t, 0).UTC(),
   681  		Version: hash,
   682  	}
   683  	if !strings.HasPrefix(hash, rev) {
   684  		info.Origin.Ref = rev
   685  	}
   686  
   687  	
   688  	
   689  	for i := 2; i < len(f); i++ {
   690  		if f[i] == "tag:" {
   691  			i++
   692  			if i < len(f) {
   693  				info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ","))
   694  			}
   695  		}
   696  	}
   697  
   698  	
   699  	
   700  	
   701  	if refs, err := r.loadRefs(ctx); err == nil {
   702  		for ref, h := range refs {
   703  			if h == hash {
   704  				if tag, found := strings.CutPrefix(ref, "refs/tags/"); found {
   705  					info.Tags = append(info.Tags, tag)
   706  				}
   707  			}
   708  		}
   709  	}
   710  	slices.Sort(info.Tags)
   711  	info.Tags = slices.Compact(info.Tags)
   712  
   713  	
   714  	
   715  	
   716  	for _, tag := range info.Tags {
   717  		if version == tag {
   718  			info.Version = version
   719  		}
   720  	}
   721  
   722  	return info, nil
   723  }
   724  
   725  func (r *gitRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
   726  	if rev == "latest" {
   727  		return r.Latest(ctx)
   728  	}
   729  	return r.statCache.Do(rev, func() (*RevInfo, error) {
   730  		return r.stat(ctx, rev)
   731  	})
   732  }
   733  
   734  func (r *gitRepo) ReadFile(ctx context.Context, rev, file string, maxSize int64) ([]byte, error) {
   735  	
   736  	info, err := r.Stat(ctx, rev) 
   737  	if err != nil {
   738  		return nil, err
   739  	}
   740  	out, err := r.runGit(ctx, "git", "cat-file", "blob", info.Name+":"+file)
   741  	if err != nil {
   742  		return nil, fs.ErrNotExist
   743  	}
   744  	return out, nil
   745  }
   746  
   747  func (r *gitRepo) RecentTag(ctx context.Context, rev, prefix string, allowed func(tag string) bool) (tag string, err error) {
   748  	info, err := r.Stat(ctx, rev)
   749  	if err != nil {
   750  		return "", err
   751  	}
   752  	rev = info.Name 
   753  
   754  	
   755  	
   756  	describe := func() (definitive bool) {
   757  		var out []byte
   758  		out, err = r.runGit(ctx, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev)
   759  		if err != nil {
   760  			return true
   761  		}
   762  
   763  		
   764  		var highest string
   765  		for _, line := range strings.Split(string(out), "\n") {
   766  			line = strings.TrimSpace(line)
   767  			
   768  			
   769  			if !strings.HasPrefix(line, "refs/tags/") {
   770  				continue
   771  			}
   772  			line = line[len("refs/tags/"):]
   773  
   774  			if !strings.HasPrefix(line, prefix) {
   775  				continue
   776  			}
   777  			if !allowed(line) {
   778  				continue
   779  			}
   780  
   781  			semtag := line[len(prefix):]
   782  			if semver.Compare(semtag, highest) > 0 {
   783  				highest = semtag
   784  			}
   785  		}
   786  
   787  		if highest != "" {
   788  			tag = prefix + highest
   789  		}
   790  
   791  		return tag != "" && !AllHex(tag)
   792  	}
   793  
   794  	if describe() {
   795  		return tag, err
   796  	}
   797  
   798  	
   799  	
   800  	tags, err := r.Tags(ctx, prefix+"v")
   801  	if err != nil {
   802  		return "", err
   803  	}
   804  	if len(tags.List) == 0 {
   805  		return "", nil
   806  	}
   807  
   808  	if r.local { 
   809  		return "", fmt.Errorf("revision does not exist locally: %s", rev)
   810  	}
   811  	
   812  	
   813  
   814  	unlock, err := r.mu.Lock()
   815  	if err != nil {
   816  		return "", err
   817  	}
   818  	defer unlock()
   819  
   820  	if err := r.fetchRefsLocked(ctx); err != nil {
   821  		return "", err
   822  	}
   823  
   824  	
   825  	
   826  	
   827  	
   828  	
   829  	
   830  	
   831  	
   832  	
   833  	
   834  	describe()
   835  	return tag, err
   836  }
   837  
   838  func (r *gitRepo) DescendsFrom(ctx context.Context, rev, tag string) (bool, error) {
   839  	
   840  	
   841  	
   842  	
   843  	
   844  	
   845  	_, err := r.runGit(ctx, "git", "merge-base", "--is-ancestor", "--", tag, rev)
   846  
   847  	
   848  	
   849  	
   850  	
   851  	
   852  	if err == nil {
   853  		return true, nil
   854  	}
   855  
   856  	
   857  	tags, err := r.Tags(ctx, tag)
   858  	if err != nil {
   859  		return false, err
   860  	}
   861  	if len(tags.List) == 0 {
   862  		return false, nil
   863  	}
   864  
   865  	
   866  	
   867  	
   868  	if _, err = r.stat(ctx, rev); err != nil {
   869  		return false, err
   870  	}
   871  
   872  	if r.local { 
   873  		return false, fmt.Errorf("revision does not exist locally: %s", rev)
   874  	}
   875  
   876  	
   877  	unlock, err := r.mu.Lock()
   878  	if err != nil {
   879  		return false, err
   880  	}
   881  	defer unlock()
   882  
   883  	if r.fetchLevel < fetchAll {
   884  		
   885  		
   886  		
   887  		
   888  		if err := r.fetchRefsLocked(ctx); err != nil {
   889  			return false, err
   890  		}
   891  	}
   892  
   893  	_, err = r.runGit(ctx, "git", "merge-base", "--is-ancestor", "--", tag, rev)
   894  	if err == nil {
   895  		return true, nil
   896  	}
   897  	if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
   898  		return false, nil
   899  	}
   900  	return false, err
   901  }
   902  
   903  func (r *gitRepo) ReadZip(ctx context.Context, rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
   904  	
   905  	args := []string{}
   906  	if subdir != "" {
   907  		args = append(args, "--", subdir)
   908  	}
   909  	info, err := r.Stat(ctx, rev) 
   910  	if err != nil {
   911  		return nil, err
   912  	}
   913  
   914  	unlock, err := r.mu.Lock()
   915  	if err != nil {
   916  		return nil, err
   917  	}
   918  	defer unlock()
   919  
   920  	if err := ensureGitAttributes(r.dir); err != nil {
   921  		return nil, err
   922  	}
   923  
   924  	
   925  	
   926  	
   927  	
   928  	
   929  	archive, err := r.runGit(ctx, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
   930  	if err != nil {
   931  		if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) {
   932  			return nil, fs.ErrNotExist
   933  		}
   934  		return nil, err
   935  	}
   936  
   937  	return io.NopCloser(bytes.NewReader(archive)), nil
   938  }
   939  
   940  
   941  
   942  
   943  
   944  
   945  
   946  
   947  func ensureGitAttributes(repoDir string) (err error) {
   948  	const attr = "\n* -export-subst -export-ignore\n"
   949  
   950  	d := repoDir + "/info"
   951  	p := d + "/attributes"
   952  
   953  	if err := os.MkdirAll(d, 0755); err != nil {
   954  		return err
   955  	}
   956  
   957  	f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
   958  	if err != nil {
   959  		return err
   960  	}
   961  	defer func() {
   962  		closeErr := f.Close()
   963  		if closeErr != nil {
   964  			err = closeErr
   965  		}
   966  	}()
   967  
   968  	b, err := io.ReadAll(f)
   969  	if err != nil {
   970  		return err
   971  	}
   972  	if !bytes.HasSuffix(b, []byte(attr)) {
   973  		_, err := f.WriteString(attr)
   974  		return err
   975  	}
   976  
   977  	return nil
   978  }
   979  
   980  func (r *gitRepo) runGit(ctx context.Context, cmdline ...any) ([]byte, error) {
   981  	args := RunArgs{cmdline: cmdline, dir: r.dir, local: r.local}
   982  	if !r.local {
   983  		
   984  		
   985  		args.env = []string{"GIT_DIR=" + r.dir}
   986  	}
   987  	return RunWithArgs(ctx, args)
   988  }
   989  
View as plain text