Home | History | Annotate | Download | only in go
      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 main
      6 
      7 import (
      8 	"bytes"
      9 	"encoding/json"
     10 	"errors"
     11 	"fmt"
     12 	"internal/singleflight"
     13 	"log"
     14 	"net/url"
     15 	"os"
     16 	"os/exec"
     17 	"path/filepath"
     18 	"regexp"
     19 	"strings"
     20 	"sync"
     21 )
     22 
     23 // A vcsCmd describes how to use a version control system
     24 // like Mercurial, Git, or Subversion.
     25 type vcsCmd struct {
     26 	name string
     27 	cmd  string // name of binary to invoke command
     28 
     29 	createCmd   []string // commands to download a fresh copy of a repository
     30 	downloadCmd []string // commands to download updates into an existing repository
     31 
     32 	tagCmd         []tagCmd // commands to list tags
     33 	tagLookupCmd   []tagCmd // commands to lookup tags before running tagSyncCmd
     34 	tagSyncCmd     []string // commands to sync to specific tag
     35 	tagSyncDefault []string // commands to sync to default tag
     36 
     37 	scheme  []string
     38 	pingCmd string
     39 
     40 	remoteRepo  func(v *vcsCmd, rootDir string) (remoteRepo string, err error)
     41 	resolveRepo func(v *vcsCmd, rootDir, remoteRepo string) (realRepo string, err error)
     42 }
     43 
     44 var defaultSecureScheme = map[string]bool{
     45 	"https":   true,
     46 	"git+ssh": true,
     47 	"bzr+ssh": true,
     48 	"svn+ssh": true,
     49 	"ssh":     true,
     50 }
     51 
     52 func (v *vcsCmd) isSecure(repo string) bool {
     53 	u, err := url.Parse(repo)
     54 	if err != nil {
     55 		// If repo is not a URL, it's not secure.
     56 		return false
     57 	}
     58 	return v.isSecureScheme(u.Scheme)
     59 }
     60 
     61 func (v *vcsCmd) isSecureScheme(scheme string) bool {
     62 	switch v.cmd {
     63 	case "git":
     64 		// GIT_ALLOW_PROTOCOL is an environment variable defined by Git. It is a
     65 		// colon-separated list of schemes that are allowed to be used with git
     66 		// fetch/clone. Any scheme not mentioned will be considered insecure.
     67 		if allow := os.Getenv("GIT_ALLOW_PROTOCOL"); allow != "" {
     68 			for _, s := range strings.Split(allow, ":") {
     69 				if s == scheme {
     70 					return true
     71 				}
     72 			}
     73 			return false
     74 		}
     75 	}
     76 	return defaultSecureScheme[scheme]
     77 }
     78 
     79 // A tagCmd describes a command to list available tags
     80 // that can be passed to tagSyncCmd.
     81 type tagCmd struct {
     82 	cmd     string // command to list tags
     83 	pattern string // regexp to extract tags from list
     84 }
     85 
     86 // vcsList lists the known version control systems
     87 var vcsList = []*vcsCmd{
     88 	vcsHg,
     89 	vcsGit,
     90 	vcsSvn,
     91 	vcsBzr,
     92 }
     93 
     94 // vcsByCmd returns the version control system for the given
     95 // command name (hg, git, svn, bzr).
     96 func vcsByCmd(cmd string) *vcsCmd {
     97 	for _, vcs := range vcsList {
     98 		if vcs.cmd == cmd {
     99 			return vcs
    100 		}
    101 	}
    102 	return nil
    103 }
    104 
    105 // vcsHg describes how to use Mercurial.
    106 var vcsHg = &vcsCmd{
    107 	name: "Mercurial",
    108 	cmd:  "hg",
    109 
    110 	createCmd:   []string{"clone -U {repo} {dir}"},
    111 	downloadCmd: []string{"pull"},
    112 
    113 	// We allow both tag and branch names as 'tags'
    114 	// for selecting a version. This lets people have
    115 	// a go.release.r60 branch and a go1 branch
    116 	// and make changes in both, without constantly
    117 	// editing .hgtags.
    118 	tagCmd: []tagCmd{
    119 		{"tags", `^(\S+)`},
    120 		{"branches", `^(\S+)`},
    121 	},
    122 	tagSyncCmd:     []string{"update -r {tag}"},
    123 	tagSyncDefault: []string{"update default"},
    124 
    125 	scheme:     []string{"https", "http", "ssh"},
    126 	pingCmd:    "identify {scheme}://{repo}",
    127 	remoteRepo: hgRemoteRepo,
    128 }
    129 
    130 func hgRemoteRepo(vcsHg *vcsCmd, rootDir string) (remoteRepo string, err error) {
    131 	out, err := vcsHg.runOutput(rootDir, "paths default")
    132 	if err != nil {
    133 		return "", err
    134 	}
    135 	return strings.TrimSpace(string(out)), nil
    136 }
    137 
    138 // vcsGit describes how to use Git.
    139 var vcsGit = &vcsCmd{
    140 	name: "Git",
    141 	cmd:  "git",
    142 
    143 	createCmd:   []string{"clone {repo} {dir}", "-go-internal-cd {dir} submodule update --init --recursive"},
    144 	downloadCmd: []string{"pull --ff-only", "submodule update --init --recursive"},
    145 
    146 	tagCmd: []tagCmd{
    147 		// tags/xxx matches a git tag named xxx
    148 		// origin/xxx matches a git branch named xxx on the default remote repository
    149 		{"show-ref", `(?:tags|origin)/(\S+)$`},
    150 	},
    151 	tagLookupCmd: []tagCmd{
    152 		{"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
    153 	},
    154 	tagSyncCmd: []string{"checkout {tag}", "submodule update --init --recursive"},
    155 	// both createCmd and downloadCmd update the working dir.
    156 	// No need to do more here. We used to 'checkout master'
    157 	// but that doesn't work if the default branch is not named master.
    158 	// DO NOT add 'checkout master' here.
    159 	// See golang.org/issue/9032.
    160 	tagSyncDefault: []string{"submodule update --init --recursive"},
    161 
    162 	scheme:     []string{"git", "https", "http", "git+ssh", "ssh"},
    163 	pingCmd:    "ls-remote {scheme}://{repo}",
    164 	remoteRepo: gitRemoteRepo,
    165 }
    166 
    167 // scpSyntaxRe matches the SCP-like addresses used by Git to access
    168 // repositories by SSH.
    169 var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
    170 
    171 func gitRemoteRepo(vcsGit *vcsCmd, rootDir string) (remoteRepo string, err error) {
    172 	cmd := "config remote.origin.url"
    173 	errParse := errors.New("unable to parse output of git " + cmd)
    174 	errRemoteOriginNotFound := errors.New("remote origin not found")
    175 	outb, err := vcsGit.run1(rootDir, cmd, nil, false)
    176 	if err != nil {
    177 		// if it doesn't output any message, it means the config argument is correct,
    178 		// but the config value itself doesn't exist
    179 		if outb != nil && len(outb) == 0 {
    180 			return "", errRemoteOriginNotFound
    181 		}
    182 		return "", err
    183 	}
    184 	out := strings.TrimSpace(string(outb))
    185 
    186 	var repoURL *url.URL
    187 	if m := scpSyntaxRe.FindStringSubmatch(out); m != nil {
    188 		// Match SCP-like syntax and convert it to a URL.
    189 		// Eg, "git (a] github.com:user/repo" becomes
    190 		// "ssh://git (a] github.com/user/repo".
    191 		repoURL = &url.URL{
    192 			Scheme: "ssh",
    193 			User:   url.User(m[1]),
    194 			Host:   m[2],
    195 			Path:   m[3],
    196 		}
    197 	} else {
    198 		repoURL, err = url.Parse(out)
    199 		if err != nil {
    200 			return "", err
    201 		}
    202 	}
    203 
    204 	// Iterate over insecure schemes too, because this function simply
    205 	// reports the state of the repo. If we can't see insecure schemes then
    206 	// we can't report the actual repo URL.
    207 	for _, s := range vcsGit.scheme {
    208 		if repoURL.Scheme == s {
    209 			return repoURL.String(), nil
    210 		}
    211 	}
    212 	return "", errParse
    213 }
    214 
    215 // vcsBzr describes how to use Bazaar.
    216 var vcsBzr = &vcsCmd{
    217 	name: "Bazaar",
    218 	cmd:  "bzr",
    219 
    220 	createCmd: []string{"branch {repo} {dir}"},
    221 
    222 	// Without --overwrite bzr will not pull tags that changed.
    223 	// Replace by --overwrite-tags after http://pad.lv/681792 goes in.
    224 	downloadCmd: []string{"pull --overwrite"},
    225 
    226 	tagCmd:         []tagCmd{{"tags", `^(\S+)`}},
    227 	tagSyncCmd:     []string{"update -r {tag}"},
    228 	tagSyncDefault: []string{"update -r revno:-1"},
    229 
    230 	scheme:      []string{"https", "http", "bzr", "bzr+ssh"},
    231 	pingCmd:     "info {scheme}://{repo}",
    232 	remoteRepo:  bzrRemoteRepo,
    233 	resolveRepo: bzrResolveRepo,
    234 }
    235 
    236 func bzrRemoteRepo(vcsBzr *vcsCmd, rootDir string) (remoteRepo string, err error) {
    237 	outb, err := vcsBzr.runOutput(rootDir, "config parent_location")
    238 	if err != nil {
    239 		return "", err
    240 	}
    241 	return strings.TrimSpace(string(outb)), nil
    242 }
    243 
    244 func bzrResolveRepo(vcsBzr *vcsCmd, rootDir, remoteRepo string) (realRepo string, err error) {
    245 	outb, err := vcsBzr.runOutput(rootDir, "info "+remoteRepo)
    246 	if err != nil {
    247 		return "", err
    248 	}
    249 	out := string(outb)
    250 
    251 	// Expect:
    252 	// ...
    253 	//   (branch root|repository branch): <URL>
    254 	// ...
    255 
    256 	found := false
    257 	for _, prefix := range []string{"\n  branch root: ", "\n  repository branch: "} {
    258 		i := strings.Index(out, prefix)
    259 		if i >= 0 {
    260 			out = out[i+len(prefix):]
    261 			found = true
    262 			break
    263 		}
    264 	}
    265 	if !found {
    266 		return "", fmt.Errorf("unable to parse output of bzr info")
    267 	}
    268 
    269 	i := strings.Index(out, "\n")
    270 	if i < 0 {
    271 		return "", fmt.Errorf("unable to parse output of bzr info")
    272 	}
    273 	out = out[:i]
    274 	return strings.TrimSpace(out), nil
    275 }
    276 
    277 // vcsSvn describes how to use Subversion.
    278 var vcsSvn = &vcsCmd{
    279 	name: "Subversion",
    280 	cmd:  "svn",
    281 
    282 	createCmd:   []string{"checkout {repo} {dir}"},
    283 	downloadCmd: []string{"update"},
    284 
    285 	// There is no tag command in subversion.
    286 	// The branch information is all in the path names.
    287 
    288 	scheme:     []string{"https", "http", "svn", "svn+ssh"},
    289 	pingCmd:    "info {scheme}://{repo}",
    290 	remoteRepo: svnRemoteRepo,
    291 }
    292 
    293 func svnRemoteRepo(vcsSvn *vcsCmd, rootDir string) (remoteRepo string, err error) {
    294 	outb, err := vcsSvn.runOutput(rootDir, "info")
    295 	if err != nil {
    296 		return "", err
    297 	}
    298 	out := string(outb)
    299 
    300 	// Expect:
    301 	// ...
    302 	// Repository Root: <URL>
    303 	// ...
    304 
    305 	i := strings.Index(out, "\nRepository Root: ")
    306 	if i < 0 {
    307 		return "", fmt.Errorf("unable to parse output of svn info")
    308 	}
    309 	out = out[i+len("\nRepository Root: "):]
    310 	i = strings.Index(out, "\n")
    311 	if i < 0 {
    312 		return "", fmt.Errorf("unable to parse output of svn info")
    313 	}
    314 	out = out[:i]
    315 	return strings.TrimSpace(out), nil
    316 }
    317 
    318 func (v *vcsCmd) String() string {
    319 	return v.name
    320 }
    321 
    322 // run runs the command line cmd in the given directory.
    323 // keyval is a list of key, value pairs.  run expands
    324 // instances of {key} in cmd into value, but only after
    325 // splitting cmd into individual arguments.
    326 // If an error occurs, run prints the command line and the
    327 // command's combined stdout+stderr to standard error.
    328 // Otherwise run discards the command's output.
    329 func (v *vcsCmd) run(dir string, cmd string, keyval ...string) error {
    330 	_, err := v.run1(dir, cmd, keyval, true)
    331 	return err
    332 }
    333 
    334 // runVerboseOnly is like run but only generates error output to standard error in verbose mode.
    335 func (v *vcsCmd) runVerboseOnly(dir string, cmd string, keyval ...string) error {
    336 	_, err := v.run1(dir, cmd, keyval, false)
    337 	return err
    338 }
    339 
    340 // runOutput is like run but returns the output of the command.
    341 func (v *vcsCmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) {
    342 	return v.run1(dir, cmd, keyval, true)
    343 }
    344 
    345 // run1 is the generalized implementation of run and runOutput.
    346 func (v *vcsCmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) {
    347 	m := make(map[string]string)
    348 	for i := 0; i < len(keyval); i += 2 {
    349 		m[keyval[i]] = keyval[i+1]
    350 	}
    351 	args := strings.Fields(cmdline)
    352 	for i, arg := range args {
    353 		args[i] = expand(m, arg)
    354 	}
    355 
    356 	if len(args) >= 2 && args[0] == "-go-internal-cd" {
    357 		if filepath.IsAbs(args[1]) {
    358 			dir = args[1]
    359 		} else {
    360 			dir = filepath.Join(dir, args[1])
    361 		}
    362 		args = args[2:]
    363 	}
    364 
    365 	_, err := exec.LookPath(v.cmd)
    366 	if err != nil {
    367 		fmt.Fprintf(os.Stderr,
    368 			"go: missing %s command. See https://golang.org/s/gogetcmd\n",
    369 			v.name)
    370 		return nil, err
    371 	}
    372 
    373 	cmd := exec.Command(v.cmd, args...)
    374 	cmd.Dir = dir
    375 	cmd.Env = envForDir(cmd.Dir, os.Environ())
    376 	if buildX {
    377 		fmt.Printf("cd %s\n", dir)
    378 		fmt.Printf("%s %s\n", v.cmd, strings.Join(args, " "))
    379 	}
    380 	var buf bytes.Buffer
    381 	cmd.Stdout = &buf
    382 	cmd.Stderr = &buf
    383 	err = cmd.Run()
    384 	out := buf.Bytes()
    385 	if err != nil {
    386 		if verbose || buildV {
    387 			fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.cmd, strings.Join(args, " "))
    388 			os.Stderr.Write(out)
    389 		}
    390 		return out, err
    391 	}
    392 	return out, nil
    393 }
    394 
    395 // ping pings to determine scheme to use.
    396 func (v *vcsCmd) ping(scheme, repo string) error {
    397 	return v.runVerboseOnly(".", v.pingCmd, "scheme", scheme, "repo", repo)
    398 }
    399 
    400 // create creates a new copy of repo in dir.
    401 // The parent of dir must exist; dir must not.
    402 func (v *vcsCmd) create(dir, repo string) error {
    403 	for _, cmd := range v.createCmd {
    404 		if err := v.run(".", cmd, "dir", dir, "repo", repo); err != nil {
    405 			return err
    406 		}
    407 	}
    408 	return nil
    409 }
    410 
    411 // download downloads any new changes for the repo in dir.
    412 func (v *vcsCmd) download(dir string) error {
    413 	for _, cmd := range v.downloadCmd {
    414 		if err := v.run(dir, cmd); err != nil {
    415 			return err
    416 		}
    417 	}
    418 	return nil
    419 }
    420 
    421 // tags returns the list of available tags for the repo in dir.
    422 func (v *vcsCmd) tags(dir string) ([]string, error) {
    423 	var tags []string
    424 	for _, tc := range v.tagCmd {
    425 		out, err := v.runOutput(dir, tc.cmd)
    426 		if err != nil {
    427 			return nil, err
    428 		}
    429 		re := regexp.MustCompile(`(?m-s)` + tc.pattern)
    430 		for _, m := range re.FindAllStringSubmatch(string(out), -1) {
    431 			tags = append(tags, m[1])
    432 		}
    433 	}
    434 	return tags, nil
    435 }
    436 
    437 // tagSync syncs the repo in dir to the named tag,
    438 // which either is a tag returned by tags or is v.tagDefault.
    439 func (v *vcsCmd) tagSync(dir, tag string) error {
    440 	if v.tagSyncCmd == nil {
    441 		return nil
    442 	}
    443 	if tag != "" {
    444 		for _, tc := range v.tagLookupCmd {
    445 			out, err := v.runOutput(dir, tc.cmd, "tag", tag)
    446 			if err != nil {
    447 				return err
    448 			}
    449 			re := regexp.MustCompile(`(?m-s)` + tc.pattern)
    450 			m := re.FindStringSubmatch(string(out))
    451 			if len(m) > 1 {
    452 				tag = m[1]
    453 				break
    454 			}
    455 		}
    456 	}
    457 
    458 	if tag == "" && v.tagSyncDefault != nil {
    459 		for _, cmd := range v.tagSyncDefault {
    460 			if err := v.run(dir, cmd); err != nil {
    461 				return err
    462 			}
    463 		}
    464 		return nil
    465 	}
    466 
    467 	for _, cmd := range v.tagSyncCmd {
    468 		if err := v.run(dir, cmd, "tag", tag); err != nil {
    469 			return err
    470 		}
    471 	}
    472 	return nil
    473 }
    474 
    475 // A vcsPath describes how to convert an import path into a
    476 // version control system and repository name.
    477 type vcsPath struct {
    478 	prefix string                              // prefix this description applies to
    479 	re     string                              // pattern for import path
    480 	repo   string                              // repository to use (expand with match of re)
    481 	vcs    string                              // version control system to use (expand with match of re)
    482 	check  func(match map[string]string) error // additional checks
    483 	ping   bool                                // ping for scheme to use to download repo
    484 
    485 	regexp *regexp.Regexp // cached compiled form of re
    486 }
    487 
    488 // vcsFromDir inspects dir and its parents to determine the
    489 // version control system and code repository to use.
    490 // On return, root is the import path
    491 // corresponding to the root of the repository.
    492 func vcsFromDir(dir, srcRoot string) (vcs *vcsCmd, root string, err error) {
    493 	// Clean and double-check that dir is in (a subdirectory of) srcRoot.
    494 	dir = filepath.Clean(dir)
    495 	srcRoot = filepath.Clean(srcRoot)
    496 	if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {
    497 		return nil, "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot)
    498 	}
    499 
    500 	origDir := dir
    501 	for len(dir) > len(srcRoot) {
    502 		for _, vcs := range vcsList {
    503 			if _, err := os.Stat(filepath.Join(dir, "."+vcs.cmd)); err == nil {
    504 				return vcs, filepath.ToSlash(dir[len(srcRoot)+1:]), nil
    505 			}
    506 		}
    507 
    508 		// Move to parent.
    509 		ndir := filepath.Dir(dir)
    510 		if len(ndir) >= len(dir) {
    511 			// Shouldn't happen, but just in case, stop.
    512 			break
    513 		}
    514 		dir = ndir
    515 	}
    516 
    517 	return nil, "", fmt.Errorf("directory %q is not using a known version control system", origDir)
    518 }
    519 
    520 // repoRoot represents a version control system, a repo, and a root of
    521 // where to put it on disk.
    522 type repoRoot struct {
    523 	vcs *vcsCmd
    524 
    525 	// repo is the repository URL, including scheme
    526 	repo string
    527 
    528 	// root is the import path corresponding to the root of the
    529 	// repository
    530 	root string
    531 
    532 	// isCustom is true for custom import paths (those defined by HTML meta tags)
    533 	isCustom bool
    534 }
    535 
    536 var httpPrefixRE = regexp.MustCompile(`^https?:`)
    537 
    538 // securityMode specifies whether a function should make network
    539 // calls using insecure transports (eg, plain text HTTP).
    540 // The zero value is "secure".
    541 type securityMode int
    542 
    543 const (
    544 	secure securityMode = iota
    545 	insecure
    546 )
    547 
    548 // repoRootForImportPath analyzes importPath to determine the
    549 // version control system, and code repository to use.
    550 func repoRootForImportPath(importPath string, security securityMode) (*repoRoot, error) {
    551 	rr, err := repoRootFromVCSPaths(importPath, "", security, vcsPaths)
    552 	if err == errUnknownSite {
    553 		// If there are wildcards, look up the thing before the wildcard,
    554 		// hoping it applies to the wildcarded parts too.
    555 		// This makes 'go get rsc.io/pdf/...' work in a fresh GOPATH.
    556 		lookup := strings.TrimSuffix(importPath, "/...")
    557 		if i := strings.Index(lookup, "/.../"); i >= 0 {
    558 			lookup = lookup[:i]
    559 		}
    560 		rr, err = repoRootForImportDynamic(lookup, security)
    561 		if err != nil {
    562 			err = fmt.Errorf("unrecognized import path %q (%v)", importPath, err)
    563 		}
    564 	}
    565 	if err != nil {
    566 		rr1, err1 := repoRootFromVCSPaths(importPath, "", security, vcsPathsAfterDynamic)
    567 		if err1 == nil {
    568 			rr = rr1
    569 			err = nil
    570 		}
    571 	}
    572 
    573 	if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.root, "...") {
    574 		// Do not allow wildcards in the repo root.
    575 		rr = nil
    576 		err = fmt.Errorf("cannot expand ... in %q", importPath)
    577 	}
    578 	return rr, err
    579 }
    580 
    581 var errUnknownSite = errors.New("dynamic lookup required to find mapping")
    582 
    583 // repoRootFromVCSPaths attempts to map importPath to a repoRoot
    584 // using the mappings defined in vcsPaths.
    585 // If scheme is non-empty, that scheme is forced.
    586 func repoRootFromVCSPaths(importPath, scheme string, security securityMode, vcsPaths []*vcsPath) (*repoRoot, error) {
    587 	// A common error is to use https://packagepath because that's what
    588 	// hg and git require. Diagnose this helpfully.
    589 	if loc := httpPrefixRE.FindStringIndex(importPath); loc != nil {
    590 		// The importPath has been cleaned, so has only one slash. The pattern
    591 		// ignores the slashes; the error message puts them back on the RHS at least.
    592 		return nil, fmt.Errorf("%q not allowed in import path", importPath[loc[0]:loc[1]]+"//")
    593 	}
    594 	for _, srv := range vcsPaths {
    595 		if !strings.HasPrefix(importPath, srv.prefix) {
    596 			continue
    597 		}
    598 		m := srv.regexp.FindStringSubmatch(importPath)
    599 		if m == nil {
    600 			if srv.prefix != "" {
    601 				return nil, fmt.Errorf("invalid %s import path %q", srv.prefix, importPath)
    602 			}
    603 			continue
    604 		}
    605 
    606 		// Build map of named subexpression matches for expand.
    607 		match := map[string]string{
    608 			"prefix": srv.prefix,
    609 			"import": importPath,
    610 		}
    611 		for i, name := range srv.regexp.SubexpNames() {
    612 			if name != "" && match[name] == "" {
    613 				match[name] = m[i]
    614 			}
    615 		}
    616 		if srv.vcs != "" {
    617 			match["vcs"] = expand(match, srv.vcs)
    618 		}
    619 		if srv.repo != "" {
    620 			match["repo"] = expand(match, srv.repo)
    621 		}
    622 		if srv.check != nil {
    623 			if err := srv.check(match); err != nil {
    624 				return nil, err
    625 			}
    626 		}
    627 		vcs := vcsByCmd(match["vcs"])
    628 		if vcs == nil {
    629 			return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
    630 		}
    631 		if srv.ping {
    632 			if scheme != "" {
    633 				match["repo"] = scheme + "://" + match["repo"]
    634 			} else {
    635 				for _, scheme := range vcs.scheme {
    636 					if security == secure && !vcs.isSecureScheme(scheme) {
    637 						continue
    638 					}
    639 					if vcs.ping(scheme, match["repo"]) == nil {
    640 						match["repo"] = scheme + "://" + match["repo"]
    641 						break
    642 					}
    643 				}
    644 			}
    645 		}
    646 		rr := &repoRoot{
    647 			vcs:  vcs,
    648 			repo: match["repo"],
    649 			root: match["root"],
    650 		}
    651 		return rr, nil
    652 	}
    653 	return nil, errUnknownSite
    654 }
    655 
    656 // repoRootForImportDynamic finds a *repoRoot for a custom domain that's not
    657 // statically known by repoRootForImportPathStatic.
    658 //
    659 // This handles custom import paths like "name.tld/pkg/foo" or just "name.tld".
    660 func repoRootForImportDynamic(importPath string, security securityMode) (*repoRoot, error) {
    661 	slash := strings.Index(importPath, "/")
    662 	if slash < 0 {
    663 		slash = len(importPath)
    664 	}
    665 	host := importPath[:slash]
    666 	if !strings.Contains(host, ".") {
    667 		return nil, errors.New("import path does not begin with hostname")
    668 	}
    669 	urlStr, body, err := httpsOrHTTP(importPath, security)
    670 	if err != nil {
    671 		msg := "https fetch: %v"
    672 		if security == insecure {
    673 			msg = "http/" + msg
    674 		}
    675 		return nil, fmt.Errorf(msg, err)
    676 	}
    677 	defer body.Close()
    678 	imports, err := parseMetaGoImports(body)
    679 	if err != nil {
    680 		return nil, fmt.Errorf("parsing %s: %v", importPath, err)
    681 	}
    682 	// Find the matched meta import.
    683 	mmi, err := matchGoImport(imports, importPath)
    684 	if err != nil {
    685 		if _, ok := err.(ImportMismatchError); !ok {
    686 			return nil, fmt.Errorf("parse %s: %v", urlStr, err)
    687 		}
    688 		return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", urlStr, err)
    689 	}
    690 	if buildV {
    691 		log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, urlStr)
    692 	}
    693 	// If the import was "uni.edu/bob/project", which said the
    694 	// prefix was "uni.edu" and the RepoRoot was "evilroot.com",
    695 	// make sure we don't trust Bob and check out evilroot.com to
    696 	// "uni.edu" yet (possibly overwriting/preempting another
    697 	// non-evil student).  Instead, first verify the root and see
    698 	// if it matches Bob's claim.
    699 	if mmi.Prefix != importPath {
    700 		if buildV {
    701 			log.Printf("get %q: verifying non-authoritative meta tag", importPath)
    702 		}
    703 		urlStr0 := urlStr
    704 		var imports []metaImport
    705 		urlStr, imports, err = metaImportsForPrefix(mmi.Prefix, security)
    706 		if err != nil {
    707 			return nil, err
    708 		}
    709 		metaImport2, err := matchGoImport(imports, importPath)
    710 		if err != nil || mmi != metaImport2 {
    711 			return nil, fmt.Errorf("%s and %s disagree about go-import for %s", urlStr0, urlStr, mmi.Prefix)
    712 		}
    713 	}
    714 
    715 	if !strings.Contains(mmi.RepoRoot, "://") {
    716 		return nil, fmt.Errorf("%s: invalid repo root %q; no scheme", urlStr, mmi.RepoRoot)
    717 	}
    718 	rr := &repoRoot{
    719 		vcs:      vcsByCmd(mmi.VCS),
    720 		repo:     mmi.RepoRoot,
    721 		root:     mmi.Prefix,
    722 		isCustom: true,
    723 	}
    724 	if rr.vcs == nil {
    725 		return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, mmi.VCS)
    726 	}
    727 	return rr, nil
    728 }
    729 
    730 var fetchGroup singleflight.Group
    731 var (
    732 	fetchCacheMu sync.Mutex
    733 	fetchCache   = map[string]fetchResult{} // key is metaImportsForPrefix's importPrefix
    734 )
    735 
    736 // metaImportsForPrefix takes a package's root import path as declared in a <meta> tag
    737 // and returns its HTML discovery URL and the parsed metaImport lines
    738 // found on the page.
    739 //
    740 // The importPath is of the form "golang.org/x/tools".
    741 // It is an error if no imports are found.
    742 // urlStr will still be valid if err != nil.
    743 // The returned urlStr will be of the form "https://golang.org/x/tools?go-get=1"
    744 func metaImportsForPrefix(importPrefix string, security securityMode) (urlStr string, imports []metaImport, err error) {
    745 	setCache := func(res fetchResult) (fetchResult, error) {
    746 		fetchCacheMu.Lock()
    747 		defer fetchCacheMu.Unlock()
    748 		fetchCache[importPrefix] = res
    749 		return res, nil
    750 	}
    751 
    752 	resi, _, _ := fetchGroup.Do(importPrefix, func() (resi interface{}, err error) {
    753 		fetchCacheMu.Lock()
    754 		if res, ok := fetchCache[importPrefix]; ok {
    755 			fetchCacheMu.Unlock()
    756 			return res, nil
    757 		}
    758 		fetchCacheMu.Unlock()
    759 
    760 		urlStr, body, err := httpsOrHTTP(importPrefix, security)
    761 		if err != nil {
    762 			return setCache(fetchResult{urlStr: urlStr, err: fmt.Errorf("fetch %s: %v", urlStr, err)})
    763 		}
    764 		imports, err := parseMetaGoImports(body)
    765 		if err != nil {
    766 			return setCache(fetchResult{urlStr: urlStr, err: fmt.Errorf("parsing %s: %v", urlStr, err)})
    767 		}
    768 		if len(imports) == 0 {
    769 			err = fmt.Errorf("fetch %s: no go-import meta tag", urlStr)
    770 		}
    771 		return setCache(fetchResult{urlStr: urlStr, imports: imports, err: err})
    772 	})
    773 	res := resi.(fetchResult)
    774 	return res.urlStr, res.imports, res.err
    775 }
    776 
    777 type fetchResult struct {
    778 	urlStr  string // e.g. "https://foo.com/x/bar?go-get=1"
    779 	imports []metaImport
    780 	err     error
    781 }
    782 
    783 // metaImport represents the parsed <meta name="go-import"
    784 // content="prefix vcs reporoot" /> tags from HTML files.
    785 type metaImport struct {
    786 	Prefix, VCS, RepoRoot string
    787 }
    788 
    789 func splitPathHasPrefix(path, prefix []string) bool {
    790 	if len(path) < len(prefix) {
    791 		return false
    792 	}
    793 	for i, p := range prefix {
    794 		if path[i] != p {
    795 			return false
    796 		}
    797 	}
    798 	return true
    799 }
    800 
    801 // A ImportMismatchError is returned where metaImport/s are present
    802 // but none match our import path.
    803 type ImportMismatchError struct {
    804 	importPath string
    805 	mismatches []string // the meta imports that were discarded for not matching our importPath
    806 }
    807 
    808 func (m ImportMismatchError) Error() string {
    809 	formattedStrings := make([]string, len(m.mismatches))
    810 	for i, pre := range m.mismatches {
    811 		formattedStrings[i] = fmt.Sprintf("meta tag %s did not match import path %s", pre, m.importPath)
    812 	}
    813 	return strings.Join(formattedStrings, ", ")
    814 }
    815 
    816 // matchGoImport returns the metaImport from imports matching importPath.
    817 // An error is returned if there are multiple matches.
    818 // errNoMatch is returned if none match.
    819 func matchGoImport(imports []metaImport, importPath string) (metaImport, error) {
    820 	match := -1
    821 	imp := strings.Split(importPath, "/")
    822 
    823 	errImportMismatch := ImportMismatchError{importPath: importPath}
    824 	for i, im := range imports {
    825 		pre := strings.Split(im.Prefix, "/")
    826 
    827 		if !splitPathHasPrefix(imp, pre) {
    828 			errImportMismatch.mismatches = append(errImportMismatch.mismatches, im.Prefix)
    829 			continue
    830 		}
    831 
    832 		if match != -1 {
    833 			return metaImport{}, fmt.Errorf("multiple meta tags match import path %q", importPath)
    834 		}
    835 		match = i
    836 	}
    837 
    838 	if match == -1 {
    839 		return metaImport{}, errImportMismatch
    840 	}
    841 	return imports[match], nil
    842 }
    843 
    844 // expand rewrites s to replace {k} with match[k] for each key k in match.
    845 func expand(match map[string]string, s string) string {
    846 	for k, v := range match {
    847 		s = strings.Replace(s, "{"+k+"}", v, -1)
    848 	}
    849 	return s
    850 }
    851 
    852 // vcsPaths defines the meaning of import paths referring to
    853 // commonly-used VCS hosting sites (github.com/user/dir)
    854 // and import paths referring to a fully-qualified importPath
    855 // containing a VCS type (foo.com/repo.git/dir)
    856 var vcsPaths = []*vcsPath{
    857 	// Github
    858 	{
    859 		prefix: "github.com/",
    860 		re:     `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
    861 		vcs:    "git",
    862 		repo:   "https://{root}",
    863 		check:  noVCSSuffix,
    864 	},
    865 
    866 	// Bitbucket
    867 	{
    868 		prefix: "bitbucket.org/",
    869 		re:     `^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
    870 		repo:   "https://{root}",
    871 		check:  bitbucketVCS,
    872 	},
    873 
    874 	// IBM DevOps Services (JazzHub)
    875 	{
    876 		prefix: "hub.jazz.net/git",
    877 		re:     `^(?P<root>hub.jazz.net/git/[a-z0-9]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
    878 		vcs:    "git",
    879 		repo:   "https://{root}",
    880 		check:  noVCSSuffix,
    881 	},
    882 
    883 	// Git at Apache
    884 	{
    885 		prefix: "git.apache.org",
    886 		re:     `^(?P<root>git.apache.org/[a-z0-9_.\-]+\.git)(/[A-Za-z0-9_.\-]+)*$`,
    887 		vcs:    "git",
    888 		repo:   "https://{root}",
    889 	},
    890 
    891 	// Git at OpenStack
    892 	{
    893 		prefix: "git.openstack.org",
    894 		re:     `^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/[A-Za-z0-9_.\-]+)*$`,
    895 		vcs:    "git",
    896 		repo:   "https://{root}",
    897 	},
    898 
    899 	// General syntax for any server.
    900 	// Must be last.
    901 	{
    902 		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_.\-]+)*$`,
    903 		ping: true,
    904 	},
    905 }
    906 
    907 // vcsPathsAfterDynamic gives additional vcsPaths entries
    908 // to try after the dynamic HTML check.
    909 // This gives those sites a chance to introduce <meta> tags
    910 // as part of a graceful transition away from the hard-coded logic.
    911 var vcsPathsAfterDynamic = []*vcsPath{
    912 	// Launchpad. See golang.org/issue/11436.
    913 	{
    914 		prefix: "launchpad.net/",
    915 		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_.\-]+)*$`,
    916 		vcs:    "bzr",
    917 		repo:   "https://{root}",
    918 		check:  launchpadVCS,
    919 	},
    920 }
    921 
    922 func init() {
    923 	// fill in cached regexps.
    924 	// Doing this eagerly discovers invalid regexp syntax
    925 	// without having to run a command that needs that regexp.
    926 	for _, srv := range vcsPaths {
    927 		srv.regexp = regexp.MustCompile(srv.re)
    928 	}
    929 	for _, srv := range vcsPathsAfterDynamic {
    930 		srv.regexp = regexp.MustCompile(srv.re)
    931 	}
    932 }
    933 
    934 // noVCSSuffix checks that the repository name does not
    935 // end in .foo for any version control system foo.
    936 // The usual culprit is ".git".
    937 func noVCSSuffix(match map[string]string) error {
    938 	repo := match["repo"]
    939 	for _, vcs := range vcsList {
    940 		if strings.HasSuffix(repo, "."+vcs.cmd) {
    941 			return fmt.Errorf("invalid version control suffix in %s path", match["prefix"])
    942 		}
    943 	}
    944 	return nil
    945 }
    946 
    947 // bitbucketVCS determines the version control system for a
    948 // Bitbucket repository, by using the Bitbucket API.
    949 func bitbucketVCS(match map[string]string) error {
    950 	if err := noVCSSuffix(match); err != nil {
    951 		return err
    952 	}
    953 
    954 	var resp struct {
    955 		SCM string `json:"scm"`
    956 	}
    957 	url := expand(match, "https://api.bitbucket.org/1.0/repositories/{bitname}")
    958 	data, err := httpGET(url)
    959 	if err != nil {
    960 		if httpErr, ok := err.(*httpError); ok && httpErr.statusCode == 403 {
    961 			// this may be a private repository. If so, attempt to determine which
    962 			// VCS it uses. See issue 5375.
    963 			root := match["root"]
    964 			for _, vcs := range []string{"git", "hg"} {
    965 				if vcsByCmd(vcs).ping("https", root) == nil {
    966 					resp.SCM = vcs
    967 					break
    968 				}
    969 			}
    970 		}
    971 
    972 		if resp.SCM == "" {
    973 			return err
    974 		}
    975 	} else {
    976 		if err := json.Unmarshal(data, &resp); err != nil {
    977 			return fmt.Errorf("decoding %s: %v", url, err)
    978 		}
    979 	}
    980 
    981 	if vcsByCmd(resp.SCM) != nil {
    982 		match["vcs"] = resp.SCM
    983 		if resp.SCM == "git" {
    984 			match["repo"] += ".git"
    985 		}
    986 		return nil
    987 	}
    988 
    989 	return fmt.Errorf("unable to detect version control system for bitbucket.org/ path")
    990 }
    991 
    992 // launchpadVCS solves the ambiguity for "lp.net/project/foo". In this case,
    993 // "foo" could be a series name registered in Launchpad with its own branch,
    994 // and it could also be the name of a directory within the main project
    995 // branch one level up.
    996 func launchpadVCS(match map[string]string) error {
    997 	if match["project"] == "" || match["series"] == "" {
    998 		return nil
    999 	}
   1000 	_, err := httpGET(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format"))
   1001 	if err != nil {
   1002 		match["root"] = expand(match, "launchpad.net/{project}")
   1003 		match["repo"] = expand(match, "https://{root}")
   1004 	}
   1005 	return nil
   1006 }
   1007