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