blob: eb52e84bd40fd0ac3e006fe932d5ebb854eb90c2 [file] [log] [blame]
package vendor
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/constabulary/gb/fileutils"
)
// RemoteRepo describes a remote dvcs repository.
type RemoteRepo interface {
// Checkout checks out a specific branch, tag, or revision.
// The interpretation of these three values is impementation
// specific.
Checkout(branch, tag, revision string) (WorkingCopy, error)
// URL returns the URL the clone was taken from. It should
// only be called after Clone.
URL() string
}
// WorkingCopy represents a local copy of a remote dvcs repository.
type WorkingCopy interface {
// Dir is the root of this working copy.
Dir() string
// Revision returns the revision of this working copy.
Revision() (string, error)
// Branch returns the branch to which this working copy belongs.
Branch() (string, error)
// Destroy removes the working copy and cleans path to the working copy.
Destroy() error
}
var (
ghregex = regexp.MustCompile(`^(?P<root>github\.com/([A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`)
bbregex = regexp.MustCompile(`^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`)
lpregex = regexp.MustCompile(`^launchpad.net/([A-Za-z0-9-._]+)(/[A-Za-z0-9-._]+)?(/.+)?`)
gcregex = regexp.MustCompile(`^(?P<root>code\.google\.com/[pr]/(?P<project>[a-z0-9\-]+)(\.(?P<subrepo>[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`)
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_.\-~]+)*$`)
)
// DeduceRemoteRepo takes a potential import path and returns a RemoteRepo
// representing the remote location of the source of an import path.
// Remote repositories can be bare import paths, or urls including a checkout scheme.
// If deduction would cause traversal of an insecure host, a message will be
// printed and the travelsal path will be ignored.
func DeduceRemoteRepo(path string, insecure bool) (RemoteRepo, string, error) {
u, err := url.Parse(path)
if err != nil {
return nil, "", fmt.Errorf("%q is not a valid import path", path)
}
var schemes []string
if u.Scheme != "" {
schemes = append(schemes, u.Scheme)
}
path = u.Host + u.Path
if !regexp.MustCompile(`^([A-Za-z0-9-]+)(\.[A-Za-z0-9-]+)+(/[A-Za-z0-9-_.~]+)*$`).MatchString(path) {
return nil, "", fmt.Errorf("%q is not a valid import path", path)
}
switch {
case ghregex.MatchString(path):
v := ghregex.FindStringSubmatch(path)
url := &url.URL{
Host: "github.com",
Path: v[2],
}
repo, err := Gitrepo(url, insecure, schemes...)
return repo, v[0][len(v[1]):], err
case bbregex.MatchString(path):
v := bbregex.FindStringSubmatch(path)
url := &url.URL{
Host: "bitbucket.org",
Path: v[2],
}
repo, err := Gitrepo(url, insecure, schemes...)
if err == nil {
return repo, v[0][len(v[1]):], nil
}
repo, err = Hgrepo(url, insecure)
if err == nil {
return repo, v[0][len(v[1]):], nil
}
return nil, "", fmt.Errorf("unknown repository type")
case gcregex.MatchString(path):
v := gcregex.FindStringSubmatch(path)
url := &url.URL{
Host: "code.google.com",
Path: "p/" + v[2],
}
repo, err := Hgrepo(url, insecure, schemes...)
if err == nil {
return repo, v[0][len(v[1]):], nil
}
repo, err = Gitrepo(url, insecure, schemes...)
if err == nil {
return repo, v[0][len(v[1]):], nil
}
return nil, "", fmt.Errorf("unknown repository type")
case lpregex.MatchString(path):
v := lpregex.FindStringSubmatch(path)
v = append(v, "", "")
if v[2] == "" {
// launchpad.net/project"
repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%v", v[1]))
return repo, "", err
}
// launchpad.net/project/series"
repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%s/%s", v[1], v[2]))
return repo, v[3], err
}
// try the general syntax
if genericre.MatchString(path) {
v := genericre.FindStringSubmatch(path)
switch v[5] {
case "git":
x := strings.SplitN(v[1], "/", 2)
url := &url.URL{
Host: x[0],
Path: x[1],
}
repo, err := Gitrepo(url, insecure, schemes...)
return repo, v[6], err
case "hg":
x := strings.SplitN(v[1], "/", 2)
url := &url.URL{
Host: x[0],
Path: x[1],
}
repo, err := Hgrepo(url, insecure, schemes...)
return repo, v[6], err
case "bzr":
repo, err := Bzrrepo("https://" + v[1])
return repo, v[6], err
default:
return nil, "", fmt.Errorf("unknown repository type: %q", v[5])
}
}
// no idea, try to resolve as a vanity import
importpath, vcs, reporoot, err := ParseMetadata(path, insecure)
if err != nil {
return nil, "", err
}
u, err = url.Parse(reporoot)
if err != nil {
return nil, "", err
}
extra := path[len(importpath):]
switch vcs {
case "git":
u.Path = u.Path[1:]
repo, err := Gitrepo(u, insecure, u.Scheme)
return repo, extra, err
case "hg":
u.Path = u.Path[1:]
repo, err := Hgrepo(u, insecure, u.Scheme)
return repo, extra, err
case "bzr":
repo, err := Bzrrepo(reporoot)
return repo, extra, err
default:
return nil, "", fmt.Errorf("unknown repository type: %q", vcs)
}
}
// Gitrepo returns a RemoteRepo representing a remote git repository.
func Gitrepo(url *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) {
if len(schemes) == 0 {
schemes = []string{"https", "git", "ssh", "http"}
}
u, err := probeGitUrl(url, insecure, schemes)
if err != nil {
return nil, err
}
return &gitrepo{
url: u,
}, nil
}
func probeGitUrl(u *url.URL, insecure bool, schemes []string) (string, error) {
git := func(url *url.URL) error {
out, err := run("git", "ls-remote", url.String(), "HEAD")
if err != nil {
return err
}
if !bytes.Contains(out, []byte("HEAD")) {
return fmt.Errorf("not a git repo")
}
return nil
}
return probe(git, u, insecure, schemes...)
}
func probeHgUrl(u *url.URL, insecure bool, schemes []string) (string, error) {
hg := func(url *url.URL) error {
_, err := run("hg", "identify", url.String())
return err
}
return probe(hg, u, insecure, schemes...)
}
func probeBzrUrl(u string) error {
bzr := func(url *url.URL) error {
_, err := run("bzr", "info", url.String())
return err
}
url, err := url.Parse(u)
if err != nil {
return err
}
_, err = probe(bzr, url, false, "https")
return err
}
// probe calls the supplied vcs function to probe a variety of url constructions.
// If vcs returns non nil, it is assumed that the url is not a valid repo.
func probe(vcs func(*url.URL) error, url *url.URL, insecure bool, schemes ...string) (string, error) {
var unsuccessful []string
for _, scheme := range schemes {
// make copy of url and apply scheme
url := *url
url.Scheme = scheme
switch url.Scheme {
case "https", "ssh":
if err := vcs(&url); err == nil {
return url.String(), nil
}
case "http", "git":
if !insecure {
log.Printf("skipping insecure protocol: %s", url.String())
continue
}
if err := vcs(&url); err == nil {
return url.String(), nil
}
default:
return "", fmt.Errorf("unsupported scheme: %v", url.Scheme)
}
unsuccessful = append(unsuccessful, url.String())
}
return "", fmt.Errorf("vcs probe failed, tried: %s", strings.Join(unsuccessful, ","))
}
// gitrepo is a git RemoteRepo.
type gitrepo struct {
// remote repository url, see man 1 git-clone
url string
}
func (g *gitrepo) URL() string {
return g.url
}
// Checkout fetchs the remote branch, tag, or revision. If more than one is
// supplied, an error is returned. If the branch is blank,
// then the default remote branch will be used. If the branch is "HEAD", an
// error will be returned.
func (g *gitrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
if branch == "HEAD" {
return nil, fmt.Errorf("cannot update %q as it has been previously fetched with -tag or -revision. Please use gvt delete then fetch again.", g.url)
}
if !atMostOne(branch, tag, revision) {
return nil, fmt.Errorf("only one of branch, tag or revision may be supplied")
}
dir, err := mktmp()
if err != nil {
return nil, err
}
wc := workingcopy{
path: dir,
}
args := []string{
"clone",
"-q", // silence progress report to stderr
g.url,
dir,
}
if branch != "" {
args = append(args, "--branch", branch)
}
if _, err := run("git", args...); err != nil {
wc.Destroy()
return nil, err
}
if revision != "" || tag != "" {
if err := runOutPath(os.Stderr, dir, "git", "checkout", "-q", oneOf(revision, tag)); err != nil {
wc.Destroy()
return nil, err
}
}
return &GitClone{wc}, nil
}
type workingcopy struct {
path string
}
func (w workingcopy) Dir() string { return w.path }
func (w workingcopy) Destroy() error {
if err := fileutils.RemoveAll(w.path); err != nil {
return err
}
parent := filepath.Dir(w.path)
return cleanPath(parent)
}
// GitClone is a git WorkingCopy.
type GitClone struct {
workingcopy
}
func (g *GitClone) Revision() (string, error) {
rev, err := runPath(g.path, "git", "rev-parse", "HEAD")
return strings.TrimSpace(string(rev)), err
}
func (g *GitClone) Branch() (string, error) {
rev, err := runPath(g.path, "git", "rev-parse", "--abbrev-ref", "HEAD")
return strings.TrimSpace(string(rev)), err
}
// Hgrepo returns a RemoteRepo representing a remote git repository.
func Hgrepo(u *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) {
if len(schemes) == 0 {
schemes = []string{"https", "http"}
}
url, err := probeHgUrl(u, insecure, schemes)
if err != nil {
return nil, err
}
return &hgrepo{
url: url,
}, nil
}
// hgrepo is a Mercurial repo.
type hgrepo struct {
// remote repository url, see man 1 hg
url string
}
func (h *hgrepo) URL() string { return h.url }
func (h *hgrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
if !atMostOne(tag, revision) {
return nil, fmt.Errorf("only one of tag or revision may be supplied")
}
dir, err := mktmp()
if err != nil {
return nil, err
}
args := []string{
"clone",
h.url,
dir,
"--noninteractive",
}
if branch != "" {
args = append(args, "--branch", branch)
}
if err := runOut(os.Stderr, "hg", args...); err != nil {
fileutils.RemoveAll(dir)
return nil, err
}
if revision != "" {
if err := runOut(os.Stderr, "hg", "--cwd", dir, "update", "-r", revision); err != nil {
fileutils.RemoveAll(dir)
return nil, err
}
}
return &HgClone{
workingcopy{
path: dir,
},
}, nil
}
// HgClone is a mercurial WorkingCopy.
type HgClone struct {
workingcopy
}
func (h *HgClone) Revision() (string, error) {
rev, err := run("hg", "--cwd", h.path, "id", "-i")
return strings.TrimSpace(string(rev)), err
}
func (h *HgClone) Branch() (string, error) {
rev, err := run("hg", "--cwd", h.path, "branch")
return strings.TrimSpace(string(rev)), err
}
// Bzrrepo returns a RemoteRepo representing a remote bzr repository.
func Bzrrepo(url string) (RemoteRepo, error) {
if err := probeBzrUrl(url); err != nil {
return nil, err
}
return &bzrrepo{
url: url,
}, nil
}
// bzrrepo is a bzr RemoteRepo.
type bzrrepo struct {
// remote repository url
url string
}
func (b *bzrrepo) URL() string {
return b.url
}
func (b *bzrrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
if !atMostOne(tag, revision) {
return nil, fmt.Errorf("only one of tag or revision may be supplied")
}
dir, err := mktmp()
if err != nil {
return nil, err
}
wc := filepath.Join(dir, "wc")
if err := runOut(os.Stderr, "bzr", "branch", b.url, wc); err != nil {
fileutils.RemoveAll(dir)
return nil, err
}
return &BzrClone{
workingcopy{
path: wc,
},
}, nil
}
// BzrClone is a bazaar WorkingCopy.
type BzrClone struct {
workingcopy
}
func (b *BzrClone) Revision() (string, error) {
return "1", nil
}
func (b *BzrClone) Branch() (string, error) {
return "master", nil
}
func cleanPath(path string) error {
if files, _ := ioutil.ReadDir(path); len(files) > 0 || filepath.Base(path) == "src" {
return nil
}
parent := filepath.Dir(path)
if err := fileutils.RemoveAll(path); err != nil {
return err
}
return cleanPath(parent)
}
func mkdir(path string) error {
return os.MkdirAll(path, 0755)
}
func mktmp() (string, error) {
return ioutil.TempDir("", "gvt-")
}
func run(c string, args ...string) ([]byte, error) {
var buf bytes.Buffer
err := runOut(&buf, c, args...)
return buf.Bytes(), err
}
func runOut(w io.Writer, c string, args ...string) error {
cmd := exec.Command(c, args...)
cmd.Stdin = nil
cmd.Stdout = w
cmd.Stderr = os.Stderr
return cmd.Run()
}
func runPath(path string, c string, args ...string) ([]byte, error) {
var buf bytes.Buffer
err := runOutPath(&buf, path, c, args...)
return buf.Bytes(), err
}
func runOutPath(w io.Writer, path string, c string, args ...string) error {
cmd := exec.Command(c, args...)
cmd.Dir = path
cmd.Stdin = nil
cmd.Stdout = w
cmd.Stderr = os.Stderr
return cmd.Run()
}
// atMostOne returns true if no more than one string supplied is not empty.
func atMostOne(args ...string) bool {
var c int
for _, arg := range args {
if arg != "" {
c++
}
}
return c < 2
}
// oneof returns the first non empty string
func oneOf(args ...string) string {
for _, arg := range args {
if arg != "" {
return arg
}
}
return ""
}