github.com/gagliardetto/golang-go@v0.0.0-20201020153340-53909ea70814/cmd/go/not-internal/modfetch/codehost/vcs.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package codehost
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"github.com/gagliardetto/golang-go/not-internal/lazyregexp"
    11  	"io"
    12  	"io/ioutil"
    13  	"os"
    14  	"path/filepath"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"time"
    20  
    21  	"github.com/gagliardetto/golang-go/cmd/go/not-internal/lockedfile"
    22  	"github.com/gagliardetto/golang-go/cmd/go/not-internal/par"
    23  	"github.com/gagliardetto/golang-go/cmd/go/not-internal/str"
    24  )
    25  
    26  // A VCSError indicates an error using a version control system.
    27  // The implication of a VCSError is that we know definitively where
    28  // to get the code, but we can't access it due to the error.
    29  // The caller should report this error instead of continuing to probe
    30  // other possible module paths.
    31  //
    32  // TODO(golang.org/issue/31730): See if we can invert this. (Return a
    33  // distinguished error for “repo not found” and treat everything else
    34  // as terminal.)
    35  type VCSError struct {
    36  	Err error
    37  }
    38  
    39  func (e *VCSError) Error() string { return e.Err.Error() }
    40  
    41  func vcsErrorf(format string, a ...interface{}) error {
    42  	return &VCSError{Err: fmt.Errorf(format, a...)}
    43  }
    44  
    45  func NewRepo(vcs, remote string) (Repo, error) {
    46  	type key struct {
    47  		vcs    string
    48  		remote string
    49  	}
    50  	type cached struct {
    51  		repo Repo
    52  		err  error
    53  	}
    54  	c := vcsRepoCache.Do(key{vcs, remote}, func() interface{} {
    55  		repo, err := newVCSRepo(vcs, remote)
    56  		if err != nil {
    57  			err = &VCSError{err}
    58  		}
    59  		return cached{repo, err}
    60  	}).(cached)
    61  
    62  	return c.repo, c.err
    63  }
    64  
    65  var vcsRepoCache par.Cache
    66  
    67  type vcsRepo struct {
    68  	mu lockedfile.Mutex // protects all commands, so we don't have to decide which are safe on a per-VCS basis
    69  
    70  	remote string
    71  	cmd    *vcsCmd
    72  	dir    string
    73  
    74  	tagsOnce sync.Once
    75  	tags     map[string]bool
    76  
    77  	branchesOnce sync.Once
    78  	branches     map[string]bool
    79  
    80  	fetchOnce sync.Once
    81  	fetchErr  error
    82  }
    83  
    84  func newVCSRepo(vcs, remote string) (Repo, error) {
    85  	if vcs == "git" {
    86  		return newGitRepo(remote, false)
    87  	}
    88  	cmd := vcsCmds[vcs]
    89  	if cmd == nil {
    90  		return nil, fmt.Errorf("unknown vcs: %s %s", vcs, remote)
    91  	}
    92  	if !strings.Contains(remote, "://") {
    93  		return nil, fmt.Errorf("invalid vcs remote: %s %s", vcs, remote)
    94  	}
    95  
    96  	r := &vcsRepo{remote: remote, cmd: cmd}
    97  	var err error
    98  	r.dir, r.mu.Path, err = WorkDir(vcsWorkDirType+vcs, r.remote)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	if cmd.init == nil {
   104  		return r, nil
   105  	}
   106  
   107  	unlock, err := r.mu.Lock()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	defer unlock()
   112  
   113  	if _, err := os.Stat(filepath.Join(r.dir, "."+vcs)); err != nil {
   114  		if _, err := Run(r.dir, cmd.init(r.remote)); err != nil {
   115  			os.RemoveAll(r.dir)
   116  			return nil, err
   117  		}
   118  	}
   119  	return r, nil
   120  }
   121  
   122  const vcsWorkDirType = "vcs1."
   123  
   124  type vcsCmd struct {
   125  	vcs           string                                                         // vcs name "hg"
   126  	init          func(remote string) []string                                   // cmd to init repo to track remote
   127  	tags          func(remote string) []string                                   // cmd to list local tags
   128  	tagRE         *lazyregexp.Regexp                                             // regexp to extract tag names from output of tags cmd
   129  	branches      func(remote string) []string                                   // cmd to list local branches
   130  	branchRE      *lazyregexp.Regexp                                             // regexp to extract branch names from output of tags cmd
   131  	badLocalRevRE *lazyregexp.Regexp                                             // regexp of names that must not be served out of local cache without doing fetch first
   132  	statLocal     func(rev, remote string) []string                              // cmd to stat local rev
   133  	parseStat     func(rev, out string) (*RevInfo, error)                        // cmd to parse output of statLocal
   134  	fetch         []string                                                       // cmd to fetch everything from remote
   135  	latest        string                                                         // name of latest commit on remote (tip, HEAD, etc)
   136  	readFile      func(rev, file, remote string) []string                        // cmd to read rev's file
   137  	readZip       func(rev, subdir, remote, target string) []string              // cmd to read rev's subdir as zip file
   138  	doReadZip     func(dst io.Writer, workDir, rev, subdir, remote string) error // arbitrary function to read rev's subdir as zip file
   139  }
   140  
   141  var re = lazyregexp.New
   142  
   143  var vcsCmds = map[string]*vcsCmd{
   144  	"hg": {
   145  		vcs: "hg",
   146  		init: func(remote string) []string {
   147  			return []string{"hg", "clone", "-U", "--", remote, "."}
   148  		},
   149  		tags: func(remote string) []string {
   150  			return []string{"hg", "tags", "-q"}
   151  		},
   152  		tagRE: re(`(?m)^[^\n]+$`),
   153  		branches: func(remote string) []string {
   154  			return []string{"hg", "branches", "-c", "-q"}
   155  		},
   156  		branchRE:      re(`(?m)^[^\n]+$`),
   157  		badLocalRevRE: re(`(?m)^(tip)$`),
   158  		statLocal: func(rev, remote string) []string {
   159  			return []string{"hg", "log", "-l1", "-r", rev, "--template", "{node} {date|hgdate} {tags}"}
   160  		},
   161  		parseStat: hgParseStat,
   162  		fetch:     []string{"hg", "pull", "-f"},
   163  		latest:    "tip",
   164  		readFile: func(rev, file, remote string) []string {
   165  			return []string{"hg", "cat", "-r", rev, file}
   166  		},
   167  		readZip: func(rev, subdir, remote, target string) []string {
   168  			pattern := []string{}
   169  			if subdir != "" {
   170  				pattern = []string{"-I", subdir + "/**"}
   171  			}
   172  			return str.StringList("hg", "archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/", pattern, "--", target)
   173  		},
   174  	},
   175  
   176  	"svn": {
   177  		vcs:  "svn",
   178  		init: nil, // no local checkout
   179  		tags: func(remote string) []string {
   180  			return []string{"svn", "list", "--", strings.TrimSuffix(remote, "/trunk") + "/tags"}
   181  		},
   182  		tagRE: re(`(?m)^(.*?)/?$`),
   183  		statLocal: func(rev, remote string) []string {
   184  			suffix := "@" + rev
   185  			if rev == "latest" {
   186  				suffix = ""
   187  			}
   188  			return []string{"svn", "log", "-l1", "--xml", "--", remote + suffix}
   189  		},
   190  		parseStat: svnParseStat,
   191  		latest:    "latest",
   192  		readFile: func(rev, file, remote string) []string {
   193  			return []string{"svn", "cat", "--", remote + "/" + file + "@" + rev}
   194  		},
   195  		doReadZip: svnReadZip,
   196  	},
   197  
   198  	"bzr": {
   199  		vcs: "bzr",
   200  		init: func(remote string) []string {
   201  			return []string{"bzr", "branch", "--use-existing-dir", "--", remote, "."}
   202  		},
   203  		fetch: []string{
   204  			"bzr", "pull", "--overwrite-tags",
   205  		},
   206  		tags: func(remote string) []string {
   207  			return []string{"bzr", "tags"}
   208  		},
   209  		tagRE:         re(`(?m)^\S+`),
   210  		badLocalRevRE: re(`^revno:-`),
   211  		statLocal: func(rev, remote string) []string {
   212  			return []string{"bzr", "log", "-l1", "--long", "--show-ids", "-r", rev}
   213  		},
   214  		parseStat: bzrParseStat,
   215  		latest:    "revno:-1",
   216  		readFile: func(rev, file, remote string) []string {
   217  			return []string{"bzr", "cat", "-r", rev, file}
   218  		},
   219  		readZip: func(rev, subdir, remote, target string) []string {
   220  			extra := []string{}
   221  			if subdir != "" {
   222  				extra = []string{"./" + subdir}
   223  			}
   224  			return str.StringList("bzr", "export", "--format=zip", "-r", rev, "--root=prefix/", "--", target, extra)
   225  		},
   226  	},
   227  
   228  	"fossil": {
   229  		vcs: "fossil",
   230  		init: func(remote string) []string {
   231  			return []string{"fossil", "clone", "--", remote, ".fossil"}
   232  		},
   233  		fetch: []string{"fossil", "pull", "-R", ".fossil"},
   234  		tags: func(remote string) []string {
   235  			return []string{"fossil", "tag", "-R", ".fossil", "list"}
   236  		},
   237  		tagRE: re(`XXXTODO`),
   238  		statLocal: func(rev, remote string) []string {
   239  			return []string{"fossil", "info", "-R", ".fossil", rev}
   240  		},
   241  		parseStat: fossilParseStat,
   242  		latest:    "trunk",
   243  		readFile: func(rev, file, remote string) []string {
   244  			return []string{"fossil", "cat", "-R", ".fossil", "-r", rev, file}
   245  		},
   246  		readZip: func(rev, subdir, remote, target string) []string {
   247  			extra := []string{}
   248  			if subdir != "" && !strings.ContainsAny(subdir, "*?[],") {
   249  				extra = []string{"--include", subdir}
   250  			}
   251  			// Note that vcsRepo.ReadZip below rewrites this command
   252  			// to run in a different directory, to work around a fossil bug.
   253  			return str.StringList("fossil", "zip", "-R", ".fossil", "--name", "prefix", extra, "--", rev, target)
   254  		},
   255  	},
   256  }
   257  
   258  func (r *vcsRepo) loadTags() {
   259  	out, err := Run(r.dir, r.cmd.tags(r.remote))
   260  	if err != nil {
   261  		return
   262  	}
   263  
   264  	// Run tag-listing command and extract tags.
   265  	r.tags = make(map[string]bool)
   266  	for _, tag := range r.cmd.tagRE.FindAllString(string(out), -1) {
   267  		if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(tag) {
   268  			continue
   269  		}
   270  		r.tags[tag] = true
   271  	}
   272  }
   273  
   274  func (r *vcsRepo) loadBranches() {
   275  	if r.cmd.branches == nil {
   276  		return
   277  	}
   278  
   279  	out, err := Run(r.dir, r.cmd.branches(r.remote))
   280  	if err != nil {
   281  		return
   282  	}
   283  
   284  	r.branches = make(map[string]bool)
   285  	for _, branch := range r.cmd.branchRE.FindAllString(string(out), -1) {
   286  		if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(branch) {
   287  			continue
   288  		}
   289  		r.branches[branch] = true
   290  	}
   291  }
   292  
   293  func (r *vcsRepo) Tags(prefix string) ([]string, error) {
   294  	unlock, err := r.mu.Lock()
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  	defer unlock()
   299  
   300  	r.tagsOnce.Do(r.loadTags)
   301  
   302  	tags := []string{}
   303  	for tag := range r.tags {
   304  		if strings.HasPrefix(tag, prefix) {
   305  			tags = append(tags, tag)
   306  		}
   307  	}
   308  	sort.Strings(tags)
   309  	return tags, nil
   310  }
   311  
   312  func (r *vcsRepo) Stat(rev string) (*RevInfo, error) {
   313  	unlock, err := r.mu.Lock()
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  	defer unlock()
   318  
   319  	if rev == "latest" {
   320  		rev = r.cmd.latest
   321  	}
   322  	r.branchesOnce.Do(r.loadBranches)
   323  	revOK := (r.cmd.badLocalRevRE == nil || !r.cmd.badLocalRevRE.MatchString(rev)) && !r.branches[rev]
   324  	if revOK {
   325  		if info, err := r.statLocal(rev); err == nil {
   326  			return info, nil
   327  		}
   328  	}
   329  
   330  	r.fetchOnce.Do(r.fetch)
   331  	if r.fetchErr != nil {
   332  		return nil, r.fetchErr
   333  	}
   334  	info, err := r.statLocal(rev)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  	if !revOK {
   339  		info.Version = info.Name
   340  	}
   341  	return info, nil
   342  }
   343  
   344  func (r *vcsRepo) fetch() {
   345  	if len(r.cmd.fetch) > 0 {
   346  		_, r.fetchErr = Run(r.dir, r.cmd.fetch)
   347  	}
   348  }
   349  
   350  func (r *vcsRepo) statLocal(rev string) (*RevInfo, error) {
   351  	out, err := Run(r.dir, r.cmd.statLocal(rev, r.remote))
   352  	if err != nil {
   353  		return nil, &UnknownRevisionError{Rev: rev}
   354  	}
   355  	return r.cmd.parseStat(rev, string(out))
   356  }
   357  
   358  func (r *vcsRepo) Latest() (*RevInfo, error) {
   359  	return r.Stat("latest")
   360  }
   361  
   362  func (r *vcsRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
   363  	if rev == "latest" {
   364  		rev = r.cmd.latest
   365  	}
   366  	_, err := r.Stat(rev) // download rev into local repo
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	// r.Stat acquires r.mu, so lock after that.
   372  	unlock, err := r.mu.Lock()
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	defer unlock()
   377  
   378  	out, err := Run(r.dir, r.cmd.readFile(rev, file, r.remote))
   379  	if err != nil {
   380  		return nil, os.ErrNotExist
   381  	}
   382  	return out, nil
   383  }
   384  
   385  func (r *vcsRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[string]*FileRev, error) {
   386  	// We don't technically need to lock here since we're returning an error
   387  	// uncondititonally, but doing so anyway will help to avoid baking in
   388  	// lock-inversion bugs.
   389  	unlock, err := r.mu.Lock()
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	defer unlock()
   394  
   395  	return nil, vcsErrorf("ReadFileRevs not implemented")
   396  }
   397  
   398  func (r *vcsRepo) RecentTag(rev, prefix, major string) (tag string, err error) {
   399  	// We don't technically need to lock here since we're returning an error
   400  	// uncondititonally, but doing so anyway will help to avoid baking in
   401  	// lock-inversion bugs.
   402  	unlock, err := r.mu.Lock()
   403  	if err != nil {
   404  		return "", err
   405  	}
   406  	defer unlock()
   407  
   408  	return "", vcsErrorf("RecentTag not implemented")
   409  }
   410  
   411  func (r *vcsRepo) DescendsFrom(rev, tag string) (bool, error) {
   412  	unlock, err := r.mu.Lock()
   413  	if err != nil {
   414  		return false, err
   415  	}
   416  	defer unlock()
   417  
   418  	return false, vcsErrorf("DescendsFrom not implemented")
   419  }
   420  
   421  func (r *vcsRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
   422  	if r.cmd.readZip == nil && r.cmd.doReadZip == nil {
   423  		return nil, vcsErrorf("ReadZip not implemented for %s", r.cmd.vcs)
   424  	}
   425  
   426  	unlock, err := r.mu.Lock()
   427  	if err != nil {
   428  		return nil, err
   429  	}
   430  	defer unlock()
   431  
   432  	if rev == "latest" {
   433  		rev = r.cmd.latest
   434  	}
   435  	f, err := ioutil.TempFile("", "go-readzip-*.zip")
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  	if r.cmd.doReadZip != nil {
   440  		lw := &limitedWriter{
   441  			W:               f,
   442  			N:               maxSize,
   443  			ErrLimitReached: errors.New("ReadZip: encoded file exceeds allowed size"),
   444  		}
   445  		err = r.cmd.doReadZip(lw, r.dir, rev, subdir, r.remote)
   446  		if err == nil {
   447  			_, err = f.Seek(0, io.SeekStart)
   448  		}
   449  	} else if r.cmd.vcs == "fossil" {
   450  		// If you run
   451  		//	fossil zip -R .fossil --name prefix trunk /tmp/x.zip
   452  		// fossil fails with "unable to create directory /tmp" [sic].
   453  		// Change the command to run in /tmp instead,
   454  		// replacing the -R argument with an absolute path.
   455  		args := r.cmd.readZip(rev, subdir, r.remote, filepath.Base(f.Name()))
   456  		for i := range args {
   457  			if args[i] == ".fossil" {
   458  				args[i] = filepath.Join(r.dir, ".fossil")
   459  			}
   460  		}
   461  		_, err = Run(filepath.Dir(f.Name()), args)
   462  	} else {
   463  		_, err = Run(r.dir, r.cmd.readZip(rev, subdir, r.remote, f.Name()))
   464  	}
   465  	if err != nil {
   466  		f.Close()
   467  		os.Remove(f.Name())
   468  		return nil, err
   469  	}
   470  	return &deleteCloser{f}, nil
   471  }
   472  
   473  // deleteCloser is a file that gets deleted on Close.
   474  type deleteCloser struct {
   475  	*os.File
   476  }
   477  
   478  func (d *deleteCloser) Close() error {
   479  	defer os.Remove(d.File.Name())
   480  	return d.File.Close()
   481  }
   482  
   483  func hgParseStat(rev, out string) (*RevInfo, error) {
   484  	f := strings.Fields(string(out))
   485  	if len(f) < 3 {
   486  		return nil, vcsErrorf("unexpected response from hg log: %q", out)
   487  	}
   488  	hash := f[0]
   489  	version := rev
   490  	if strings.HasPrefix(hash, version) {
   491  		version = hash // extend to full hash
   492  	}
   493  	t, err := strconv.ParseInt(f[1], 10, 64)
   494  	if err != nil {
   495  		return nil, vcsErrorf("invalid time from hg log: %q", out)
   496  	}
   497  
   498  	var tags []string
   499  	for _, tag := range f[3:] {
   500  		if tag != "tip" {
   501  			tags = append(tags, tag)
   502  		}
   503  	}
   504  	sort.Strings(tags)
   505  
   506  	info := &RevInfo{
   507  		Name:    hash,
   508  		Short:   ShortenSHA1(hash),
   509  		Time:    time.Unix(t, 0).UTC(),
   510  		Version: version,
   511  		Tags:    tags,
   512  	}
   513  	return info, nil
   514  }
   515  
   516  func bzrParseStat(rev, out string) (*RevInfo, error) {
   517  	var revno int64
   518  	var tm time.Time
   519  	for _, line := range strings.Split(out, "\n") {
   520  		if line == "" || line[0] == ' ' || line[0] == '\t' {
   521  			// End of header, start of commit message.
   522  			break
   523  		}
   524  		if line[0] == '-' {
   525  			continue
   526  		}
   527  		i := strings.Index(line, ":")
   528  		if i < 0 {
   529  			// End of header, start of commit message.
   530  			break
   531  		}
   532  		key, val := line[:i], strings.TrimSpace(line[i+1:])
   533  		switch key {
   534  		case "revno":
   535  			if j := strings.Index(val, " "); j >= 0 {
   536  				val = val[:j]
   537  			}
   538  			i, err := strconv.ParseInt(val, 10, 64)
   539  			if err != nil {
   540  				return nil, vcsErrorf("unexpected revno from bzr log: %q", line)
   541  			}
   542  			revno = i
   543  		case "timestamp":
   544  			j := strings.Index(val, " ")
   545  			if j < 0 {
   546  				return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
   547  			}
   548  			t, err := time.Parse("2006-01-02 15:04:05 -0700", val[j+1:])
   549  			if err != nil {
   550  				return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
   551  			}
   552  			tm = t.UTC()
   553  		}
   554  	}
   555  	if revno == 0 || tm.IsZero() {
   556  		return nil, vcsErrorf("unexpected response from bzr log: %q", out)
   557  	}
   558  
   559  	info := &RevInfo{
   560  		Name:    fmt.Sprintf("%d", revno),
   561  		Short:   fmt.Sprintf("%012d", revno),
   562  		Time:    tm,
   563  		Version: rev,
   564  	}
   565  	return info, nil
   566  }
   567  
   568  func fossilParseStat(rev, out string) (*RevInfo, error) {
   569  	for _, line := range strings.Split(out, "\n") {
   570  		if strings.HasPrefix(line, "uuid:") {
   571  			f := strings.Fields(line)
   572  			if len(f) != 5 || len(f[1]) != 40 || f[4] != "UTC" {
   573  				return nil, vcsErrorf("unexpected response from fossil info: %q", line)
   574  			}
   575  			t, err := time.Parse("2006-01-02 15:04:05", f[2]+" "+f[3])
   576  			if err != nil {
   577  				return nil, vcsErrorf("unexpected response from fossil info: %q", line)
   578  			}
   579  			hash := f[1]
   580  			version := rev
   581  			if strings.HasPrefix(hash, version) {
   582  				version = hash // extend to full hash
   583  			}
   584  			info := &RevInfo{
   585  				Name:    hash,
   586  				Short:   ShortenSHA1(hash),
   587  				Time:    t,
   588  				Version: version,
   589  			}
   590  			return info, nil
   591  		}
   592  	}
   593  	return nil, vcsErrorf("unexpected response from fossil info: %q", out)
   594  }
   595  
   596  type limitedWriter struct {
   597  	W               io.Writer
   598  	N               int64
   599  	ErrLimitReached error
   600  }
   601  
   602  func (l *limitedWriter) Write(p []byte) (n int, err error) {
   603  	if l.N > 0 {
   604  		max := len(p)
   605  		if l.N < int64(max) {
   606  			max = int(l.N)
   607  		}
   608  		n, err = l.W.Write(p[:max])
   609  		l.N -= int64(n)
   610  		if err != nil || n >= len(p) {
   611  			return n, err
   612  		}
   613  	}
   614  
   615  	return n, l.ErrLimitReached
   616  }