github.com/FenixAra/go@v0.0.0-20170127160404-96ea0918e670/src/cmd/go/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 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@github.com:user/repo" becomes 190 // "ssh://git@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 }