github.com/jd-ly/tools@v0.5.7/go/vcs/vcs.go (about)

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