github.com/comcast/canticle@v0.0.0-20161108184242-c53cface56e8/canticles/vcs.go (about)

     1  package canticles
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path"
     9  	"regexp"
    10  	"strings"
    11  	"sync"
    12  
    13  	"golang.org/x/tools/go/vcs"
    14  )
    15  
    16  // TODO: We should just rip out the reliance on tools/vcs. Most of it
    17  // is so non functional it is just a headache.
    18  
    19  // A VCS has the ability to create and change the revision of a
    20  // package. A VCS is generall resolved using a RepoDiscovery.
    21  type VCS interface {
    22  	Create(rev string) error
    23  	SetRev(rev string) error
    24  	GetRev() (string, error)
    25  	GetBranch() (string, error)
    26  	UpdateBranch(branch string) (updated bool, update string, err error)
    27  	GetSource() (string, error)
    28  	GetRoot() string
    29  }
    30  
    31  // GitAtVCS creates a VCS cmd that supports the "git@blah.com:" syntax
    32  func GitAtVCS() *vcs.Cmd {
    33  	v := &vcs.Cmd{}
    34  	*v = *vcs.ByCmd("git")
    35  	v.CreateCmd = "clone {repo} {dir}"
    36  	v.PingCmd = "ls-remote {scheme}@{repo}"
    37  	v.Scheme = []string{"git"}
    38  	v.PingCmd = "ls-remote {scheme}@{repo}"
    39  	return v
    40  }
    41  
    42  // A VCSCmd is used to run a VCS command for a repo
    43  type VCSCmd struct {
    44  	Name       string
    45  	Cmd        string
    46  	Args       []string
    47  	ParseRegex *regexp.Regexp
    48  }
    49  
    50  // ExecWithArgs overriden from the default
    51  func (vc *VCSCmd) ExecWithArgs(repo string, args []string) (string, error) {
    52  	LogVerbose("Running command: %s %v in dir %s", vc.Cmd, args, repo)
    53  	cmd := exec.Command(vc.Cmd, args...)
    54  	cmd.Dir = repo
    55  	result, err := cmd.CombinedOutput()
    56  	resultTrim := strings.TrimSpace(string(result))
    57  	rev := vc.ParseRegex.FindSubmatch([]byte(resultTrim))
    58  	switch {
    59  	case err != nil:
    60  		return "", fmt.Errorf("Error getting revision %s", result)
    61  	case result == nil:
    62  		return "", errors.New("Error vcs returned no info for revision")
    63  	case rev == nil:
    64  		return "", fmt.Errorf("Error parsing cmd result:\n%s", string(result))
    65  	default:
    66  		return string(rev[1]), nil
    67  	}
    68  }
    69  
    70  // Exec executes this command with its arguments and parses them using
    71  // regexp. Return an error if the command generates an error or we can
    72  // not parse the results.
    73  func (vc *VCSCmd) Exec(repo string) (string, error) {
    74  	return vc.ExecWithArgs(repo, vc.Args)
    75  }
    76  
    77  // ExecReplace replaces the value in this commands args with values
    78  // from vals and executes the function.
    79  func (vc *VCSCmd) ExecReplace(repo string, vals map[string]string) (string, error) {
    80  	replacements := make([]string, 0, len(vals)*2)
    81  	for k, v := range vals {
    82  		replacements = append(replacements, k, v)
    83  	}
    84  	replacer := strings.NewReplacer(replacements...)
    85  	args := make([]string, 0, len(vc.Args))
    86  	for _, arg := range vc.Args {
    87  		args = append(args, replacer.Replace(arg))
    88  	}
    89  	return vc.ExecWithArgs(repo, args)
    90  }
    91  
    92  var (
    93  	// GitRevCmd attempts to pull the current git from a git
    94  	// repo. It will fail if the work tree is "dirty".
    95  	GitRevCmd = &VCSCmd{
    96  		Name:       "Git",
    97  		Cmd:        "git",
    98  		Args:       []string{"rev-parse", "HEAD"},
    99  		ParseRegex: regexp.MustCompile(`(\S+)`),
   100  	}
   101  	// SvnRevCmd attempts to pull the current svnversion from a svn
   102  	// repo.
   103  	SvnRevCmd = &VCSCmd{
   104  		Name:       "Subversion",
   105  		Cmd:        "svnversion",
   106  		ParseRegex: regexp.MustCompile(`^(\S+)$`), // svnversion doesn't have a bad exitcode if not in svndir
   107  	}
   108  	// BzrRevCmd attempts to pull the current revno from a Bazaar
   109  	// repo.
   110  	BzrRevCmd = &VCSCmd{
   111  		Name:       "Bazaar",
   112  		Cmd:        "bzr",
   113  		Args:       []string{"revno"},
   114  		ParseRegex: regexp.MustCompile(`(\S+)`),
   115  	}
   116  	// HgRevCmd attempts to pull the current node from a Mercurial
   117  	// repo.
   118  	HgRevCmd = &VCSCmd{
   119  		Name:       "Mercurial",
   120  		Cmd:        "hg",
   121  		Args:       []string{"log", "--template", "{node}"},
   122  		ParseRegex: regexp.MustCompile(`(\S+)`),
   123  	}
   124  	// RevCmds is a map of cmd (git, svn, etc.) to
   125  	// the cmd to parse its revision.
   126  	RevCmds = map[string]*VCSCmd{
   127  		GitRevCmd.Name: GitRevCmd,
   128  		SvnRevCmd.Name: SvnRevCmd,
   129  		BzrRevCmd.Name: BzrRevCmd,
   130  		HgRevCmd.Name:  HgRevCmd,
   131  	}
   132  
   133  	// GitRemoteCmd attempts to pull the origin of a git repo.
   134  	GitRemoteCmd = &VCSCmd{
   135  		Name:       "Git",
   136  		Cmd:        "git",
   137  		Args:       []string{"ls-remote", "--get-url", "origin"},
   138  		ParseRegex: regexp.MustCompile(`^(.+)$`),
   139  	}
   140  	// SvnRemoteCmd attempts to pull the origin of a svn repo.
   141  	SvnRemoteCmd = &VCSCmd{
   142  		Name:       "Subversion",
   143  		Cmd:        "svn",
   144  		Args:       []string{"info"},
   145  		ParseRegex: regexp.MustCompile(`^URL: (.+)$`), // svnversion doesn't have a bad exitcode if not in svndir
   146  	}
   147  	// HgRemoteCmd attempts to pull the current default paths from
   148  	// a Mercurial repo.
   149  	HgRemoteCmd = &VCSCmd{
   150  		Name:       "Mercurial",
   151  		Cmd:        "hg",
   152  		Args:       []string{"paths", "default"},
   153  		ParseRegex: regexp.MustCompile(`(.+)`),
   154  	}
   155  	// RemoteCmds is a map of cmd (git, svn, etc.) to
   156  	// the cmd to parse its revision.
   157  	RemoteCmds = map[string]*VCSCmd{
   158  		GitRemoteCmd.Name: GitRemoteCmd,
   159  		SvnRemoteCmd.Name: SvnRemoteCmd,
   160  		HgRemoteCmd.Name:  HgRemoteCmd,
   161  	}
   162  
   163  	// GitBranchCmd is used to get the current branch (if present)
   164  	GitBranchCmd = &VCSCmd{
   165  		Name:       "Git",
   166  		Cmd:        "git",
   167  		Args:       []string{"symbolic-ref", "--short", "HEAD"},
   168  		ParseRegex: regexp.MustCompile(`(.+)`),
   169  	}
   170  	// HgBranchCmd is used to get the current branch (if present)
   171  	HgBranchCmd = &VCSCmd{
   172  		Name:       "Mercurial",
   173  		Cmd:        "hg",
   174  		Args:       []string{"id", "-b"},
   175  		ParseRegex: regexp.MustCompile(`(.+)`),
   176  	}
   177  	// SvnBranchCmd is used to get the current branch (if present)
   178  	SvnBranchCmd = &VCSCmd{
   179  		Name:       "Subversion",
   180  		Cmd:        "svn",
   181  		Args:       []string{"info"},
   182  		ParseRegex: regexp.MustCompile(`^URL: (.+)$`),
   183  	}
   184  	// BzrBranchCmd is used to get the current branch (if present)
   185  	BzrBranchCmd = &VCSCmd{
   186  		Name:       "Bazaar",
   187  		Cmd:        "bzr",
   188  		Args:       []string{"version-info"},
   189  		ParseRegex: regexp.MustCompile(`branch-nick: (.+)`),
   190  	}
   191  	// BranchCmds is a map of cmd (git, svn, etc.) to
   192  	// the cmd to parse the current branch
   193  	BranchCmds = map[string]*VCSCmd{
   194  		GitBranchCmd.Name: GitBranchCmd,
   195  		SvnBranchCmd.Name: SvnBranchCmd,
   196  		HgBranchCmd.Name:  HgBranchCmd,
   197  		BzrBranchCmd.Name: BzrBranchCmd,
   198  	}
   199  )
   200  
   201  // An UpdateCMD is used to update a local copy of remote branches and
   202  // tags. Not relevant for Bazaar and SVN.
   203  var (
   204  	// GitUpdateCmd is used to update local copy's of remote branches (if present)
   205  	GitUpdateCmd = &VCSCmd{
   206  		Name:       "Git",
   207  		Cmd:        "git",
   208  		Args:       []string{"fetch", "--all"},
   209  		ParseRegex: regexp.MustCompile(`(.+)`),
   210  	}
   211  	// HgUpdateCmd is used used to update local copy's of remote branches (if present)
   212  	HgUpdateCmd = &VCSCmd{
   213  		Name:       "Mercurial",
   214  		Cmd:        "hg",
   215  		Args:       []string{"pull"},
   216  		ParseRegex: regexp.MustCompile(`(.+)`),
   217  	}
   218  	// BranchCmds is a map of cmd (git, svn, etc.) to
   219  	// the cmd to parse the current branch
   220  	UpdateCmds = map[string]*VCSCmd{
   221  		GitUpdateCmd.Name: GitUpdateCmd,
   222  		HgUpdateCmd.Name:  HgUpdateCmd,
   223  	}
   224  )
   225  
   226  // A TagSyncCmd is used to set the revision of a git repo to the specified tag or branch.
   227  var (
   228  	GitTagSyncCmd = &VCSCmd{
   229  		Name:       "Git",
   230  		Cmd:        "git",
   231  		Args:       []string{"checkout", "{tag}"},
   232  		ParseRegex: regexp.MustCompile(`(.+)`),
   233  	}
   234  	HgTagSyncCmd = &VCSCmd{
   235  		Name:       "Mercurial",
   236  		Cmd:        "hg",
   237  		Args:       []string{"update", "-r", "{tag}"},
   238  		ParseRegex: regexp.MustCompile(`(.+)`),
   239  	}
   240  	BzrTagSyncCmd = &VCSCmd{
   241  		Name:       "Bazaar",
   242  		Cmd:        "bzr",
   243  		Args:       []string{"update", "-r", "{tag}"},
   244  		ParseRegex: regexp.MustCompile(`(Updated to .+|Tree is up)$`),
   245  	}
   246  	SvnTagSyncCmd = &VCSCmd{
   247  		Name:       "Subversion",
   248  		Cmd:        "svn",
   249  		Args:       []string{"update", "--accept", "postpone", "-r", "{tag}"},
   250  		ParseRegex: regexp.MustCompile(`(Updated to .+|At revision)`),
   251  	}
   252  	TagSyncCmds = map[string]*VCSCmd{
   253  		GitTagSyncCmd.Name: GitTagSyncCmd,
   254  		HgTagSyncCmd.Name:  HgTagSyncCmd,
   255  		BzrTagSyncCmd.Name: BzrTagSyncCmd,
   256  		SvnTagSyncCmd.Name: SvnTagSyncCmd,
   257  	}
   258  )
   259  
   260  // A BranchUpdateCmd is used to update a branch (assumed to be already
   261  // checked out) against a remote source. These commands will fail if
   262  // the git equivalent of a "fast forward merge" can not be completed.
   263  // The svn and bzr commands are the same as the tagsync commands.
   264  var (
   265  	GitBranchUpdateCmd = &VCSCmd{
   266  		Name:       "Git",
   267  		Cmd:        "git",
   268  		Args:       []string{"pull", "--ff-only", "origin", "{branch}"},
   269  		ParseRegex: regexp.MustCompile(`(Already|Updating .+)`),
   270  	}
   271  	HgBranchUpdateCmd = &VCSCmd{
   272  		Name:       "Mercurial",
   273  		Cmd:        "hg",
   274  		Args:       []string{"pull", "-u"},
   275  		ParseRegex: regexp.MustCompile(`(added .+|no changes found)$`),
   276  	}
   277  	BranchUpdateCmds = map[string]*VCSCmd{
   278  		GitBranchUpdateCmd.Name: GitBranchUpdateCmd,
   279  		HgBranchUpdateCmd.Name:  HgBranchUpdateCmd,
   280  		BzrTagSyncCmd.Name:      BzrTagSyncCmd,
   281  		SvnTagSyncCmd.Name:      SvnTagSyncCmd,
   282  	}
   283  	BranchUpdatedRegexs = map[string]*regexp.Regexp{
   284  		GitBranchUpdateCmd.Name: regexp.MustCompile(`(Updating .+)`),
   285  		HgBranchUpdateCmd.Name:  regexp.MustCompile(`(added .+)`),
   286  		BzrTagSyncCmd.Name:      regexp.MustCompile(`(Updated to .+)`),
   287  		SvnTagSyncCmd.Name:      regexp.MustCompile(`(Updated to .+)`),
   288  	}
   289  )
   290  
   291  func GetSvnBranches(path string) ([]string, error) {
   292  	return nil, errors.New("Not implemented")
   293  }
   294  
   295  func GetGitBranches(path string) ([]string, error) {
   296  	cmd := exec.Command("git", "show-ref")
   297  	cmd.Dir = path
   298  	result, err := cmd.CombinedOutput()
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  	lines := strings.Split(string(result), "\n")
   303  	var results []string
   304  	for _, line := range lines {
   305  		parts := strings.Split(line, " ")
   306  		if len(parts) > 1 {
   307  			refName := parts[1]
   308  			switch {
   309  			case strings.HasPrefix(refName, "refs/heads/"):
   310  				results = append(results, strings.TrimPrefix(refName, "refs/heads/"))
   311  			case strings.HasPrefix(refName, "refs/remotes/"):
   312  				// refs/remotes/origin/<branchname>
   313  				remoteRef := strings.SplitN(strings.TrimPrefix(refName, "refs/remotes/"), "/", 2)
   314  				results = append(results, remoteRef[1])
   315  			}
   316  		}
   317  	}
   318  	return results, nil
   319  }
   320  
   321  func GetHgBranches(path string) ([]string, error) {
   322  	return nil, errors.New("Not implemented")
   323  }
   324  
   325  func GetBzrBranches(path string) ([]string, error) {
   326  	return nil, errors.New("Not implemented")
   327  }
   328  
   329  var BranchFuncs = map[string]func(string) ([]string, error){
   330  	GitBranchCmd.Name: GetGitBranches,
   331  	SvnBranchCmd.Name: GetSvnBranches,
   332  	HgBranchCmd.Name:  GetHgBranches,
   333  	BzrBranchCmd.Name: GetBzrBranches,
   334  }
   335  
   336  // A LocalVCS uses packages and version control systems available at a
   337  // local srcpath to control a local destpath (it copies the files over).
   338  type LocalVCS struct {
   339  	Package            string
   340  	Root               string
   341  	SrcPath            string
   342  	Cmd                *vcs.Cmd
   343  	CurrentRevCmd      *VCSCmd        // CurrentRevCommand to check the current revision for sourcepath.
   344  	RemoteCmd          *VCSCmd        // RemoteCmd to obtain the upstream (remote) for a repo
   345  	BranchCmd          *VCSCmd        // BranchCmd to obtains the current branch if on one
   346  	UpdateCmd          *VCSCmd        // UpdateCMD is used to pull remote updates but NOT update the local
   347  	BranchUpdateCmd    *VCSCmd        // BranchUpdateCmd is used to update a local branch with a remote
   348  	BranchUpdatedRegex *regexp.Regexp // The regex to examine if an update occured from a branch update cmd
   349  	SyncCmd            *VCSCmd
   350  	Branches           func(path string) ([]string, error)
   351  }
   352  
   353  // NewLocalVCS returns a a LocalVCS with CurrentRevCmd initialized
   354  // from the cmd's name using RevCmds and RemoteCmd from RemoteCmds.
   355  func NewLocalVCS(pkg, root, srcPath string, cmd *vcs.Cmd) *LocalVCS {
   356  	return &LocalVCS{
   357  		Package:            pkg,
   358  		Root:               root,
   359  		SrcPath:            srcPath,
   360  		Cmd:                cmd,
   361  		CurrentRevCmd:      RevCmds[cmd.Name],
   362  		RemoteCmd:          RemoteCmds[cmd.Name],
   363  		BranchCmd:          BranchCmds[cmd.Name],
   364  		UpdateCmd:          UpdateCmds[cmd.Name],
   365  		Branches:           BranchFuncs[cmd.Name],
   366  		BranchUpdateCmd:    BranchUpdateCmds[cmd.Name],
   367  		BranchUpdatedRegex: BranchUpdatedRegexs[cmd.Name],
   368  		SyncCmd:            TagSyncCmds[cmd.Name],
   369  	}
   370  }
   371  
   372  // Create will copy (using a dir copier) the package from srcpath to
   373  // destpath and then call set.
   374  func (lv *LocalVCS) Create(rev string) error {
   375  	return lv.SetRev(rev)
   376  }
   377  
   378  // SetRev will use the LocalVCS's Cmd.TagSync method to change the
   379  // revision of a repo if rev is not the empty string and Cmd is not
   380  // nil.
   381  func (lv *LocalVCS) SetRev(rev string) error {
   382  	if lv.Cmd == nil || rev == "" {
   383  		return nil
   384  	}
   385  	src := PackageSource(lv.SrcPath, lv.Root)
   386  	// Update against remotes if we need too
   387  	if lv.UpdateCmd != nil {
   388  		if _, err := lv.UpdateCmd.Exec(src); err != nil {
   389  			return err
   390  		}
   391  	}
   392  	// For revisions we just want to check it out
   393  	if err := lv.TagSync(rev); err != nil {
   394  		return err
   395  	}
   396  	return nil
   397  }
   398  
   399  func (lv *LocalVCS) TagSync(rev string) error {
   400  	LogVerbose("Tag sync to: %s", rev)
   401  	if lv.SyncCmd == nil {
   402  		return nil
   403  	}
   404  	_, err := lv.SyncCmd.ExecReplace(PackageSource(lv.SrcPath, lv.Root), map[string]string{"{tag}": rev})
   405  	if err == nil {
   406  		return nil
   407  	}
   408  	LogVerbose("Tag sync failed with err: %s", err.Error())
   409  	return lv.Cmd.TagSync(PackageSource(lv.SrcPath, lv.Root), rev)
   410  }
   411  
   412  func (lv *LocalVCS) RevIsBranch(rev string) bool {
   413  	branches, err := lv.Branches(PackageSource(lv.SrcPath, lv.Root))
   414  	if err != nil {
   415  		LogVerbose("Error getting branches %s", err.Error())
   416  		return false
   417  	}
   418  	LogVerbose("Found branches %v", branches)
   419  	for _, br := range branches {
   420  		if rev == br {
   421  			return true
   422  		}
   423  	}
   424  	return false
   425  }
   426  
   427  // GetRev will return current revision of the local repo.  If the
   428  // local package is not under a VCS it will return nil, nil.  If the
   429  // vcs can not query the version it will return nil and an error.
   430  func (lv *LocalVCS) GetRev() (string, error) {
   431  	if lv.CurrentRevCmd == nil || lv.Cmd == nil {
   432  		return "", nil
   433  	}
   434  	return lv.CurrentRevCmd.Exec(PackageSource(lv.SrcPath, lv.Root))
   435  
   436  }
   437  
   438  // GetSource on a LocalVCS will attempt to determine the local repos
   439  // upstream source. See the RemoteCmd for each VCS for behavior.
   440  func (lv *LocalVCS) GetSource() (string, error) {
   441  	if lv.RemoteCmd == nil {
   442  		return "", nil
   443  	}
   444  	return lv.RemoteCmd.Exec(PackageSource(lv.SrcPath, lv.Root))
   445  }
   446  
   447  // GetRoot on a LocalVCS will return PackageName for SrcPath
   448  func (lv *LocalVCS) GetRoot() string {
   449  	return lv.Root
   450  }
   451  
   452  // GetBranch on a LocalVCS will return the branch (if any) for the
   453  // current local repo. If none GetBranch will return an error.
   454  func (lv *LocalVCS) GetBranch() (string, error) {
   455  	return lv.BranchCmd.Exec(PackageSource(lv.SrcPath, lv.Root))
   456  }
   457  
   458  // UpdateBranch will return true if the local branch was updated,
   459  // false if not. Error will be non nil if an error occured during the
   460  // udpate.
   461  func (lv *LocalVCS) UpdateBranch(branch string) (updated bool, update string, err error) {
   462  	if !lv.RevIsBranch(branch) {
   463  		return false, fmt.Sprintf("rev %s is not a branch", branch), nil
   464  	}
   465  	res, err := lv.BranchUpdateCmd.ExecReplace(
   466  		PackageSource(lv.SrcPath, lv.Root),
   467  		map[string]string{"{branch}": branch},
   468  	)
   469  	if lv.BranchUpdatedRegex.Match([]byte(res)) {
   470  		return true, res, err
   471  	}
   472  	return false, res, err
   473  }
   474  
   475  // VCSType represents a prefix to look for, a scheme to ping a path
   476  // with and a VCS command to do the pinging.
   477  type VCSType struct {
   478  	Prefix string
   479  	Scheme string
   480  	VCS    *vcs.Cmd
   481  }
   482  
   483  // VCSTypes is the list of VCSType used by GuessVCS
   484  var VCSTypes = []VCSType{
   485  	{"git+ssh://", "git+ssh", vcs.ByCmd("git")},
   486  	{"git://", "git", vcs.ByCmd("git")},
   487  	{"git@", "git", GitAtVCS()},
   488  	{"ssh://hg@", "ssh", vcs.ByCmd("hg")},
   489  	{"svn://", "svn", vcs.ByCmd("svn")},
   490  	{"bzr://", "bzr", vcs.ByCmd("bzr")},
   491  	{"https://", "https", vcs.ByCmd("git")}, // not so sure this is a good idea
   492  }
   493  
   494  // GuessVCS attempts to guess the VCS given a url. This uses the
   495  // VCSTypes array, checking for prefixes that match and attempting to
   496  // ping the VCS with the given scheme
   497  func GuessVCS(url string) *vcs.Cmd {
   498  	for _, vt := range VCSTypes {
   499  		if !strings.HasPrefix(url, vt.Prefix) {
   500  			continue
   501  		}
   502  		path := strings.TrimPrefix(url, vt.Scheme)
   503  		path = strings.TrimPrefix(path, "://")
   504  		path = strings.TrimPrefix(path, "@")
   505  		LogVerbose("Pinging path %s with scheme %s for vcs %s", path, vt.Scheme, vt.VCS.Name)
   506  		if err := vt.VCS.Ping(vt.Scheme, path); err != nil {
   507  			LogVerbose("Error pinging path %s with scheme %s", path, vt.Scheme)
   508  			continue
   509  		}
   510  		return vt.VCS
   511  	}
   512  	return nil
   513  }
   514  
   515  // PackageVCS wraps the underlying golang.org/x/tools/go/vcs to
   516  // present the interface we need. It also implements the functionality
   517  // necessary for SetRev to happen correctly.
   518  type PackageVCS struct {
   519  	Repo   *vcs.RepoRoot
   520  	Gopath string
   521  }
   522  
   523  // UpdateBranch will attempt to construct a local vcs and update that.
   524  func (pv *PackageVCS) UpdateBranch(branch string) (updated bool, update string, err error) {
   525  	lv := NewLocalVCS(pv.Repo.Root, pv.Repo.Root, pv.Gopath, pv.Repo.VCS)
   526  	return lv.UpdateBranch(branch)
   527  }
   528  
   529  // Create clones the VCS into the location provided by Repo.Root
   530  func (pv *PackageVCS) Create(rev string) error {
   531  	v := pv.Repo.VCS
   532  	dir := PackageSource(pv.Gopath, pv.Repo.Root)
   533  	if err := v.Create(dir, pv.Repo.Repo); err != nil {
   534  		return err
   535  	}
   536  	if rev == "" {
   537  		return nil
   538  	}
   539  	return pv.SetRev(rev)
   540  }
   541  
   542  // SetRev changes the revision of the Repo.Root to the value
   543  // provided. This also modifies the git based vcs to be able to deal
   544  // with non named revisions (sigh).
   545  func (pv *PackageVCS) SetRev(rev string) error {
   546  	lv := NewLocalVCS(pv.Repo.Root, pv.Repo.Root, pv.Gopath, pv.Repo.VCS)
   547  	return lv.TagSync(rev)
   548  }
   549  
   550  // GetRev does not work on remote VCS's and will always return a not
   551  // implemented error.
   552  func (pv *PackageVCS) GetRev() (string, error) {
   553  	return "", errors.New("package VCS currently does not support GetRev")
   554  }
   555  
   556  // GetRoot will return pv.Repo.Root
   557  func (pv *PackageVCS) GetRoot() string {
   558  	return pv.Repo.Root
   559  }
   560  
   561  // GetSource returns the pv.Repo.Repo
   562  func (pv *PackageVCS) GetSource() (string, error) {
   563  	return pv.Repo.Repo, nil
   564  }
   565  
   566  // GetBranch does not work on remote VCS for now and will return an
   567  // error.
   568  func (pv *PackageVCS) GetBranch() (string, error) {
   569  	return "", errors.New("package VCS currently does not support GetBranch")
   570  }
   571  
   572  // A ResolutionFailureError contains status as to whether this is a resolution failure
   573  // or of some other type
   574  type ResolutionFailureError struct {
   575  	Err error
   576  	Pkg string
   577  	VCS string
   578  }
   579  
   580  // A NewResolutionFailureError with the pkg and vcs passed in
   581  func NewResolutionFailureError(pkg, vcs string) *ResolutionFailureError {
   582  	return &ResolutionFailureError{
   583  		Err: fmt.Errorf("pkg %s could not be resolved by vcs %s", pkg, vcs),
   584  		Pkg: pkg,
   585  		VCS: vcs,
   586  	}
   587  }
   588  
   589  // Error message attached to this vcs error
   590  func (re ResolutionFailureError) Error() string {
   591  	return re.Err.Error()
   592  }
   593  
   594  // ResolutionFailureErr will return non nil if a RepoResolver could not
   595  // resolve a VCS.
   596  func ResolutionFailureErr(err error) *ResolutionFailureError {
   597  	if err == nil {
   598  		return nil
   599  	}
   600  	if re, ok := err.(*ResolutionFailureError); ok {
   601  		return re
   602  	}
   603  	return nil
   604  }
   605  
   606  // RepoResolver provides the mechanisms for resolving a VCS from an
   607  // importpath and sourceUrl.
   608  type RepoResolver interface {
   609  	ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error)
   610  }
   611  
   612  // DefaultRepoResolver attempts to resolve a repo using the go
   613  // vcs.RepoRootForImportPath semantics and guessing logic.
   614  type DefaultRepoResolver struct {
   615  	Gopath string
   616  }
   617  
   618  // TrimPathToRoot will take import path github.comcast.com/x/tools/go/vcs
   619  // and root golang.org/x/tools and create github.comcast.com/x/tools.
   620  func TrimPathToRoot(importPath, root string) (string, error) {
   621  	pathParts := strings.Split(importPath, "/")
   622  	rootParts := strings.Split(root, "/")
   623  
   624  	if len(pathParts) < len(rootParts) {
   625  		return "", fmt.Errorf("path %s does not contain enough prefix for path %s", importPath, root)
   626  	}
   627  	return path.Join(pathParts[0:len(rootParts)]...), nil
   628  }
   629  
   630  // ResolveRepo on a default reporesolver is effectively go get wraped
   631  // to use the url string.
   632  func (dr *DefaultRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) {
   633  	// We guess our vcs based off our url path if present
   634  	resolvePath := getResolvePath(importPath)
   635  
   636  	LogVerbose("Attempting to use go get vcs for url: %s", resolvePath)
   637  	vcs.Verbose = Verbose
   638  	repo, err := vcs.RepoRootForImportPath(resolvePath, true)
   639  	if err != nil {
   640  		LogVerbose("Failed creating VCS for url: %s, err: %s", resolvePath, err.Error())
   641  		return nil, err
   642  	}
   643  
   644  	// If we found something return non nil
   645  	repo.Root, err = TrimPathToRoot(importPath, repo.Root)
   646  	if err != nil {
   647  		LogVerbose("Failed creating VCS for url: %s, err: %s", resolvePath, err.Error())
   648  		return nil, err
   649  	}
   650  	v := &PackageVCS{Repo: repo, Gopath: dr.Gopath}
   651  	LogVerbose("Created VCS for url: %s", resolvePath)
   652  	return v, nil
   653  }
   654  
   655  // RemoteRepoResolver attempts to resolve a repo using the internal
   656  // guessing logic for Canticle.
   657  type RemoteRepoResolver struct {
   658  	Gopath string
   659  }
   660  
   661  // ResolveRepo on the remoterepo resolver uses our own GuessVCS
   662  // method. It mostly looks at protocol cues like svn:// and git@.
   663  func (rr *RemoteRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) {
   664  	resolvePath := getResolvePath(importPath)
   665  	if dep != nil && dep.SourcePath != "" {
   666  		resolvePath = getResolvePath(dep.SourcePath)
   667  	}
   668  	// Attempt our internal guessing logic first
   669  	LogVerbose("Attempting to use default resolver for url: %s", resolvePath)
   670  	v := GuessVCS(resolvePath)
   671  	if v == nil {
   672  		return nil, NewResolutionFailureError(importPath, "remote")
   673  	}
   674  
   675  	root := dep.Root
   676  	if root == "" {
   677  		root = importPath
   678  	}
   679  	pv := &PackageVCS{
   680  		Repo: &vcs.RepoRoot{
   681  			VCS:  v,
   682  			Repo: resolvePath,
   683  			Root: root,
   684  		},
   685  		Gopath: rr.Gopath,
   686  	}
   687  	return pv, nil
   688  }
   689  
   690  func getResolvePath(importPath string) string {
   691  	if strings.Contains(importPath, "/") {
   692  		return importPath
   693  	} else {
   694  		return importPath + "/"
   695  	}
   696  }
   697  
   698  // LocalRepoResolver will attempt to find local copies of a repo in
   699  // LocalPath (treating it like a gopath) and provide VCS systems for
   700  // updating them in RemotePath (also treaded like a gopath).
   701  type LocalRepoResolver struct {
   702  	LocalPath string
   703  }
   704  
   705  // ResolveRepo on a local resolver may return an error if:
   706  // *  The local package is not present (no directory) in LocalPath
   707  // *  The local "package" is a file in localpath
   708  // *  There was an error stating the directory for the localPkg
   709  func (lr *LocalRepoResolver) ResolveRepo(pkg string, dep *CanticleDependency) (VCS, error) {
   710  	LogVerbose("Finding local vcs for package: %s\n", pkg)
   711  	fullPath := PackageSource(lr.LocalPath, getResolvePath(pkg))
   712  	s, err := os.Stat(fullPath)
   713  	switch {
   714  	case err != nil:
   715  		LogVerbose("Error stating local copy of package: %s %s\n", fullPath, err.Error())
   716  		return nil, err
   717  	case s != nil && s.IsDir():
   718  		cmd, root, err := vcs.FromDir(fullPath, lr.LocalPath)
   719  		if err != nil {
   720  			LogVerbose("Error with local vcs: %s", err.Error())
   721  			return nil, err
   722  		}
   723  		root, _ = PackageName(lr.LocalPath, path.Join(lr.LocalPath, root))
   724  		v := NewLocalVCS(root, root, lr.LocalPath, cmd)
   725  		LogVerbose("Created vcs for local pkg: %+v", v)
   726  		return v, nil
   727  	default:
   728  		LogVerbose("Could not resolve local vcs for package: %s", fullPath)
   729  		return nil, NewResolutionFailureError(pkg, "local")
   730  	}
   731  }
   732  
   733  // CompositeRepoResolver calls the repos in resolvers in order,
   734  // discarding errors and returning the first VCS found.
   735  type CompositeRepoResolver struct {
   736  	Resolvers []RepoResolver
   737  }
   738  
   739  // ResolveRepo for the composite attempts its sub Resolvers in order
   740  // ignoring any errors. If all resolvers fail a ResolutionFailureError
   741  // will be returned.
   742  func (cr *CompositeRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) {
   743  	for _, r := range cr.Resolvers {
   744  		vcs, err := r.ResolveRepo(importPath, dep)
   745  		if vcs != nil && err == nil {
   746  			return vcs, nil
   747  		}
   748  	}
   749  	return nil, NewResolutionFailureError(importPath, "composite")
   750  }
   751  
   752  type resolve struct {
   753  	v   VCS
   754  	err error
   755  }
   756  
   757  // MemoizedRepoResolver remembers the results of previously attempted
   758  // resolutions and will not attempt the same resolution twice.
   759  type MemoizedRepoResolver struct {
   760  	sync.RWMutex
   761  	resolvedPaths map[string]*resolve
   762  	resolver      RepoResolver
   763  }
   764  
   765  // NewMemoizedRepoResolver creates a memozied version of the passed in
   766  // resolver.
   767  func NewMemoizedRepoResolver(resolver RepoResolver) *MemoizedRepoResolver {
   768  	return &MemoizedRepoResolver{
   769  		resolvedPaths: make(map[string]*resolve),
   770  		resolver:      resolver,
   771  	}
   772  }
   773  
   774  // ResolveRepo on a MemoizedRepoResolver will cache the results of its
   775  // child resolver.
   776  func (mr *MemoizedRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) {
   777  	mr.RLock()
   778  	r := mr.resolvedPaths[importPath]
   779  	mr.RUnlock()
   780  	if r != nil {
   781  		return r.v, r.err
   782  	}
   783  
   784  	v, err := mr.resolver.ResolveRepo(importPath, dep)
   785  	mr.Lock()
   786  	mr.resolvedPaths[importPath] = &resolve{v, err}
   787  	mr.Unlock()
   788  	return v, err
   789  }