github.com/sc0rp1us/gb@v0.4.1-0.20160319180011-4ba8cf1baa5a/vendor/repo.go (about)

     1  package vendor
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/url"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/constabulary/gb/fileutils"
    16  )
    17  
    18  // RemoteRepo describes a remote dvcs repository.
    19  type RemoteRepo interface {
    20  
    21  	// Checkout checks out a specific branch, tag, or revision.
    22  	// The interpretation of these three values is impementation
    23  	// specific.
    24  	Checkout(branch, tag, revision string) (WorkingCopy, error)
    25  
    26  	// URL returns the URL the clone was taken from. It should
    27  	// only be called after Clone.
    28  	URL() string
    29  }
    30  
    31  // WorkingCopy represents a local copy of a remote dvcs repository.
    32  type WorkingCopy interface {
    33  
    34  	// Dir is the root of this working copy.
    35  	Dir() string
    36  
    37  	// Revision returns the revision of this working copy.
    38  	Revision() (string, error)
    39  
    40  	// Branch returns the branch to which this working copy belongs.
    41  	Branch() (string, error)
    42  
    43  	// Destroy removes the working copy and cleans path to the working copy.
    44  	Destroy() error
    45  }
    46  
    47  var (
    48  	ghregex   = regexp.MustCompile(`^(?P<root>github\.com/([A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`)
    49  	bbregex   = regexp.MustCompile(`^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`)
    50  	lpregex   = regexp.MustCompile(`^launchpad.net/([A-Za-z0-9-._]+)(/[A-Za-z0-9-._]+)?(/.+)?`)
    51  	gcregex   = regexp.MustCompile(`^(?P<root>code\.google\.com/[pr]/(?P<project>[a-z0-9\-]+)(\.(?P<subrepo>[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`)
    52  	genericre = regexp.MustCompile(`^(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/~]*?)\.(?P<vcs>bzr|git|hg|svn))([/A-Za-z0-9_.\-]+)*$`)
    53  )
    54  
    55  // DeduceRemoteRepo takes a potential import path and returns a RemoteRepo
    56  // representing the remote location of the source of an import path.
    57  // Remote repositories can be bare import paths, or urls including a checkout scheme.
    58  // If deduction would cause traversal of an insecure host, a message will be
    59  // printed and the travelsal path will be ignored.
    60  func DeduceRemoteRepo(path string, insecure bool) (RemoteRepo, string, error) {
    61  	u, err := url.Parse(path)
    62  	if err != nil {
    63  		return nil, "", fmt.Errorf("%q is not a valid import path", path)
    64  	}
    65  
    66  	var schemes []string
    67  	if u.Scheme != "" {
    68  		schemes = append(schemes, u.Scheme)
    69  	}
    70  
    71  	path = u.Host + u.Path
    72  	if !regexp.MustCompile(`^([A-Za-z0-9-]+)(\.[A-Za-z0-9-]+)+(/[A-Za-z0-9-_.~]+)*$`).MatchString(path) {
    73  		return nil, "", fmt.Errorf("%q is not a valid import path", path)
    74  	}
    75  
    76  	switch {
    77  	case ghregex.MatchString(path):
    78  		v := ghregex.FindStringSubmatch(path)
    79  		url := &url.URL{
    80  			Host: "github.com",
    81  			Path: v[2],
    82  		}
    83  		repo, err := Gitrepo(url, insecure, schemes...)
    84  		return repo, v[0][len(v[1]):], err
    85  	case bbregex.MatchString(path):
    86  		v := bbregex.FindStringSubmatch(path)
    87  		url := &url.URL{
    88  			Host: "bitbucket.org",
    89  			Path: v[2],
    90  		}
    91  		repo, err := Gitrepo(url, insecure, schemes...)
    92  		if err == nil {
    93  			return repo, v[0][len(v[1]):], nil
    94  		}
    95  		repo, err = Hgrepo(url, insecure)
    96  		if err == nil {
    97  			return repo, v[0][len(v[1]):], nil
    98  		}
    99  		return nil, "", fmt.Errorf("unknown repository type")
   100  	case gcregex.MatchString(path):
   101  		v := gcregex.FindStringSubmatch(path)
   102  		url := &url.URL{
   103  			Host: "code.google.com",
   104  			Path: "p/" + v[2],
   105  		}
   106  		repo, err := Hgrepo(url, insecure, schemes...)
   107  		if err == nil {
   108  			return repo, v[0][len(v[1]):], nil
   109  		}
   110  		repo, err = Gitrepo(url, insecure, schemes...)
   111  		if err == nil {
   112  			return repo, v[0][len(v[1]):], nil
   113  		}
   114  		return nil, "", fmt.Errorf("unknown repository type")
   115  	case lpregex.MatchString(path):
   116  		v := lpregex.FindStringSubmatch(path)
   117  		v = append(v, "", "")
   118  		if v[2] == "" {
   119  			// launchpad.net/project"
   120  			repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%v", v[1]))
   121  			return repo, "", err
   122  		}
   123  		// launchpad.net/project/series"
   124  		repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%s/%s", v[1], v[2]))
   125  		return repo, v[3], err
   126  	}
   127  
   128  	// try the general syntax
   129  	if genericre.MatchString(path) {
   130  		v := genericre.FindStringSubmatch(path)
   131  		switch v[5] {
   132  		case "git":
   133  			x := strings.SplitN(v[1], "/", 2)
   134  			url := &url.URL{
   135  				Host: x[0],
   136  				Path: x[1],
   137  			}
   138  			repo, err := Gitrepo(url, insecure, schemes...)
   139  			return repo, v[6], err
   140  		case "hg":
   141  			x := strings.SplitN(v[1], "/", 2)
   142  			url := &url.URL{
   143  				Host: x[0],
   144  				Path: x[1],
   145  			}
   146  			repo, err := Hgrepo(url, insecure, schemes...)
   147  			return repo, v[6], err
   148  		case "bzr":
   149  			repo, err := Bzrrepo("https://" + v[1])
   150  			return repo, v[6], err
   151  		default:
   152  			return nil, "", fmt.Errorf("unknown repository type: %q", v[5])
   153  
   154  		}
   155  	}
   156  
   157  	// no idea, try to resolve as a vanity import
   158  	importpath, vcs, reporoot, err := ParseMetadata(path, insecure)
   159  	if err != nil {
   160  		return nil, "", err
   161  	}
   162  	u, err = url.Parse(reporoot)
   163  	if err != nil {
   164  		return nil, "", err
   165  	}
   166  	extra := path[len(importpath):]
   167  	switch vcs {
   168  	case "git":
   169  		u.Path = u.Path[1:]
   170  		repo, err := Gitrepo(u, insecure, u.Scheme)
   171  		return repo, extra, err
   172  	case "hg":
   173  		u.Path = u.Path[1:]
   174  		repo, err := Hgrepo(u, insecure, u.Scheme)
   175  		return repo, extra, err
   176  	case "bzr":
   177  		repo, err := Bzrrepo(reporoot)
   178  		return repo, extra, err
   179  	default:
   180  		return nil, "", fmt.Errorf("unknown repository type: %q", vcs)
   181  	}
   182  }
   183  
   184  // Gitrepo returns a RemoteRepo representing a remote git repository.
   185  func Gitrepo(url *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) {
   186  	if len(schemes) == 0 {
   187  		schemes = []string{"ssh", "https", "git", "http"}
   188  	}
   189  	u, err := probeGitUrl(url, insecure, schemes)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	return &gitrepo{
   194  		url: u,
   195  	}, nil
   196  }
   197  
   198  func probeGitUrl(u *url.URL, insecure bool, schemes []string) (string, error) {
   199  	git := func(url *url.URL) error {
   200  		out, err := run("git", "ls-remote", url.String(), "HEAD")
   201  		if err != nil {
   202  			return err
   203  		}
   204  
   205  		if !bytes.Contains(out, []byte("HEAD")) {
   206  			return fmt.Errorf("not a git repo")
   207  		}
   208  		return nil
   209  	}
   210  	return probe(git, u, insecure, schemes...)
   211  }
   212  
   213  func probeHgUrl(u *url.URL, insecure bool, schemes []string) (string, error) {
   214  	hg := func(url *url.URL) error {
   215  		_, err := run("hg", "identify", url.String())
   216  		return err
   217  	}
   218  	return probe(hg, u, insecure, schemes...)
   219  }
   220  
   221  func probeBzrUrl(u string) error {
   222  	bzr := func(url *url.URL) error {
   223  		_, err := run("bzr", "info", url.String())
   224  		return err
   225  	}
   226  	url, err := url.Parse(u)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	_, err = probe(bzr, url, false, "https")
   231  	return err
   232  }
   233  
   234  // probe calls the supplied vcs function to probe a variety of url constructions.
   235  // If vcs returns non nil, it is assumed that the url is not a valid repo.
   236  func probe(vcs func(*url.URL) error, vcsUrl *url.URL, insecure bool, schemes ...string) (string, error) {
   237  	var unsuccessful []string
   238  	for _, scheme := range schemes {
   239  
   240  		// make copy of url and apply scheme
   241  		vcsUrl := *vcsUrl
   242  		vcsUrl.Scheme = scheme
   243  
   244  		switch vcsUrl.Scheme {
   245  		case "ssh":
   246  			vcsUrl.User = url.User("git")
   247  			if err := vcs(&vcsUrl); err == nil {
   248  				return vcsUrl.String(), nil
   249  			}
   250  		case "https":
   251  			if err := vcs(&vcsUrl); err == nil {
   252  				return vcsUrl.String(), nil
   253  			}
   254  
   255  		case "http", "git":
   256  			if !insecure {
   257  				fmt.Println("skipping insecure protocol:", vcsUrl.String())
   258  				continue
   259  			}
   260  			if err := vcs(&vcsUrl); err == nil {
   261  				return vcsUrl.String(), nil
   262  			}
   263  		default:
   264  			return "", fmt.Errorf("unsupported scheme: %v", vcsUrl.Scheme)
   265  		}
   266  		unsuccessful = append(unsuccessful, vcsUrl.String())
   267  	}
   268  	return "", fmt.Errorf("vcs probe failed, tried: %s", strings.Join(unsuccessful, ","))
   269  }
   270  
   271  // gitrepo is a git RemoteRepo.
   272  type gitrepo struct {
   273  
   274  	// remote repository url, see man 1 git-clone
   275  	url string
   276  }
   277  
   278  func (g *gitrepo) URL() string {
   279  	return g.url
   280  }
   281  
   282  // Checkout fetchs the remote branch, tag, or revision. If more than one is
   283  // supplied, an error is returned. If the branch is blank,
   284  // then the default remote branch will be used. If the branch is "HEAD", an
   285  // error will be returned.
   286  func (g *gitrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
   287  	if branch == "HEAD" {
   288  		return nil, fmt.Errorf("cannot update %q as it has been previously fetched with -tag or -revision. Please use gb vendor delete then fetch again.", g.url)
   289  	}
   290  	if !atMostOne(branch, tag, revision) {
   291  		return nil, fmt.Errorf("only one of branch, tag or revision may be supplied")
   292  	}
   293  	dir, err := mktmp()
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	wc := workingcopy{
   298  		path: dir,
   299  	}
   300  
   301  	args := []string{
   302  		"clone",
   303  		"-q", // silence progress report to stderr
   304  		g.url,
   305  		dir,
   306  	}
   307  	if branch != "" {
   308  		args = append(args, "--branch", branch)
   309  	}
   310  
   311  	if _, err := run("git", args...); err != nil {
   312  		wc.Destroy()
   313  		return nil, err
   314  	}
   315  
   316  	if revision != "" || tag != "" {
   317  		if err := runOutPath(os.Stderr, dir, "git", "checkout", "-q", oneOf(revision, tag)); err != nil {
   318  			wc.Destroy()
   319  			return nil, err
   320  		}
   321  	}
   322  
   323  	return &GitClone{wc}, nil
   324  }
   325  
   326  type workingcopy struct {
   327  	path string
   328  }
   329  
   330  func (w workingcopy) Dir() string { return w.path }
   331  
   332  func (w workingcopy) Destroy() error {
   333  	if err := fileutils.RemoveAll(w.path); err != nil {
   334  		return err
   335  	}
   336  	parent := filepath.Dir(w.path)
   337  	return cleanPath(parent)
   338  }
   339  
   340  // GitClone is a git WorkingCopy.
   341  type GitClone struct {
   342  	workingcopy
   343  }
   344  
   345  func (g *GitClone) Revision() (string, error) {
   346  	rev, err := runPath(g.path, "git", "rev-parse", "HEAD")
   347  	return strings.TrimSpace(string(rev)), err
   348  }
   349  
   350  func (g *GitClone) Branch() (string, error) {
   351  	rev, err := runPath(g.path, "git", "rev-parse", "--abbrev-ref", "HEAD")
   352  	return strings.TrimSpace(string(rev)), err
   353  }
   354  
   355  // Hgrepo returns a RemoteRepo representing a remote git repository.
   356  func Hgrepo(u *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) {
   357  	if len(schemes) == 0 {
   358  		schemes = []string{"https", "http"}
   359  	}
   360  	url, err := probeHgUrl(u, insecure, schemes)
   361  	if err != nil {
   362  		return nil, err
   363  	}
   364  	return &hgrepo{
   365  		url: url,
   366  	}, nil
   367  }
   368  
   369  // hgrepo is a Mercurial repo.
   370  type hgrepo struct {
   371  
   372  	// remote repository url, see man 1 hg
   373  	url string
   374  }
   375  
   376  func (h *hgrepo) URL() string { return h.url }
   377  
   378  func (h *hgrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
   379  	if !atMostOne(tag, revision) {
   380  		return nil, fmt.Errorf("only one of tag or revision may be supplied")
   381  	}
   382  	dir, err := mktmp()
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  	args := []string{
   387  		"clone",
   388  		h.url,
   389  		dir,
   390  		"--noninteractive",
   391  	}
   392  
   393  	if branch != "" {
   394  		args = append(args, "--branch", branch)
   395  	}
   396  	if err := runOut(os.Stderr, "hg", args...); err != nil {
   397  		fileutils.RemoveAll(dir)
   398  		return nil, err
   399  	}
   400  	if revision != "" {
   401  		if err := runOut(os.Stderr, "hg", "--cwd", dir, "update", "-r", revision); err != nil {
   402  			fileutils.RemoveAll(dir)
   403  			return nil, err
   404  		}
   405  	}
   406  
   407  	return &HgClone{
   408  		workingcopy{
   409  			path: dir,
   410  		},
   411  	}, nil
   412  }
   413  
   414  // HgClone is a mercurial WorkingCopy.
   415  type HgClone struct {
   416  	workingcopy
   417  }
   418  
   419  func (h *HgClone) Revision() (string, error) {
   420  	rev, err := run("hg", "--cwd", h.path, "id", "-i")
   421  	return strings.TrimSpace(string(rev)), err
   422  }
   423  
   424  func (h *HgClone) Branch() (string, error) {
   425  	rev, err := run("hg", "--cwd", h.path, "branch")
   426  	return strings.TrimSpace(string(rev)), err
   427  }
   428  
   429  // Bzrrepo returns a RemoteRepo representing a remote bzr repository.
   430  func Bzrrepo(url string) (RemoteRepo, error) {
   431  	if err := probeBzrUrl(url); err != nil {
   432  		return nil, err
   433  	}
   434  	return &bzrrepo{
   435  		url: url,
   436  	}, nil
   437  }
   438  
   439  // bzrrepo is a bzr RemoteRepo.
   440  type bzrrepo struct {
   441  
   442  	// remote repository url
   443  	url string
   444  }
   445  
   446  func (b *bzrrepo) URL() string {
   447  	return b.url
   448  }
   449  
   450  func (b *bzrrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
   451  	if !atMostOne(tag, revision) {
   452  		return nil, fmt.Errorf("only one of tag or revision may be supplied")
   453  	}
   454  	dir, err := mktmp()
   455  	if err != nil {
   456  		return nil, err
   457  	}
   458  	wc := filepath.Join(dir, "wc")
   459  	if err := runOut(os.Stderr, "bzr", "branch", b.url, wc); err != nil {
   460  		fileutils.RemoveAll(dir)
   461  		return nil, err
   462  	}
   463  
   464  	return &BzrClone{
   465  		workingcopy{
   466  			path: wc,
   467  		},
   468  	}, nil
   469  }
   470  
   471  // BzrClone is a bazaar WorkingCopy.
   472  type BzrClone struct {
   473  	workingcopy
   474  }
   475  
   476  func (b *BzrClone) Revision() (string, error) {
   477  	return "1", nil
   478  }
   479  
   480  func (b *BzrClone) Branch() (string, error) {
   481  	return "master", nil
   482  }
   483  
   484  func cleanPath(path string) error {
   485  	if files, _ := ioutil.ReadDir(path); len(files) > 0 || filepath.Base(path) == "src" {
   486  		return nil
   487  	}
   488  	parent := filepath.Dir(path)
   489  	if err := fileutils.RemoveAll(path); err != nil {
   490  		return err
   491  	}
   492  	return cleanPath(parent)
   493  }
   494  
   495  func mkdir(path string) error {
   496  	return os.MkdirAll(path, 0755)
   497  }
   498  
   499  func mktmp() (string, error) {
   500  	return ioutil.TempDir("", "gb-vendor-")
   501  }
   502  
   503  func run(c string, args ...string) ([]byte, error) {
   504  	var buf bytes.Buffer
   505  	err := runOut(&buf, c, args...)
   506  	return buf.Bytes(), err
   507  }
   508  
   509  func runOut(w io.Writer, c string, args ...string) error {
   510  	cmd := exec.Command(c, args...)
   511  	cmd.Stdin = nil
   512  	cmd.Stdout = w
   513  	cmd.Stderr = os.Stderr
   514  	return cmd.Run()
   515  }
   516  
   517  func runPath(path string, c string, args ...string) ([]byte, error) {
   518  	var buf bytes.Buffer
   519  	err := runOutPath(&buf, path, c, args...)
   520  	return buf.Bytes(), err
   521  }
   522  
   523  func runOutPath(w io.Writer, path string, c string, args ...string) error {
   524  	cmd := exec.Command(c, args...)
   525  	cmd.Dir = path
   526  	cmd.Stdin = nil
   527  	cmd.Stdout = w
   528  	cmd.Stderr = os.Stderr
   529  	return cmd.Run()
   530  }
   531  
   532  // atMostOne returns true if no more than one string supplied is not empty.
   533  func atMostOne(args ...string) bool {
   534  	var c int
   535  	for _, arg := range args {
   536  		if arg != "" {
   537  			c++
   538  		}
   539  	}
   540  	return c < 2
   541  }
   542  
   543  // oneof returns the first non empty string
   544  func oneOf(args ...string) string {
   545  	for _, arg := range args {
   546  		if arg != "" {
   547  			return arg
   548  		}
   549  	}
   550  	return ""
   551  }