...

Source file src/golang.org/x/tools/go/vcs/vcs.go

Documentation: golang.org/x/tools/go/vcs

     1  // Copyright 2012 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 vcs exposes functions for resolving import paths
     6  // and using version control systems, which can be used to
     7  // implement behavior similar to the standard "go get" command.
     8  //
     9  // This package is a copy of internal code in package cmd/go/internal/get,
    10  // modified to make the identifiers exported. It's provided here
    11  // for developers who want to write tools with similar semantics.
    12  // It needs to be manually kept in sync with upstream when changes are
    13  // made to cmd/go/internal/get; see https://golang.org/issue/11490.
    14  package vcs // import "golang.org/x/tools/go/vcs"
    15  
    16  import (
    17  	"bytes"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	exec "golang.org/x/sys/execabs"
    22  	"log"
    23  	"net/url"
    24  	"os"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strconv"
    28  	"strings"
    29  )
    30  
    31  // Verbose enables verbose operation logging.
    32  var Verbose bool
    33  
    34  // ShowCmd controls whether VCS commands are printed.
    35  var ShowCmd bool
    36  
    37  // A Cmd describes how to use a version control system
    38  // like Mercurial, Git, or Subversion.
    39  type Cmd struct {
    40  	Name string
    41  	Cmd  string // name of binary to invoke command
    42  
    43  	CreateCmd   string // command to download a fresh copy of a repository
    44  	DownloadCmd string // command to download updates into an existing repository
    45  
    46  	TagCmd         []TagCmd // commands to list tags
    47  	TagLookupCmd   []TagCmd // commands to lookup tags before running tagSyncCmd
    48  	TagSyncCmd     string   // command to sync to specific tag
    49  	TagSyncDefault string   // command to sync to default tag
    50  
    51  	LogCmd string // command to list repository changelogs in an XML format
    52  
    53  	Scheme  []string
    54  	PingCmd string
    55  }
    56  
    57  // A TagCmd describes a command to list available tags
    58  // that can be passed to Cmd.TagSyncCmd.
    59  type TagCmd struct {
    60  	Cmd     string // command to list tags
    61  	Pattern string // regexp to extract tags from list
    62  }
    63  
    64  // vcsList lists the known version control systems
    65  var vcsList = []*Cmd{
    66  	vcsHg,
    67  	vcsGit,
    68  	vcsSvn,
    69  	vcsBzr,
    70  }
    71  
    72  // ByCmd returns the version control system for the given
    73  // command name (hg, git, svn, bzr).
    74  func ByCmd(cmd string) *Cmd {
    75  	for _, vcs := range vcsList {
    76  		if vcs.Cmd == cmd {
    77  			return vcs
    78  		}
    79  	}
    80  	return nil
    81  }
    82  
    83  // vcsHg describes how to use Mercurial.
    84  var vcsHg = &Cmd{
    85  	Name: "Mercurial",
    86  	Cmd:  "hg",
    87  
    88  	CreateCmd:   "clone -U {repo} {dir}",
    89  	DownloadCmd: "pull",
    90  
    91  	// We allow both tag and branch names as 'tags'
    92  	// for selecting a version.  This lets people have
    93  	// a go.release.r60 branch and a go1 branch
    94  	// and make changes in both, without constantly
    95  	// editing .hgtags.
    96  	TagCmd: []TagCmd{
    97  		{"tags", `^(\S+)`},
    98  		{"branches", `^(\S+)`},
    99  	},
   100  	TagSyncCmd:     "update -r {tag}",
   101  	TagSyncDefault: "update default",
   102  
   103  	LogCmd: "log --encoding=utf-8 --limit={limit} --template={template}",
   104  
   105  	Scheme:  []string{"https", "http", "ssh"},
   106  	PingCmd: "identify {scheme}://{repo}",
   107  }
   108  
   109  // vcsGit describes how to use Git.
   110  var vcsGit = &Cmd{
   111  	Name: "Git",
   112  	Cmd:  "git",
   113  
   114  	CreateCmd:   "clone {repo} {dir}",
   115  	DownloadCmd: "pull --ff-only",
   116  
   117  	TagCmd: []TagCmd{
   118  		// tags/xxx matches a git tag named xxx
   119  		// origin/xxx matches a git branch named xxx on the default remote repository
   120  		{"show-ref", `(?:tags|origin)/(\S+)$`},
   121  	},
   122  	TagLookupCmd: []TagCmd{
   123  		{"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
   124  	},
   125  	TagSyncCmd:     "checkout {tag}",
   126  	TagSyncDefault: "checkout master",
   127  
   128  	Scheme:  []string{"git", "https", "http", "git+ssh"},
   129  	PingCmd: "ls-remote {scheme}://{repo}",
   130  }
   131  
   132  // vcsBzr describes how to use Bazaar.
   133  var vcsBzr = &Cmd{
   134  	Name: "Bazaar",
   135  	Cmd:  "bzr",
   136  
   137  	CreateCmd: "branch {repo} {dir}",
   138  
   139  	// Without --overwrite bzr will not pull tags that changed.
   140  	// Replace by --overwrite-tags after http://pad.lv/681792 goes in.
   141  	DownloadCmd: "pull --overwrite",
   142  
   143  	TagCmd:         []TagCmd{{"tags", `^(\S+)`}},
   144  	TagSyncCmd:     "update -r {tag}",
   145  	TagSyncDefault: "update -r revno:-1",
   146  
   147  	Scheme:  []string{"https", "http", "bzr", "bzr+ssh"},
   148  	PingCmd: "info {scheme}://{repo}",
   149  }
   150  
   151  // vcsSvn describes how to use Subversion.
   152  var vcsSvn = &Cmd{
   153  	Name: "Subversion",
   154  	Cmd:  "svn",
   155  
   156  	CreateCmd:   "checkout {repo} {dir}",
   157  	DownloadCmd: "update",
   158  
   159  	// There is no tag command in subversion.
   160  	// The branch information is all in the path names.
   161  
   162  	LogCmd: "log --xml --limit={limit}",
   163  
   164  	Scheme:  []string{"https", "http", "svn", "svn+ssh"},
   165  	PingCmd: "info {scheme}://{repo}",
   166  }
   167  
   168  func (v *Cmd) String() string {
   169  	return v.Name
   170  }
   171  
   172  // run runs the command line cmd in the given directory.
   173  // keyval is a list of key, value pairs.  run expands
   174  // instances of {key} in cmd into value, but only after
   175  // splitting cmd into individual arguments.
   176  // If an error occurs, run prints the command line and the
   177  // command's combined stdout+stderr to standard error.
   178  // Otherwise run discards the command's output.
   179  func (v *Cmd) run(dir string, cmd string, keyval ...string) error {
   180  	_, err := v.run1(dir, cmd, keyval, true)
   181  	return err
   182  }
   183  
   184  // runVerboseOnly is like run but only generates error output to standard error in verbose mode.
   185  func (v *Cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error {
   186  	_, err := v.run1(dir, cmd, keyval, false)
   187  	return err
   188  }
   189  
   190  // runOutput is like run but returns the output of the command.
   191  func (v *Cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) {
   192  	return v.run1(dir, cmd, keyval, true)
   193  }
   194  
   195  // run1 is the generalized implementation of run and runOutput.
   196  func (v *Cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) {
   197  	m := make(map[string]string)
   198  	for i := 0; i < len(keyval); i += 2 {
   199  		m[keyval[i]] = keyval[i+1]
   200  	}
   201  	args := strings.Fields(cmdline)
   202  	for i, arg := range args {
   203  		args[i] = expand(m, arg)
   204  	}
   205  
   206  	_, err := exec.LookPath(v.Cmd)
   207  	if err != nil {
   208  		fmt.Fprintf(os.Stderr,
   209  			"go: missing %s command. See http://golang.org/s/gogetcmd\n",
   210  			v.Name)
   211  		return nil, err
   212  	}
   213  
   214  	cmd := exec.Command(v.Cmd, args...)
   215  	cmd.Dir = dir
   216  	cmd.Env = envForDir(cmd.Dir)
   217  	if ShowCmd {
   218  		fmt.Printf("cd %s\n", dir)
   219  		fmt.Printf("%s %s\n", v.Cmd, strings.Join(args, " "))
   220  	}
   221  	var buf bytes.Buffer
   222  	cmd.Stdout = &buf
   223  	cmd.Stderr = &buf
   224  	err = cmd.Run()
   225  	out := buf.Bytes()
   226  	if err != nil {
   227  		if verbose || Verbose {
   228  			fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.Cmd, strings.Join(args, " "))
   229  			os.Stderr.Write(out)
   230  		}
   231  		return nil, err
   232  	}
   233  	return out, nil
   234  }
   235  
   236  // Ping pings the repo to determine if scheme used is valid.
   237  // This repo must be pingable with this scheme and VCS.
   238  func (v *Cmd) Ping(scheme, repo string) error {
   239  	return v.runVerboseOnly(".", v.PingCmd, "scheme", scheme, "repo", repo)
   240  }
   241  
   242  // Create creates a new copy of repo in dir.
   243  // The parent of dir must exist; dir must not.
   244  func (v *Cmd) Create(dir, repo string) error {
   245  	return v.run(".", v.CreateCmd, "dir", dir, "repo", repo)
   246  }
   247  
   248  // CreateAtRev creates a new copy of repo in dir at revision rev.
   249  // The parent of dir must exist; dir must not.
   250  // rev must be a valid revision in repo.
   251  func (v *Cmd) CreateAtRev(dir, repo, rev string) error {
   252  	if err := v.Create(dir, repo); err != nil {
   253  		return err
   254  	}
   255  	return v.run(dir, v.TagSyncCmd, "tag", rev)
   256  }
   257  
   258  // Download downloads any new changes for the repo in dir.
   259  // dir must be a valid VCS repo compatible with v.
   260  func (v *Cmd) Download(dir string) error {
   261  	return v.run(dir, v.DownloadCmd)
   262  }
   263  
   264  // Tags returns the list of available tags for the repo in dir.
   265  // dir must be a valid VCS repo compatible with v.
   266  func (v *Cmd) Tags(dir string) ([]string, error) {
   267  	var tags []string
   268  	for _, tc := range v.TagCmd {
   269  		out, err := v.runOutput(dir, tc.Cmd)
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  		re := regexp.MustCompile(`(?m-s)` + tc.Pattern)
   274  		for _, m := range re.FindAllStringSubmatch(string(out), -1) {
   275  			tags = append(tags, m[1])
   276  		}
   277  	}
   278  	return tags, nil
   279  }
   280  
   281  // TagSync syncs the repo in dir to the named tag, which is either a
   282  // tag returned by Tags or the empty string (the default tag).
   283  // dir must be a valid VCS repo compatible with v and the tag must exist.
   284  func (v *Cmd) TagSync(dir, tag string) error {
   285  	if v.TagSyncCmd == "" {
   286  		return nil
   287  	}
   288  	if tag != "" {
   289  		for _, tc := range v.TagLookupCmd {
   290  			out, err := v.runOutput(dir, tc.Cmd, "tag", tag)
   291  			if err != nil {
   292  				return err
   293  			}
   294  			re := regexp.MustCompile(`(?m-s)` + tc.Pattern)
   295  			m := re.FindStringSubmatch(string(out))
   296  			if len(m) > 1 {
   297  				tag = m[1]
   298  				break
   299  			}
   300  		}
   301  	}
   302  	if tag == "" && v.TagSyncDefault != "" {
   303  		return v.run(dir, v.TagSyncDefault)
   304  	}
   305  	return v.run(dir, v.TagSyncCmd, "tag", tag)
   306  }
   307  
   308  // Log logs the changes for the repo in dir.
   309  // dir must be a valid VCS repo compatible with v.
   310  func (v *Cmd) Log(dir, logTemplate string) ([]byte, error) {
   311  	if err := v.Download(dir); err != nil {
   312  		return []byte{}, err
   313  	}
   314  
   315  	const N = 50 // how many revisions to grab
   316  	return v.runOutput(dir, v.LogCmd, "limit", strconv.Itoa(N), "template", logTemplate)
   317  }
   318  
   319  // LogAtRev logs the change for repo in dir at the rev revision.
   320  // dir must be a valid VCS repo compatible with v.
   321  // rev must be a valid revision for the repo in dir.
   322  func (v *Cmd) LogAtRev(dir, rev, logTemplate string) ([]byte, error) {
   323  	if err := v.Download(dir); err != nil {
   324  		return []byte{}, err
   325  	}
   326  
   327  	// Append revision flag to LogCmd.
   328  	logAtRevCmd := v.LogCmd + " --rev=" + rev
   329  	return v.runOutput(dir, logAtRevCmd, "limit", strconv.Itoa(1), "template", logTemplate)
   330  }
   331  
   332  // A vcsPath describes how to convert an import path into a
   333  // version control system and repository name.
   334  type vcsPath struct {
   335  	prefix string                              // prefix this description applies to
   336  	re     string                              // pattern for import path
   337  	repo   string                              // repository to use (expand with match of re)
   338  	vcs    string                              // version control system to use (expand with match of re)
   339  	check  func(match map[string]string) error // additional checks
   340  	ping   bool                                // ping for scheme to use to download repo
   341  
   342  	regexp *regexp.Regexp // cached compiled form of re
   343  }
   344  
   345  // FromDir inspects dir and its parents to determine the
   346  // version control system and code repository to use.
   347  // On return, root is the import path
   348  // corresponding to the root of the repository.
   349  func FromDir(dir, srcRoot string) (vcs *Cmd, root string, err error) {
   350  	// Clean and double-check that dir is in (a subdirectory of) srcRoot.
   351  	dir = filepath.Clean(dir)
   352  	srcRoot = filepath.Clean(srcRoot)
   353  	if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {
   354  		return nil, "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot)
   355  	}
   356  
   357  	var vcsRet *Cmd
   358  	var rootRet string
   359  
   360  	origDir := dir
   361  	for len(dir) > len(srcRoot) {
   362  		for _, vcs := range vcsList {
   363  			if _, err := os.Stat(filepath.Join(dir, "."+vcs.Cmd)); err == nil {
   364  				root := filepath.ToSlash(dir[len(srcRoot)+1:])
   365  				// Record first VCS we find, but keep looking,
   366  				// to detect mistakes like one kind of VCS inside another.
   367  				if vcsRet == nil {
   368  					vcsRet = vcs
   369  					rootRet = root
   370  					continue
   371  				}
   372  				// Allow .git inside .git, which can arise due to submodules.
   373  				if vcsRet == vcs && vcs.Cmd == "git" {
   374  					continue
   375  				}
   376  				// Otherwise, we have one VCS inside a different VCS.
   377  				return nil, "", fmt.Errorf("directory %q uses %s, but parent %q uses %s",
   378  					filepath.Join(srcRoot, rootRet), vcsRet.Cmd, filepath.Join(srcRoot, root), vcs.Cmd)
   379  			}
   380  		}
   381  
   382  		// Move to parent.
   383  		ndir := filepath.Dir(dir)
   384  		if len(ndir) >= len(dir) {
   385  			// Shouldn't happen, but just in case, stop.
   386  			break
   387  		}
   388  		dir = ndir
   389  	}
   390  
   391  	if vcsRet != nil {
   392  		return vcsRet, rootRet, nil
   393  	}
   394  
   395  	return nil, "", fmt.Errorf("directory %q is not using a known version control system", origDir)
   396  }
   397  
   398  // RepoRoot represents a version control system, a repo, and a root of
   399  // where to put it on disk.
   400  type RepoRoot struct {
   401  	VCS *Cmd
   402  
   403  	// Repo is the repository URL, including scheme.
   404  	Repo string
   405  
   406  	// Root is the import path corresponding to the root of the
   407  	// repository.
   408  	Root string
   409  }
   410  
   411  // RepoRootForImportPath analyzes importPath to determine the
   412  // version control system, and code repository to use.
   413  func RepoRootForImportPath(importPath string, verbose bool) (*RepoRoot, error) {
   414  	rr, err := RepoRootForImportPathStatic(importPath, "")
   415  	if err == errUnknownSite {
   416  		rr, err = RepoRootForImportDynamic(importPath, verbose)
   417  
   418  		// RepoRootForImportDynamic returns error detail
   419  		// that is irrelevant if the user didn't intend to use a
   420  		// dynamic import in the first place.
   421  		// Squelch it.
   422  		if err != nil {
   423  			if Verbose {
   424  				log.Printf("import %q: %v", importPath, err)
   425  			}
   426  			err = fmt.Errorf("unrecognized import path %q", importPath)
   427  		}
   428  	}
   429  
   430  	if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") {
   431  		// Do not allow wildcards in the repo root.
   432  		rr = nil
   433  		err = fmt.Errorf("cannot expand ... in %q", importPath)
   434  	}
   435  	return rr, err
   436  }
   437  
   438  var errUnknownSite = errors.New("dynamic lookup required to find mapping")
   439  
   440  // RepoRootForImportPathStatic attempts to map importPath to a
   441  // RepoRoot using the commonly-used VCS hosting sites in vcsPaths
   442  // (github.com/user/dir), or from a fully-qualified importPath already
   443  // containing its VCS type (foo.com/repo.git/dir)
   444  //
   445  // If scheme is non-empty, that scheme is forced.
   446  func RepoRootForImportPathStatic(importPath, scheme string) (*RepoRoot, error) {
   447  	if strings.Contains(importPath, "://") {
   448  		return nil, fmt.Errorf("invalid import path %q", importPath)
   449  	}
   450  	for _, srv := range vcsPaths {
   451  		if !strings.HasPrefix(importPath, srv.prefix) {
   452  			continue
   453  		}
   454  		m := srv.regexp.FindStringSubmatch(importPath)
   455  		if m == nil {
   456  			if srv.prefix != "" {
   457  				return nil, fmt.Errorf("invalid %s import path %q", srv.prefix, importPath)
   458  			}
   459  			continue
   460  		}
   461  
   462  		// Build map of named subexpression matches for expand.
   463  		match := map[string]string{
   464  			"prefix": srv.prefix,
   465  			"import": importPath,
   466  		}
   467  		for i, name := range srv.regexp.SubexpNames() {
   468  			if name != "" && match[name] == "" {
   469  				match[name] = m[i]
   470  			}
   471  		}
   472  		if srv.vcs != "" {
   473  			match["vcs"] = expand(match, srv.vcs)
   474  		}
   475  		if srv.repo != "" {
   476  			match["repo"] = expand(match, srv.repo)
   477  		}
   478  		if srv.check != nil {
   479  			if err := srv.check(match); err != nil {
   480  				return nil, err
   481  			}
   482  		}
   483  		vcs := ByCmd(match["vcs"])
   484  		if vcs == nil {
   485  			return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
   486  		}
   487  		if srv.ping {
   488  			if scheme != "" {
   489  				match["repo"] = scheme + "://" + match["repo"]
   490  			} else {
   491  				for _, scheme := range vcs.Scheme {
   492  					if vcs.Ping(scheme, match["repo"]) == nil {
   493  						match["repo"] = scheme + "://" + match["repo"]
   494  						break
   495  					}
   496  				}
   497  			}
   498  		}
   499  		rr := &RepoRoot{
   500  			VCS:  vcs,
   501  			Repo: match["repo"],
   502  			Root: match["root"],
   503  		}
   504  		return rr, nil
   505  	}
   506  	return nil, errUnknownSite
   507  }
   508  
   509  // RepoRootForImportDynamic finds a *RepoRoot for a custom domain that's not
   510  // statically known by RepoRootForImportPathStatic.
   511  //
   512  // This handles custom import paths like "name.tld/pkg/foo" or just "name.tld".
   513  func RepoRootForImportDynamic(importPath string, verbose bool) (*RepoRoot, error) {
   514  	slash := strings.Index(importPath, "/")
   515  	if slash < 0 {
   516  		slash = len(importPath)
   517  	}
   518  	host := importPath[:slash]
   519  	if !strings.Contains(host, ".") {
   520  		return nil, errors.New("import path doesn't contain a hostname")
   521  	}
   522  	urlStr, body, err := httpsOrHTTP(importPath)
   523  	if err != nil {
   524  		return nil, fmt.Errorf("http/https fetch: %v", err)
   525  	}
   526  	defer body.Close()
   527  	imports, err := parseMetaGoImports(body)
   528  	if err != nil {
   529  		return nil, fmt.Errorf("parsing %s: %v", importPath, err)
   530  	}
   531  	metaImport, err := matchGoImport(imports, importPath)
   532  	if err != nil {
   533  		if err != errNoMatch {
   534  			return nil, fmt.Errorf("parse %s: %v", urlStr, err)
   535  		}
   536  		return nil, fmt.Errorf("parse %s: no go-import meta tags", urlStr)
   537  	}
   538  	if verbose {
   539  		log.Printf("get %q: found meta tag %#v at %s", importPath, metaImport, urlStr)
   540  	}
   541  	// If the import was "uni.edu/bob/project", which said the
   542  	// prefix was "uni.edu" and the RepoRoot was "evilroot.com",
   543  	// make sure we don't trust Bob and check out evilroot.com to
   544  	// "uni.edu" yet (possibly overwriting/preempting another
   545  	// non-evil student).  Instead, first verify the root and see
   546  	// if it matches Bob's claim.
   547  	if metaImport.Prefix != importPath {
   548  		if verbose {
   549  			log.Printf("get %q: verifying non-authoritative meta tag", importPath)
   550  		}
   551  		urlStr0 := urlStr
   552  		urlStr, body, err = httpsOrHTTP(metaImport.Prefix)
   553  		if err != nil {
   554  			return nil, fmt.Errorf("fetch %s: %v", urlStr, err)
   555  		}
   556  		imports, err := parseMetaGoImports(body)
   557  		if err != nil {
   558  			return nil, fmt.Errorf("parsing %s: %v", importPath, err)
   559  		}
   560  		if len(imports) == 0 {
   561  			return nil, fmt.Errorf("fetch %s: no go-import meta tag", urlStr)
   562  		}
   563  		metaImport2, err := matchGoImport(imports, importPath)
   564  		if err != nil || metaImport != metaImport2 {
   565  			return nil, fmt.Errorf("%s and %s disagree about go-import for %s", urlStr0, urlStr, metaImport.Prefix)
   566  		}
   567  	}
   568  
   569  	if err := validateRepoRoot(metaImport.RepoRoot); err != nil {
   570  		return nil, fmt.Errorf("%s: invalid repo root %q: %v", urlStr, metaImport.RepoRoot, err)
   571  	}
   572  	rr := &RepoRoot{
   573  		VCS:  ByCmd(metaImport.VCS),
   574  		Repo: metaImport.RepoRoot,
   575  		Root: metaImport.Prefix,
   576  	}
   577  	if rr.VCS == nil {
   578  		return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, metaImport.VCS)
   579  	}
   580  	return rr, nil
   581  }
   582  
   583  // validateRepoRoot returns an error if repoRoot does not seem to be
   584  // a valid URL with scheme.
   585  func validateRepoRoot(repoRoot string) error {
   586  	url, err := url.Parse(repoRoot)
   587  	if err != nil {
   588  		return err
   589  	}
   590  	if url.Scheme == "" {
   591  		return errors.New("no scheme")
   592  	}
   593  	return nil
   594  }
   595  
   596  // metaImport represents the parsed <meta name="go-import"
   597  // content="prefix vcs reporoot" /> tags from HTML files.
   598  type metaImport struct {
   599  	Prefix, VCS, RepoRoot string
   600  }
   601  
   602  // errNoMatch is returned from matchGoImport when there's no applicable match.
   603  var errNoMatch = errors.New("no import match")
   604  
   605  // pathPrefix reports whether sub is a prefix of s,
   606  // only considering entire path components.
   607  func pathPrefix(s, sub string) bool {
   608  	// strings.HasPrefix is necessary but not sufficient.
   609  	if !strings.HasPrefix(s, sub) {
   610  		return false
   611  	}
   612  	// The remainder after the prefix must either be empty or start with a slash.
   613  	rem := s[len(sub):]
   614  	return rem == "" || rem[0] == '/'
   615  }
   616  
   617  // matchGoImport returns the metaImport from imports matching importPath.
   618  // An error is returned if there are multiple matches.
   619  // errNoMatch is returned if none match.
   620  func matchGoImport(imports []metaImport, importPath string) (_ metaImport, err error) {
   621  	match := -1
   622  	for i, im := range imports {
   623  		if !pathPrefix(importPath, im.Prefix) {
   624  			continue
   625  		}
   626  
   627  		if match != -1 {
   628  			err = fmt.Errorf("multiple meta tags match import path %q", importPath)
   629  			return
   630  		}
   631  		match = i
   632  	}
   633  	if match == -1 {
   634  		err = errNoMatch
   635  		return
   636  	}
   637  	return imports[match], nil
   638  }
   639  
   640  // expand rewrites s to replace {k} with match[k] for each key k in match.
   641  func expand(match map[string]string, s string) string {
   642  	for k, v := range match {
   643  		s = strings.Replace(s, "{"+k+"}", v, -1)
   644  	}
   645  	return s
   646  }
   647  
   648  // vcsPaths lists the known vcs paths.
   649  var vcsPaths = []*vcsPath{
   650  	// Github
   651  	{
   652  		prefix: "github.com/",
   653  		re:     `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[\p{L}0-9_.\-]+)*$`,
   654  		vcs:    "git",
   655  		repo:   "https://{root}",
   656  		check:  noVCSSuffix,
   657  	},
   658  
   659  	// Bitbucket
   660  	{
   661  		prefix: "bitbucket.org/",
   662  		re:     `^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
   663  		repo:   "https://{root}",
   664  		check:  bitbucketVCS,
   665  	},
   666  
   667  	// Launchpad
   668  	{
   669  		prefix: "launchpad.net/",
   670  		re:     `^(?P<root>launchpad\.net/((?P<project>[A-Za-z0-9_.\-]+)(?P<series>/[A-Za-z0-9_.\-]+)?|~[A-Za-z0-9_.\-]+/(\+junk|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
   671  		vcs:    "bzr",
   672  		repo:   "https://{root}",
   673  		check:  launchpadVCS,
   674  	},
   675  
   676  	// Git at OpenStack
   677  	{
   678  		prefix: "git.openstack.org",
   679  		re:     `^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/[A-Za-z0-9_.\-]+)*$`,
   680  		vcs:    "git",
   681  		repo:   "https://{root}",
   682  		check:  noVCSSuffix,
   683  	},
   684  
   685  	// General syntax for any server.
   686  	{
   687  		re:   `^(?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_.\-]+)*$`,
   688  		ping: true,
   689  	},
   690  }
   691  
   692  func init() {
   693  	// fill in cached regexps.
   694  	// Doing this eagerly discovers invalid regexp syntax
   695  	// without having to run a command that needs that regexp.
   696  	for _, srv := range vcsPaths {
   697  		srv.regexp = regexp.MustCompile(srv.re)
   698  	}
   699  }
   700  
   701  // noVCSSuffix checks that the repository name does not
   702  // end in .foo for any version control system foo.
   703  // The usual culprit is ".git".
   704  func noVCSSuffix(match map[string]string) error {
   705  	repo := match["repo"]
   706  	for _, vcs := range vcsList {
   707  		if strings.HasSuffix(repo, "."+vcs.Cmd) {
   708  			return fmt.Errorf("invalid version control suffix in %s path", match["prefix"])
   709  		}
   710  	}
   711  	return nil
   712  }
   713  
   714  // bitbucketVCS determines the version control system for a
   715  // Bitbucket repository, by using the Bitbucket API.
   716  func bitbucketVCS(match map[string]string) error {
   717  	if err := noVCSSuffix(match); err != nil {
   718  		return err
   719  	}
   720  
   721  	var resp struct {
   722  		SCM string `json:"scm"`
   723  	}
   724  	url := expand(match, "https://api.bitbucket.org/2.0/repositories/{bitname}?fields=scm")
   725  	data, err := httpGET(url)
   726  	if err != nil {
   727  		return err
   728  	}
   729  	if err := json.Unmarshal(data, &resp); err != nil {
   730  		return fmt.Errorf("decoding %s: %v", url, err)
   731  	}
   732  
   733  	if ByCmd(resp.SCM) != nil {
   734  		match["vcs"] = resp.SCM
   735  		if resp.SCM == "git" {
   736  			match["repo"] += ".git"
   737  		}
   738  		return nil
   739  	}
   740  
   741  	return fmt.Errorf("unable to detect version control system for bitbucket.org/ path")
   742  }
   743  
   744  // launchpadVCS solves the ambiguity for "lp.net/project/foo". In this case,
   745  // "foo" could be a series name registered in Launchpad with its own branch,
   746  // and it could also be the name of a directory within the main project
   747  // branch one level up.
   748  func launchpadVCS(match map[string]string) error {
   749  	if match["project"] == "" || match["series"] == "" {
   750  		return nil
   751  	}
   752  	_, err := httpGET(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format"))
   753  	if err != nil {
   754  		match["root"] = expand(match, "launchpad.net/{project}")
   755  		match["repo"] = expand(match, "https://{root}")
   756  	}
   757  	return nil
   758  }
   759  

View as plain text