github.com/sdboyer/gps@v0.16.3/vcs_source.go (about)

     1  package gps
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/Masterminds/semver"
    14  	"github.com/sdboyer/gps/internal/fs"
    15  	"github.com/sdboyer/gps/pkgtree"
    16  )
    17  
    18  type baseVCSSource struct {
    19  	repo ctxRepo
    20  }
    21  
    22  func (bs *baseVCSSource) sourceType() string {
    23  	return string(bs.repo.Vcs())
    24  }
    25  
    26  func (bs *baseVCSSource) existsLocally(ctx context.Context) bool {
    27  	return bs.repo.CheckLocal()
    28  }
    29  
    30  // TODO reimpl for git
    31  func (bs *baseVCSSource) existsUpstream(ctx context.Context) bool {
    32  	return !bs.repo.Ping()
    33  }
    34  
    35  func (bs *baseVCSSource) upstreamURL() string {
    36  	return bs.repo.Remote()
    37  }
    38  
    39  func (bs *baseVCSSource) getManifestAndLock(ctx context.Context, pr ProjectRoot, r Revision, an ProjectAnalyzer) (Manifest, Lock, error) {
    40  	err := bs.repo.updateVersion(ctx, r.String())
    41  	if err != nil {
    42  		return nil, nil, unwrapVcsErr(err)
    43  	}
    44  
    45  	m, l, err := an.DeriveManifestAndLock(bs.repo.LocalPath(), pr)
    46  	if err != nil {
    47  		return nil, nil, err
    48  	}
    49  
    50  	if l != nil && l != Lock(nil) {
    51  		l = prepLock(l)
    52  	}
    53  
    54  	return prepManifest(m), l, nil
    55  }
    56  
    57  func (bs *baseVCSSource) revisionPresentIn(r Revision) (bool, error) {
    58  	return bs.repo.IsReference(string(r)), nil
    59  }
    60  
    61  // initLocal clones/checks out the upstream repository to disk for the first
    62  // time.
    63  func (bs *baseVCSSource) initLocal(ctx context.Context) error {
    64  	err := bs.repo.get(ctx)
    65  
    66  	if err != nil {
    67  		return unwrapVcsErr(err)
    68  	}
    69  	return nil
    70  }
    71  
    72  // updateLocal ensures the local data (versions and code) we have about the
    73  // source is fully up to date with that of the canonical upstream source.
    74  func (bs *baseVCSSource) updateLocal(ctx context.Context) error {
    75  	err := bs.repo.fetch(ctx)
    76  
    77  	if err != nil {
    78  		return unwrapVcsErr(err)
    79  	}
    80  	return nil
    81  }
    82  
    83  func (bs *baseVCSSource) listPackages(ctx context.Context, pr ProjectRoot, r Revision) (ptree pkgtree.PackageTree, err error) {
    84  	err = bs.repo.updateVersion(ctx, r.String())
    85  
    86  	if err != nil {
    87  		err = unwrapVcsErr(err)
    88  	} else {
    89  		ptree, err = pkgtree.ListPackages(bs.repo.LocalPath(), string(pr))
    90  	}
    91  
    92  	return
    93  }
    94  
    95  func (bs *baseVCSSource) exportRevisionTo(ctx context.Context, r Revision, to string) error {
    96  	// Only make the parent dir, as CopyDir will balk on trying to write to an
    97  	// empty but existing dir.
    98  	if err := os.MkdirAll(filepath.Dir(to), 0777); err != nil {
    99  		return err
   100  	}
   101  
   102  	if err := bs.repo.updateVersion(ctx, r.String()); err != nil {
   103  		return unwrapVcsErr(err)
   104  	}
   105  
   106  	// TODO(sdboyer) this is a simplistic approach and relying on the tools
   107  	// themselves might make it faster, but git's the overwhelming case (and has
   108  	// its own method) so fine for now
   109  	return fs.CopyDir(bs.repo.LocalPath(), to)
   110  }
   111  
   112  // gitSource is a generic git repository implementation that should work with
   113  // all standard git remotes.
   114  type gitSource struct {
   115  	baseVCSSource
   116  }
   117  
   118  func (s *gitSource) exportRevisionTo(ctx context.Context, rev Revision, to string) error {
   119  	r := s.repo
   120  
   121  	if err := os.MkdirAll(to, 0777); err != nil {
   122  		return err
   123  	}
   124  
   125  	// Back up original index
   126  	idx, bak := filepath.Join(r.LocalPath(), ".git", "index"), filepath.Join(r.LocalPath(), ".git", "origindex")
   127  	err := fs.RenameWithFallback(idx, bak)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	// could have an err here...but it's hard to imagine how?
   133  	defer fs.RenameWithFallback(bak, idx)
   134  
   135  	out, err := runFromRepoDir(ctx, r, "git", "read-tree", rev.String())
   136  	if err != nil {
   137  		return fmt.Errorf("%s: %s", out, err)
   138  	}
   139  
   140  	// Ensure we have exactly one trailing slash
   141  	to = strings.TrimSuffix(to, string(os.PathSeparator)) + string(os.PathSeparator)
   142  	// Checkout from our temporary index to the desired target location on
   143  	// disk; now it's git's job to make it fast.
   144  	//
   145  	// Sadly, this approach *does* also write out vendor dirs. There doesn't
   146  	// appear to be a way to make checkout-index respect sparse checkout
   147  	// rules (-a supercedes it). The alternative is using plain checkout,
   148  	// though we have a bunch of housekeeping to do to set up, then tear
   149  	// down, the sparse checkout controls, as well as restore the original
   150  	// index and HEAD.
   151  	out, err = runFromRepoDir(ctx, r, "git", "checkout-index", "-a", "--prefix="+to)
   152  	if err != nil {
   153  		return fmt.Errorf("%s: %s", out, err)
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  func (s *gitSource) listVersions(ctx context.Context) (vlist []PairedVersion, err error) {
   160  	r := s.repo
   161  
   162  	var out []byte
   163  	c := newMonitoredCmd(exec.Command("git", "ls-remote", r.Remote()), 30*time.Second)
   164  	// Ensure no prompting for PWs
   165  	c.cmd.Env = mergeEnvLists([]string{"GIT_ASKPASS=", "GIT_TERMINAL_PROMPT=0"}, os.Environ())
   166  	out, err = c.combinedOutput(ctx)
   167  
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	all := bytes.Split(bytes.TrimSpace(out), []byte("\n"))
   173  	if len(all) == 1 && len(all[0]) == 0 {
   174  		return nil, fmt.Errorf("no data returned from ls-remote")
   175  	}
   176  
   177  	// Pull out the HEAD rev (it's always first) so we know what branches to
   178  	// mark as default. This is, perhaps, not the best way to glean this, but it
   179  	// was good enough for git itself until 1.8.5. Also, the alternative is
   180  	// sniffing data out of the pack protocol, which is a separate request, and
   181  	// also waaaay more than we want to do right now.
   182  	//
   183  	// The cost is that we could potentially have multiple branches marked as
   184  	// the default. If that does occur, a later check (again, emulating git
   185  	// <1.8.5 behavior) further narrows the failure mode by choosing master as
   186  	// the sole default branch if a) master exists and b) master is one of the
   187  	// branches marked as a default.
   188  	//
   189  	// This all reduces the failure mode to a very narrow range of
   190  	// circumstances. Nevertheless, if we do end up emitting multiple
   191  	// default branches, it is possible that a user could end up following a
   192  	// non-default branch, IF:
   193  	//
   194  	// * Multiple branches match the HEAD rev
   195  	// * None of them are master
   196  	// * The solver makes it into the branch list in the version queue
   197  	// * The user/tool has provided no constraint (so, anyConstraint)
   198  	// * A branch that is not actually the default, but happens to share the
   199  	//   rev, is lexicographically less than the true default branch
   200  	//
   201  	// If all of those conditions are met, then the user would end up with an
   202  	// erroneous non-default branch in their lock file.
   203  	headrev := Revision(all[0][:40])
   204  	var onedef, multidef, defmaster bool
   205  
   206  	smap := make(map[string]bool)
   207  	uniq := 0
   208  	vlist = make([]PairedVersion, len(all)-1) // less 1, because always ignore HEAD
   209  	for _, pair := range all {
   210  		var v PairedVersion
   211  		if string(pair[46:51]) == "heads" {
   212  			rev := Revision(pair[:40])
   213  
   214  			isdef := rev == headrev
   215  			n := string(pair[52:])
   216  			if isdef {
   217  				if onedef {
   218  					multidef = true
   219  				}
   220  				onedef = true
   221  				if n == "master" {
   222  					defmaster = true
   223  				}
   224  			}
   225  			v = branchVersion{
   226  				name:      n,
   227  				isDefault: isdef,
   228  			}.Is(rev).(PairedVersion)
   229  
   230  			vlist[uniq] = v
   231  			uniq++
   232  		} else if string(pair[46:50]) == "tags" {
   233  			vstr := string(pair[51:])
   234  			if strings.HasSuffix(vstr, "^{}") {
   235  				// If the suffix is there, then we *know* this is the rev of
   236  				// the underlying commit object that we actually want
   237  				vstr = strings.TrimSuffix(vstr, "^{}")
   238  			} else if smap[vstr] {
   239  				// Already saw the deref'd version of this tag, if one
   240  				// exists, so skip this.
   241  				continue
   242  				// Can only hit this branch if we somehow got the deref'd
   243  				// version first. Which should be impossible, but this
   244  				// covers us in case of weirdness, anyway.
   245  			}
   246  			v = NewVersion(vstr).Is(Revision(pair[:40])).(PairedVersion)
   247  			smap[vstr] = true
   248  			vlist[uniq] = v
   249  			uniq++
   250  		}
   251  	}
   252  
   253  	// Trim off excess from the slice
   254  	vlist = vlist[:uniq]
   255  
   256  	// There were multiple default branches, but one was master. So, go through
   257  	// and strip the default flag from all the non-master branches.
   258  	if multidef && defmaster {
   259  		for k, v := range vlist {
   260  			pv := v.(PairedVersion)
   261  			if bv, ok := pv.Unpair().(branchVersion); ok {
   262  				if bv.name != "master" && bv.isDefault == true {
   263  					bv.isDefault = false
   264  					vlist[k] = bv.Is(pv.Underlying())
   265  				}
   266  			}
   267  		}
   268  	}
   269  
   270  	return
   271  }
   272  
   273  // gopkginSource is a specialized git source that performs additional filtering
   274  // according to the input URL.
   275  type gopkginSource struct {
   276  	gitSource
   277  	major uint64
   278  }
   279  
   280  func (s *gopkginSource) listVersions(ctx context.Context) ([]PairedVersion, error) {
   281  	ovlist, err := s.gitSource.listVersions(ctx)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	// Apply gopkg.in's filtering rules
   287  	vlist := make([]PairedVersion, len(ovlist))
   288  	k := 0
   289  	var dbranch int // index of branch to be marked default
   290  	var bsv *semver.Version
   291  	for _, v := range ovlist {
   292  		// all git versions will always be paired
   293  		pv := v.(versionPair)
   294  		switch tv := pv.v.(type) {
   295  		case semVersion:
   296  			if tv.sv.Major() == s.major {
   297  				vlist[k] = v
   298  				k++
   299  			}
   300  		case branchVersion:
   301  			// The semver lib isn't exactly the same as gopkg.in's logic, but
   302  			// it's close enough that it's probably fine to use. We can be more
   303  			// exact if real problems crop up. The most obvious vector for
   304  			// problems is that we totally ignore the "unstable" designation
   305  			// right now.
   306  			sv, err := semver.NewVersion(tv.name)
   307  			if err != nil || sv.Major() != s.major {
   308  				// not a semver-shaped branch name at all, or not the same major
   309  				// version as specified in the import path constraint
   310  				continue
   311  			}
   312  
   313  			// Turn off the default branch marker unconditionally; we can't know
   314  			// which one to mark as default until we've seen them all
   315  			tv.isDefault = false
   316  			// Figure out if this is the current leader for default branch
   317  			if bsv == nil || bsv.LessThan(sv) {
   318  				bsv = sv
   319  				dbranch = k
   320  			}
   321  			pv.v = tv
   322  			vlist[k] = pv
   323  			k++
   324  		}
   325  		// The switch skips plainVersions because they cannot possibly meet
   326  		// gopkg.in's requirements
   327  	}
   328  
   329  	vlist = vlist[:k]
   330  	if bsv != nil {
   331  		dbv := vlist[dbranch].(versionPair)
   332  		vlist[dbranch] = branchVersion{
   333  			name:      dbv.v.(branchVersion).name,
   334  			isDefault: true,
   335  		}.Is(dbv.r)
   336  	}
   337  
   338  	return vlist, nil
   339  }
   340  
   341  // bzrSource is a generic bzr repository implementation that should work with
   342  // all standard bazaar remotes.
   343  type bzrSource struct {
   344  	baseVCSSource
   345  }
   346  
   347  func (s *bzrSource) listVersions(ctx context.Context) ([]PairedVersion, error) {
   348  	r := s.repo
   349  
   350  	// Now, list all the tags
   351  	out, err := runFromRepoDir(ctx, r, "bzr", "tags", "--show-ids", "-v")
   352  	if err != nil {
   353  		return nil, fmt.Errorf("%s: %s", err, string(out))
   354  	}
   355  
   356  	all := bytes.Split(bytes.TrimSpace(out), []byte("\n"))
   357  
   358  	var branchrev []byte
   359  	branchrev, err = runFromRepoDir(ctx, r, "bzr", "version-info", "--custom", "--template={revision_id}", "--revision=branch:.")
   360  	br := string(branchrev)
   361  	if err != nil {
   362  		return nil, fmt.Errorf("%s: %s", err, br)
   363  	}
   364  
   365  	vlist := make([]PairedVersion, 0, len(all)+1)
   366  
   367  	// Now, all the tags.
   368  	for _, line := range all {
   369  		idx := bytes.IndexByte(line, 32) // space
   370  		v := NewVersion(string(line[:idx]))
   371  		r := Revision(bytes.TrimSpace(line[idx:]))
   372  		vlist = append(vlist, v.Is(r))
   373  	}
   374  
   375  	// Last, add the default branch, hardcoding the visual representation of it
   376  	// that bzr uses when operating in the workflow mode we're using.
   377  	v := newDefaultBranch("(default)")
   378  	vlist = append(vlist, v.Is(Revision(string(branchrev))))
   379  
   380  	return vlist, nil
   381  }
   382  
   383  // hgSource is a generic hg repository implementation that should work with
   384  // all standard mercurial servers.
   385  type hgSource struct {
   386  	baseVCSSource
   387  }
   388  
   389  func (s *hgSource) listVersions(ctx context.Context) ([]PairedVersion, error) {
   390  	var vlist []PairedVersion
   391  
   392  	r := s.repo
   393  	// Now, list all the tags
   394  	out, err := runFromRepoDir(ctx, r, "hg", "tags", "--debug", "--verbose")
   395  	if err != nil {
   396  		return nil, fmt.Errorf("%s: %s", err, string(out))
   397  	}
   398  
   399  	all := bytes.Split(bytes.TrimSpace(out), []byte("\n"))
   400  	lbyt := []byte("local")
   401  	nulrev := []byte("0000000000000000000000000000000000000000")
   402  	for _, line := range all {
   403  		if bytes.Equal(lbyt, line[len(line)-len(lbyt):]) {
   404  			// Skip local tags
   405  			continue
   406  		}
   407  
   408  		// tip is magic, don't include it
   409  		if bytes.HasPrefix(line, []byte("tip")) {
   410  			continue
   411  		}
   412  
   413  		// Split on colon; this gets us the rev and the tag plus local revno
   414  		pair := bytes.Split(line, []byte(":"))
   415  		if bytes.Equal(nulrev, pair[1]) {
   416  			// null rev indicates this tag is marked for deletion
   417  			continue
   418  		}
   419  
   420  		idx := bytes.IndexByte(pair[0], 32) // space
   421  		v := NewVersion(string(pair[0][:idx])).Is(Revision(pair[1])).(PairedVersion)
   422  		vlist = append(vlist, v)
   423  	}
   424  
   425  	// bookmarks next, because the presence of the magic @ bookmark has to
   426  	// determine how we handle the branches
   427  	var magicAt bool
   428  	out, err = runFromRepoDir(ctx, r, "hg", "bookmarks", "--debug")
   429  	if err != nil {
   430  		// better nothing than partial and misleading
   431  		return nil, fmt.Errorf("%s: %s", err, string(out))
   432  	}
   433  
   434  	out = bytes.TrimSpace(out)
   435  	if !bytes.Equal(out, []byte("no bookmarks set")) {
   436  		all = bytes.Split(out, []byte("\n"))
   437  		for _, line := range all {
   438  			// Trim leading spaces, and * marker if present
   439  			line = bytes.TrimLeft(line, " *")
   440  			pair := bytes.Split(line, []byte(":"))
   441  			// if this doesn't split exactly once, we have something weird
   442  			if len(pair) != 2 {
   443  				continue
   444  			}
   445  
   446  			// Split on colon; this gets us the rev and the branch plus local revno
   447  			idx := bytes.IndexByte(pair[0], 32) // space
   448  			// if it's the magic @ marker, make that the default branch
   449  			str := string(pair[0][:idx])
   450  			var v PairedVersion
   451  			if str == "@" {
   452  				magicAt = true
   453  				v = newDefaultBranch(str).Is(Revision(pair[1])).(PairedVersion)
   454  			} else {
   455  				v = NewBranch(str).Is(Revision(pair[1])).(PairedVersion)
   456  			}
   457  			vlist = append(vlist, v)
   458  		}
   459  	}
   460  
   461  	out, err = runFromRepoDir(ctx, r, "hg", "branches", "-c", "--debug")
   462  	if err != nil {
   463  		// better nothing than partial and misleading
   464  		return nil, fmt.Errorf("%s: %s", err, string(out))
   465  	}
   466  
   467  	all = bytes.Split(bytes.TrimSpace(out), []byte("\n"))
   468  	for _, line := range all {
   469  		// Trim inactive and closed suffixes, if present; we represent these
   470  		// anyway
   471  		line = bytes.TrimSuffix(line, []byte(" (inactive)"))
   472  		line = bytes.TrimSuffix(line, []byte(" (closed)"))
   473  
   474  		// Split on colon; this gets us the rev and the branch plus local revno
   475  		pair := bytes.Split(line, []byte(":"))
   476  		idx := bytes.IndexByte(pair[0], 32) // space
   477  		str := string(pair[0][:idx])
   478  		// if there was no magic @ bookmark, and this is mercurial's magic
   479  		// "default" branch, then mark it as default branch
   480  		var v PairedVersion
   481  		if !magicAt && str == "default" {
   482  			v = newDefaultBranch(str).Is(Revision(pair[1])).(PairedVersion)
   483  		} else {
   484  			v = NewBranch(str).Is(Revision(pair[1])).(PairedVersion)
   485  		}
   486  		vlist = append(vlist, v)
   487  	}
   488  
   489  	return vlist, nil
   490  }
   491  
   492  type repo struct {
   493  	// Object for direct repo interaction
   494  	r ctxRepo
   495  }
   496  
   497  // This func copied from Masterminds/vcs so we can exec our own commands
   498  func mergeEnvLists(in, out []string) []string {
   499  NextVar:
   500  	for _, inkv := range in {
   501  		k := strings.SplitAfterN(inkv, "=", 2)[0]
   502  		for i, outkv := range out {
   503  			if strings.HasPrefix(outkv, k) {
   504  				out[i] = inkv
   505  				continue NextVar
   506  			}
   507  		}
   508  		out = append(out, inkv)
   509  	}
   510  	return out
   511  }