Source file src/cmd/go/internal/modfetch/codehost/vcs.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  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"internal/lazyregexp"
    12  	"io"
    13  	"io/fs"
    14  	"os"
    15  	"path/filepath"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"sync/atomic"
    21  	"time"
    22  
    23  	"cmd/go/internal/base"
    24  	"cmd/go/internal/cfg"
    25  	"cmd/go/internal/lockedfile"
    26  	"cmd/go/internal/str"
    27  	"cmd/internal/par"
    28  
    29  	"golang.org/x/mod/semver"
    30  )
    31  
    32  // A VCSError indicates an error using a version control system.
    33  // The implication of a VCSError is that we know definitively where
    34  // to get the code, but we can't access it due to the error.
    35  // The caller should report this error instead of continuing to probe
    36  // other possible module paths.
    37  //
    38  // TODO(golang.org/issue/31730): See if we can invert this. (Return a
    39  // distinguished error for “repo not found” and treat everything else
    40  // as terminal.)
    41  type VCSError struct {
    42  	Err error
    43  }
    44  
    45  func (e *VCSError) Error() string { return e.Err.Error() }
    46  
    47  func (e *VCSError) Unwrap() error { return e.Err }
    48  
    49  func vcsErrorf(format string, a ...any) error {
    50  	return &VCSError{Err: fmt.Errorf(format, a...)}
    51  }
    52  
    53  type vcsCacheKey struct {
    54  	vcs    string
    55  	remote string
    56  	local  bool
    57  }
    58  
    59  func NewRepo(ctx context.Context, vcs, remote string, local bool) (Repo, error) {
    60  	return vcsRepoCache.Do(vcsCacheKey{vcs, remote, local}, func() (Repo, error) {
    61  		repo, err := newVCSRepo(ctx, vcs, remote, local)
    62  		if err != nil {
    63  			return nil, &VCSError{err}
    64  		}
    65  		return repo, nil
    66  	})
    67  }
    68  
    69  var vcsRepoCache par.ErrCache[vcsCacheKey, Repo]
    70  
    71  type vcsRepo struct {
    72  	mu lockedfile.Mutex // protects all commands, so we don't have to decide which are safe on a per-VCS basis
    73  
    74  	remote string
    75  	cmd    *vcsCmd
    76  	dir    string
    77  	local  bool
    78  
    79  	tagsOnce sync.Once
    80  	tags     map[string]bool
    81  
    82  	branchesOnce sync.Once
    83  	branches     map[string]bool
    84  
    85  	fetchOnce sync.Once
    86  	fetchErr  error
    87  	fetched   atomic.Bool
    88  
    89  	repoSumOnce sync.Once
    90  	repoSum     string
    91  
    92  	statCache     par.ErrCache[string, *RevInfo]  // cache key is revision
    93  	readFileCache par.ErrCache[[2]string, []byte] // cache key is revision and file path
    94  }
    95  
    96  func newVCSRepo(ctx context.Context, vcs, remote string, local bool) (Repo, error) {
    97  	if vcs == "git" {
    98  		return newGitRepo(ctx, remote, local)
    99  	}
   100  	r := &vcsRepo{remote: remote, local: local}
   101  	cmd := vcsCmds[vcs]
   102  	if cmd == nil {
   103  		return nil, fmt.Errorf("unknown vcs: %s %s", vcs, remote)
   104  	}
   105  	r.cmd = cmd
   106  	if local {
   107  		info, err := os.Stat(remote)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  		if !info.IsDir() {
   112  			return nil, fmt.Errorf("%s exists but is not a directory", remote)
   113  		}
   114  		r.dir = remote
   115  		r.mu.Path = r.dir + ".lock"
   116  		return r, nil
   117  	}
   118  	if !strings.Contains(remote, "://") {
   119  		return nil, fmt.Errorf("invalid vcs remote: %s %s", vcs, remote)
   120  	}
   121  	var err error
   122  	r.dir, r.mu.Path, err = WorkDir(ctx, vcsWorkDirType+vcs, r.remote)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	if cmd.init == nil {
   128  		return r, nil
   129  	}
   130  
   131  	unlock, err := r.mu.Lock()
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	defer unlock()
   136  
   137  	if _, err := os.Stat(filepath.Join(r.dir, "."+vcs)); err != nil {
   138  		release, err := base.AcquireNet()
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  		_, err = Run(ctx, r.dir, cmd.init(r.remote))
   143  		if err == nil && cmd.postInit != nil {
   144  			err = cmd.postInit(ctx, r)
   145  		}
   146  		release()
   147  
   148  		if err != nil {
   149  			os.RemoveAll(r.dir)
   150  			return nil, err
   151  		}
   152  	}
   153  	return r, nil
   154  }
   155  
   156  const vcsWorkDirType = "vcs1."
   157  
   158  type vcsCmd struct {
   159  	vcs                string                                            // vcs name "hg"
   160  	init               func(remote string) []string                      // cmd to init repo to track remote
   161  	postInit           func(context.Context, *vcsRepo) error             // func to init repo after .init runs
   162  	repoSum            func(remote string) []string                      // cmd to calculate reposum of remote repo
   163  	lookupRef          func(remote, ref string) []string                 // cmd to look up ref in remote repo
   164  	tags               func(remote string) []string                      // cmd to list local tags
   165  	tagsNeedsFetch     bool                                              // run fetch before tags
   166  	tagRE              *lazyregexp.Regexp                                // regexp to extract tag names from output of tags cmd
   167  	branches           func(remote string) []string                      // cmd to list local branches
   168  	branchesNeedsFetch bool                                              // run branches before tags
   169  	branchRE           *lazyregexp.Regexp                                // regexp to extract branch names from output of tags cmd
   170  	badLocalRevRE      *lazyregexp.Regexp                                // regexp of names that must not be served out of local cache without doing fetch first
   171  	statLocal          func(rev, remote string) []string                 // cmd to stat local rev
   172  	parseStat          func(rev, out string) (*RevInfo, error)           // func to parse output of statLocal
   173  	fetch              []string                                          // cmd to fetch everything from remote
   174  	latest             string                                            // name of latest commit on remote (tip, HEAD, etc)
   175  	descendsFrom       func(rev, tag string) []string                    // cmd to check whether rev descends from tag
   176  	recentTags         func(rev string) []string                         // cmd to print tag ancestors of rev
   177  	readFile           func(rev, file, remote string) []string           // cmd to read rev's file
   178  	readZip            func(rev, subdir, remote, target string) []string // cmd to read rev's subdir as zip file
   179  
   180  	// arbitrary function to read rev's subdir as zip file
   181  	doReadZip func(ctx context.Context, dst io.Writer, workDir, rev, subdir, remote string) error
   182  }
   183  
   184  var re = lazyregexp.New
   185  
   186  var vcsCmds = map[string]*vcsCmd{
   187  	"hg": {
   188  		vcs: "hg",
   189  		repoSum: func(remote string) []string {
   190  			return []string{
   191  				"hg",
   192  				"--config=extensions.goreposum=" + filepath.Join(cfg.GOROOT, "lib/hg/goreposum.py"),
   193  				"goreposum",
   194  				"--",
   195  				remote,
   196  			}
   197  		},
   198  		lookupRef: func(remote, ref string) []string {
   199  			return []string{
   200  				"hg",
   201  				"--config=extensions.goreposum=" + filepath.Join(cfg.GOROOT, "lib/hg/goreposum.py"),
   202  				"golookup",
   203  				"--",
   204  				remote,
   205  				ref,
   206  			}
   207  		},
   208  		init: func(remote string) []string {
   209  			return []string{"hg", "init", "."}
   210  		},
   211  		postInit: hgAddRemote,
   212  		tags: func(remote string) []string {
   213  			return []string{"hg", "tags", "-q"}
   214  		},
   215  		tagsNeedsFetch: true,
   216  		tagRE:          re(`(?m)^[^\n]+$`),
   217  		branches: func(remote string) []string {
   218  			return []string{"hg", "branches", "-c", "-q"}
   219  		},
   220  		branchesNeedsFetch: true,
   221  		branchRE:           re(`(?m)^[^\n]+$`),
   222  		badLocalRevRE:      re(`(?m)^(tip)$`),
   223  		statLocal: func(rev, remote string) []string {
   224  			return []string{"hg", "log", "-l1", fmt.Sprintf("--rev=%s", rev), "--template", "{node} {date|hgdate} {tags}"}
   225  		},
   226  		parseStat: hgParseStat,
   227  		fetch:     []string{"hg", "pull", "-f"},
   228  		latest:    "tip",
   229  		descendsFrom: func(rev, tag string) []string {
   230  			return []string{"hg", "log", "--rev=ancestors(" + rev + ") and " + tag}
   231  		},
   232  		recentTags: func(rev string) []string {
   233  			return []string{"hg", "log", "--rev=ancestors(" + rev + ") and tag()", "--template", "{tags}\n"}
   234  		},
   235  		readFile: func(rev, file, remote string) []string {
   236  			return []string{"hg", "cat", fmt.Sprintf("--rev=%s", rev), "--", file}
   237  		},
   238  		readZip: func(rev, subdir, remote, target string) []string {
   239  			pattern := []string{}
   240  			if subdir != "" {
   241  				pattern = []string{fmt.Sprintf("--include=%s", subdir+"/**")}
   242  			}
   243  			return str.StringList("hg", "archive", "-t", "zip", "--no-decode", fmt.Sprintf("--rev=%s", rev), "--prefix=prefix/", pattern, "--", target)
   244  		},
   245  	},
   246  
   247  	"svn": {
   248  		vcs:  "svn",
   249  		init: nil, // no local checkout
   250  		tags: func(remote string) []string {
   251  			return []string{"svn", "list", "--", strings.TrimSuffix(remote, "/trunk") + "/tags"}
   252  		},
   253  		tagRE: re(`(?m)^(.*?)/?$`),
   254  		statLocal: func(rev, remote string) []string {
   255  			suffix := "@" + rev
   256  			if rev == "latest" {
   257  				suffix = ""
   258  			}
   259  			return []string{"svn", "log", "-l1", "--xml", "--", remote + suffix}
   260  		},
   261  		parseStat: svnParseStat,
   262  		latest:    "latest",
   263  		readFile: func(rev, file, remote string) []string {
   264  			return []string{"svn", "cat", "--", remote + "/" + file + "@" + rev}
   265  		},
   266  		doReadZip: svnReadZip,
   267  	},
   268  
   269  	"fossil": {
   270  		vcs: "fossil",
   271  		init: func(remote string) []string {
   272  			return []string{"fossil", "clone", "--", remote, ".fossil"}
   273  		},
   274  		fetch: []string{"fossil", "pull", "-R", ".fossil"},
   275  		tags: func(remote string) []string {
   276  			return []string{"fossil", "tag", "-R", ".fossil", "list"}
   277  		},
   278  		tagRE: re(`XXXTODO`),
   279  		statLocal: func(rev, remote string) []string {
   280  			return []string{"fossil", "info", "-R", ".fossil", "--", rev}
   281  		},
   282  		parseStat: fossilParseStat,
   283  		latest:    "trunk",
   284  		readFile: func(rev, file, remote string) []string {
   285  			return []string{"fossil", "cat", "-R", ".fossil", fmt.Sprintf("-r=%s", rev), "--", file}
   286  		},
   287  		readZip: func(rev, subdir, remote, target string) []string {
   288  			extra := []string{}
   289  			if subdir != "" && !strings.ContainsAny(subdir, "*?[],") {
   290  				extra = []string{fmt.Sprintf("--include=%s", subdir)}
   291  			}
   292  			// Note that vcsRepo.ReadZip below rewrites this command
   293  			// to run in a different directory, to work around a fossil bug.
   294  			return str.StringList("fossil", "zip", "-R", ".fossil", "--name", "prefix", extra, "--", rev, target)
   295  		},
   296  	},
   297  }
   298  
   299  func (r *vcsRepo) loadTags(ctx context.Context) {
   300  	if r.cmd.tagsNeedsFetch {
   301  		r.fetchOnce.Do(func() { r.fetch(ctx) })
   302  	}
   303  
   304  	out, err := Run(ctx, r.dir, r.cmd.tags(r.remote))
   305  	if err != nil {
   306  		return
   307  	}
   308  
   309  	// Run tag-listing command and extract tags.
   310  	r.tags = make(map[string]bool)
   311  	for _, tag := range r.cmd.tagRE.FindAllString(string(out), -1) {
   312  		if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(tag) {
   313  			continue
   314  		}
   315  		r.tags[tag] = true
   316  	}
   317  }
   318  
   319  func (r *vcsRepo) loadBranches(ctx context.Context) {
   320  	if r.cmd.branches == nil {
   321  		return
   322  	}
   323  
   324  	if r.cmd.branchesNeedsFetch {
   325  		r.fetchOnce.Do(func() { r.fetch(ctx) })
   326  	}
   327  
   328  	out, err := Run(ctx, r.dir, r.cmd.branches(r.remote))
   329  	if err != nil {
   330  		return
   331  	}
   332  
   333  	r.branches = make(map[string]bool)
   334  	for _, branch := range r.cmd.branchRE.FindAllString(string(out), -1) {
   335  		if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(branch) {
   336  			continue
   337  		}
   338  		r.branches[branch] = true
   339  	}
   340  }
   341  
   342  func (r *vcsRepo) loadRepoSum(ctx context.Context) {
   343  	if r.cmd.repoSum == nil {
   344  		return
   345  	}
   346  	where := r.remote
   347  	if r.fetched.Load() {
   348  		where = "." // use local repo
   349  	}
   350  	out, err := Run(ctx, r.dir, r.cmd.repoSum(where))
   351  	if err != nil {
   352  		return
   353  	}
   354  	r.repoSum = strings.TrimSpace(string(out))
   355  }
   356  
   357  func (r *vcsRepo) lookupRef(ctx context.Context, ref string) (string, error) {
   358  	if r.cmd.lookupRef == nil {
   359  		return "", fmt.Errorf("no lookupRef")
   360  	}
   361  	out, err := Run(ctx, r.dir, r.cmd.lookupRef(r.remote, ref))
   362  	if err != nil {
   363  		return "", err
   364  	}
   365  	return strings.TrimSpace(string(out)), nil
   366  }
   367  
   368  // repoSumOrigin returns an Origin containing a RepoSum.
   369  func (r *vcsRepo) repoSumOrigin(ctx context.Context) *Origin {
   370  	origin := &Origin{
   371  		VCS:     r.cmd.vcs,
   372  		URL:     r.remote,
   373  		RepoSum: r.repoSum,
   374  	}
   375  	r.repoSumOnce.Do(func() { r.loadRepoSum(ctx) })
   376  	origin.RepoSum = r.repoSum
   377  	return origin
   378  }
   379  
   380  func (r *vcsRepo) CheckReuse(ctx context.Context, old *Origin, subdir string) error {
   381  	if old == nil {
   382  		return fmt.Errorf("missing origin")
   383  	}
   384  	if old.VCS != r.cmd.vcs || old.URL != r.remote {
   385  		return fmt.Errorf("origin moved from %v %q to %v %q", old.VCS, old.URL, r.cmd.vcs, r.remote)
   386  	}
   387  	if old.Subdir != subdir {
   388  		return fmt.Errorf("origin moved from %v %q %q to %v %q %q", old.VCS, old.URL, old.Subdir, r.cmd.vcs, r.remote, subdir)
   389  	}
   390  
   391  	if old.Ref == "" && old.RepoSum == "" && old.Hash != "" {
   392  		// Hash has to remain in repo.
   393  		hash, err := r.lookupRef(ctx, old.Hash)
   394  		if err == nil && hash == old.Hash {
   395  			return nil
   396  		}
   397  		if err != nil {
   398  			return fmt.Errorf("looking up hash: %v", err)
   399  		}
   400  		return fmt.Errorf("hash changed") // weird but maybe they made a tag
   401  	}
   402  
   403  	if old.Ref != "" && old.RepoSum == "" {
   404  		hash, err := r.lookupRef(ctx, old.Ref)
   405  		if err == nil && hash != "" && hash == old.Hash {
   406  			return nil
   407  		}
   408  	}
   409  
   410  	r.repoSumOnce.Do(func() { r.loadRepoSum(ctx) })
   411  	if r.repoSum != "" {
   412  		if old.RepoSum == "" {
   413  			return fmt.Errorf("non-specific origin")
   414  		}
   415  		if old.RepoSum != r.repoSum {
   416  			return fmt.Errorf("repo changed")
   417  		}
   418  		return nil
   419  	}
   420  	return fmt.Errorf("vcs %s: CheckReuse: %w", r.cmd.vcs, errors.ErrUnsupported)
   421  }
   422  
   423  func (r *vcsRepo) Tags(ctx context.Context, prefix string) (*Tags, error) {
   424  	unlock, err := r.mu.Lock()
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  	defer unlock()
   429  
   430  	r.tagsOnce.Do(func() { r.loadTags(ctx) })
   431  	tags := &Tags{
   432  		Origin: r.repoSumOrigin(ctx),
   433  		List:   []Tag{},
   434  	}
   435  	for tag := range r.tags {
   436  		if strings.HasPrefix(tag, prefix) {
   437  			tags.List = append(tags.List, Tag{tag, ""})
   438  		}
   439  	}
   440  	sort.Slice(tags.List, func(i, j int) bool {
   441  		return tags.List[i].Name < tags.List[j].Name
   442  	})
   443  	return tags, nil
   444  }
   445  
   446  func (r *vcsRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
   447  	return r.statCache.Do(rev, func() (*RevInfo, error) {
   448  		unlock, err := r.mu.Lock()
   449  		if err != nil {
   450  			return nil, err
   451  		}
   452  		defer unlock()
   453  
   454  		if rev == "latest" {
   455  			rev = r.cmd.latest
   456  		}
   457  		r.branchesOnce.Do(func() { r.loadBranches(ctx) })
   458  		if r.local {
   459  			// Ignore the badLocalRevRE precondition in local only mode.
   460  			// We cannot fetch latest upstream changes so only serve what's in the local cache.
   461  			return r.statLocal(ctx, rev)
   462  		}
   463  		revOK := (r.cmd.badLocalRevRE == nil || !r.cmd.badLocalRevRE.MatchString(rev)) && !r.branches[rev]
   464  		if revOK {
   465  			if info, err := r.statLocal(ctx, rev); err == nil {
   466  				return info, nil
   467  			}
   468  		}
   469  
   470  		r.fetchOnce.Do(func() { r.fetch(ctx) })
   471  		if r.fetchErr != nil {
   472  			return nil, r.fetchErr
   473  		}
   474  		info, err := r.statLocal(ctx, rev)
   475  		if err != nil {
   476  			return info, err
   477  		}
   478  		if !revOK {
   479  			info.Version = info.Name
   480  		}
   481  		return info, nil
   482  	})
   483  }
   484  
   485  func (r *vcsRepo) fetch(ctx context.Context) {
   486  	if len(r.cmd.fetch) > 0 {
   487  		release, err := base.AcquireNet()
   488  		if err != nil {
   489  			r.fetchErr = err
   490  			return
   491  		}
   492  		_, r.fetchErr = Run(ctx, r.dir, r.cmd.fetch)
   493  		release()
   494  		r.fetched.Store(true)
   495  	}
   496  }
   497  
   498  func (r *vcsRepo) statLocal(ctx context.Context, rev string) (*RevInfo, error) {
   499  	out, err := Run(ctx, r.dir, r.cmd.statLocal(rev, r.remote))
   500  	if err != nil {
   501  		info := &RevInfo{Origin: r.repoSumOrigin(ctx)}
   502  		return info, &UnknownRevisionError{Rev: rev}
   503  	}
   504  	info, err := r.cmd.parseStat(rev, string(out))
   505  	if err != nil {
   506  		return nil, err
   507  	}
   508  	if info.Origin == nil {
   509  		info.Origin = new(Origin)
   510  	}
   511  	info.Origin.VCS = r.cmd.vcs
   512  	info.Origin.URL = r.remote
   513  	info.Origin.Ref = rev
   514  	if strings.HasPrefix(info.Name, rev) && len(rev) >= 12 {
   515  		info.Origin.Ref = "" // duplicates Hash
   516  	}
   517  	return info, nil
   518  }
   519  
   520  func (r *vcsRepo) Latest(ctx context.Context) (*RevInfo, error) {
   521  	return r.Stat(ctx, "latest")
   522  }
   523  
   524  func (r *vcsRepo) ReadFile(ctx context.Context, rev, file string, maxSize int64) ([]byte, error) {
   525  	return r.readFileCache.Do([2]string{rev, file}, func() ([]byte, error) {
   526  		if rev == "latest" {
   527  			rev = r.cmd.latest
   528  		}
   529  		_, err := r.Stat(ctx, rev) // download rev into local repo
   530  		if err != nil {
   531  			return nil, err
   532  		}
   533  
   534  		// r.Stat acquires r.mu, so lock after that.
   535  		unlock, err := r.mu.Lock()
   536  		if err != nil {
   537  			return nil, err
   538  		}
   539  		defer unlock()
   540  
   541  		out, err := Run(ctx, r.dir, r.cmd.readFile(rev, file, r.remote))
   542  		if err != nil {
   543  			return nil, fs.ErrNotExist
   544  		}
   545  		return out, nil
   546  	})
   547  }
   548  
   549  func (r *vcsRepo) RecentTag(ctx context.Context, rev, prefix string, allowed func(string) bool) (tag string, err error) {
   550  	// Only lock for the subprocess execution, not for the tag scan.
   551  	// allowed may call other methods that acquire the lock.
   552  	unlock, err := r.mu.Lock()
   553  	if err != nil {
   554  		return "", err
   555  	}
   556  
   557  	if r.cmd.recentTags == nil {
   558  		unlock()
   559  		return "", vcsErrorf("vcs %s: RecentTag: %w", r.cmd.vcs, errors.ErrUnsupported)
   560  	}
   561  	out, err := Run(ctx, r.dir, r.cmd.recentTags(rev))
   562  	unlock()
   563  	if err != nil {
   564  		return "", err
   565  	}
   566  
   567  	highest := ""
   568  	for _, tag := range strings.Fields(string(out)) {
   569  		if !strings.HasPrefix(tag, prefix) || !allowed(tag) {
   570  			continue
   571  		}
   572  		semtag := tag[len(prefix):]
   573  		if semver.Compare(semtag, highest) > 0 {
   574  			highest = semtag
   575  		}
   576  	}
   577  	if highest != "" {
   578  		return prefix + highest, nil
   579  	}
   580  	return "", nil
   581  }
   582  
   583  func (r *vcsRepo) DescendsFrom(ctx context.Context, rev, tag string) (bool, error) {
   584  	unlock, err := r.mu.Lock()
   585  	if err != nil {
   586  		return false, err
   587  	}
   588  	defer unlock()
   589  
   590  	if r.cmd.descendsFrom == nil {
   591  		return false, vcsErrorf("vcs %s: DescendsFrom: %w", r.cmd.vcs, errors.ErrUnsupported)
   592  	}
   593  
   594  	out, err := Run(ctx, r.dir, r.cmd.descendsFrom(rev, tag))
   595  	if err != nil {
   596  		return false, err
   597  	}
   598  	return strings.TrimSpace(string(out)) != "", nil
   599  }
   600  
   601  func (r *vcsRepo) ReadZip(ctx context.Context, rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
   602  	if r.cmd.readZip == nil && r.cmd.doReadZip == nil {
   603  		return nil, vcsErrorf("vcs %s: ReadZip: %w", r.cmd.vcs, errors.ErrUnsupported)
   604  	}
   605  
   606  	if rev == "latest" {
   607  		rev = r.cmd.latest
   608  	}
   609  	_, err = r.Stat(ctx, rev) // download rev into local repo
   610  	if err != nil {
   611  		return nil, err
   612  	}
   613  
   614  	unlock, err := r.mu.Lock()
   615  	if err != nil {
   616  		return nil, err
   617  	}
   618  	defer unlock()
   619  
   620  	f, err := os.CreateTemp("", "go-readzip-*.zip")
   621  	if err != nil {
   622  		return nil, err
   623  	}
   624  	if r.cmd.doReadZip != nil {
   625  		lw := &limitedWriter{
   626  			W:               f,
   627  			N:               maxSize,
   628  			ErrLimitReached: errors.New("ReadZip: encoded file exceeds allowed size"),
   629  		}
   630  		err = r.cmd.doReadZip(ctx, lw, r.dir, rev, subdir, r.remote)
   631  		if err == nil {
   632  			_, err = f.Seek(0, io.SeekStart)
   633  		}
   634  	} else if r.cmd.vcs == "fossil" {
   635  		// If you run
   636  		//	fossil zip -R .fossil --name prefix trunk /tmp/x.zip
   637  		// fossil fails with "unable to create directory /tmp" [sic].
   638  		// Change the command to run in /tmp instead,
   639  		// replacing the -R argument with an absolute path.
   640  		args := r.cmd.readZip(rev, subdir, r.remote, filepath.Base(f.Name()))
   641  		for i := range args {
   642  			if args[i] == ".fossil" {
   643  				args[i] = filepath.Join(r.dir, ".fossil")
   644  			}
   645  		}
   646  		_, err = Run(ctx, filepath.Dir(f.Name()), args)
   647  	} else {
   648  		_, err = Run(ctx, r.dir, r.cmd.readZip(rev, subdir, r.remote, f.Name()))
   649  	}
   650  	if err != nil {
   651  		f.Close()
   652  		os.Remove(f.Name())
   653  		return nil, err
   654  	}
   655  	return &deleteCloser{f}, nil
   656  }
   657  
   658  // deleteCloser is a file that gets deleted on Close.
   659  type deleteCloser struct {
   660  	*os.File
   661  }
   662  
   663  func (d *deleteCloser) Close() error {
   664  	defer os.Remove(d.File.Name())
   665  	return d.File.Close()
   666  }
   667  
   668  func hgAddRemote(ctx context.Context, r *vcsRepo) error {
   669  	// Write .hg/hgrc with remote URL in it.
   670  	return os.WriteFile(filepath.Join(r.dir, ".hg/hgrc"), []byte(fmt.Sprintf("[paths]\ndefault = %s\n", r.remote)), 0666)
   671  }
   672  
   673  func hgParseStat(rev, out string) (*RevInfo, error) {
   674  	f := strings.Fields(out)
   675  	if len(f) < 3 {
   676  		return nil, vcsErrorf("unexpected response from hg log: %q", out)
   677  	}
   678  	hash := f[0]
   679  	version := rev
   680  	if strings.HasPrefix(hash, version) {
   681  		version = hash // extend to full hash
   682  	}
   683  	t, err := strconv.ParseInt(f[1], 10, 64)
   684  	if err != nil {
   685  		return nil, vcsErrorf("invalid time from hg log: %q", out)
   686  	}
   687  
   688  	var tags []string
   689  	for _, tag := range f[3:] {
   690  		if tag != "tip" {
   691  			tags = append(tags, tag)
   692  		}
   693  	}
   694  	sort.Strings(tags)
   695  
   696  	info := &RevInfo{
   697  		Origin:  &Origin{Hash: hash},
   698  		Name:    hash,
   699  		Short:   ShortenSHA1(hash),
   700  		Time:    time.Unix(t, 0).UTC(),
   701  		Version: version,
   702  		Tags:    tags,
   703  	}
   704  	return info, nil
   705  }
   706  
   707  func fossilParseStat(rev, out string) (*RevInfo, error) {
   708  	for line := range strings.SplitSeq(out, "\n") {
   709  		if strings.HasPrefix(line, "uuid:") || strings.HasPrefix(line, "hash:") {
   710  			f := strings.Fields(line)
   711  			if len(f) != 5 || len(f[1]) != 40 || f[4] != "UTC" {
   712  				return nil, vcsErrorf("unexpected response from fossil info: %q", line)
   713  			}
   714  			t, err := time.Parse(time.DateTime, f[2]+" "+f[3])
   715  			if err != nil {
   716  				return nil, vcsErrorf("unexpected response from fossil info: %q", line)
   717  			}
   718  			hash := f[1]
   719  			version := rev
   720  			if strings.HasPrefix(hash, version) {
   721  				version = hash // extend to full hash
   722  			}
   723  			info := &RevInfo{
   724  				Origin:  &Origin{Hash: hash},
   725  				Name:    hash,
   726  				Short:   ShortenSHA1(hash),
   727  				Time:    t,
   728  				Version: version,
   729  			}
   730  			return info, nil
   731  		}
   732  	}
   733  	return nil, vcsErrorf("unexpected response from fossil info: %q", out)
   734  }
   735  
   736  type limitedWriter struct {
   737  	W               io.Writer
   738  	N               int64
   739  	ErrLimitReached error
   740  }
   741  
   742  func (l *limitedWriter) Write(p []byte) (n int, err error) {
   743  	if l.N > 0 {
   744  		max := len(p)
   745  		if l.N < int64(max) {
   746  			max = int(l.N)
   747  		}
   748  		n, err = l.W.Write(p[:max])
   749  		l.N -= int64(n)
   750  		if err != nil || n >= len(p) {
   751  			return n, err
   752  		}
   753  	}
   754  
   755  	return n, l.ErrLimitReached
   756  }
   757  

View as plain text