github.com/freddyisaac/sicortex-golang@v0.0.0-20231019035217-e03519e66f60/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 var vcsRet *vcsCmd 501 var rootRet string 502 503 origDir := dir 504 for len(dir) > len(srcRoot) { 505 for _, vcs := range vcsList { 506 if _, err := os.Stat(filepath.Join(dir, "."+vcs.cmd)); err == nil { 507 root := filepath.ToSlash(dir[len(srcRoot)+1:]) 508 // Record first VCS we find, but keep looking, 509 // to detect mistakes like one kind of VCS inside another. 510 if vcsRet == nil { 511 vcsRet = vcs 512 rootRet = root 513 continue 514 } 515 // Allow .git inside .git, which can arise due to submodules. 516 if vcsRet == vcs && vcs.cmd == "git" { 517 continue 518 } 519 // Otherwise, we have one VCS inside a different VCS. 520 return nil, "", fmt.Errorf("directory %q uses %s, but parent %q uses %s", 521 filepath.Join(srcRoot, rootRet), vcsRet.cmd, filepath.Join(srcRoot, root), vcs.cmd) 522 } 523 } 524 525 // Move to parent. 526 ndir := filepath.Dir(dir) 527 if len(ndir) >= len(dir) { 528 // Shouldn't happen, but just in case, stop. 529 break 530 } 531 dir = ndir 532 } 533 534 if vcsRet != nil { 535 return vcsRet, rootRet, nil 536 } 537 538 return nil, "", fmt.Errorf("directory %q is not using a known version control system", origDir) 539 } 540 541 // checkNestedVCS checks for an incorrectly-nested VCS-inside-VCS 542 // situation for dir, checking parents up until srcRoot. 543 func checkNestedVCS(vcs *vcsCmd, dir, srcRoot string) error { 544 if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator { 545 return fmt.Errorf("directory %q is outside source root %q", dir, srcRoot) 546 } 547 548 otherDir := dir 549 for len(otherDir) > len(srcRoot) { 550 for _, otherVCS := range vcsList { 551 if _, err := os.Stat(filepath.Join(otherDir, "."+otherVCS.cmd)); err == nil { 552 // Allow expected vcs in original dir. 553 if otherDir == dir && otherVCS == vcs { 554 continue 555 } 556 // Allow .git inside .git, which can arise due to submodules. 557 if otherVCS == vcs && vcs.cmd == "git" { 558 continue 559 } 560 // Otherwise, we have one VCS inside a different VCS. 561 return fmt.Errorf("directory %q uses %s, but parent %q uses %s", dir, vcs.cmd, otherDir, otherVCS.cmd) 562 } 563 } 564 // Move to parent. 565 newDir := filepath.Dir(otherDir) 566 if len(newDir) >= len(otherDir) { 567 // Shouldn't happen, but just in case, stop. 568 break 569 } 570 otherDir = newDir 571 } 572 573 return nil 574 } 575 576 // repoRoot represents a version control system, a repo, and a root of 577 // where to put it on disk. 578 type repoRoot struct { 579 vcs *vcsCmd 580 581 // repo is the repository URL, including scheme 582 repo string 583 584 // root is the import path corresponding to the root of the 585 // repository 586 root string 587 588 // isCustom is true for custom import paths (those defined by HTML meta tags) 589 isCustom bool 590 } 591 592 var httpPrefixRE = regexp.MustCompile(`^https?:`) 593 594 // securityMode specifies whether a function should make network 595 // calls using insecure transports (eg, plain text HTTP). 596 // The zero value is "secure". 597 type securityMode int 598 599 const ( 600 secure securityMode = iota 601 insecure 602 ) 603 604 // repoRootForImportPath analyzes importPath to determine the 605 // version control system, and code repository to use. 606 func repoRootForImportPath(importPath string, security securityMode) (*repoRoot, error) { 607 rr, err := repoRootFromVCSPaths(importPath, "", security, vcsPaths) 608 if err == errUnknownSite { 609 // If there are wildcards, look up the thing before the wildcard, 610 // hoping it applies to the wildcarded parts too. 611 // This makes 'go get rsc.io/pdf/...' work in a fresh GOPATH. 612 lookup := strings.TrimSuffix(importPath, "/...") 613 if i := strings.Index(lookup, "/.../"); i >= 0 { 614 lookup = lookup[:i] 615 } 616 rr, err = repoRootForImportDynamic(lookup, security) 617 if err != nil { 618 err = fmt.Errorf("unrecognized import path %q (%v)", importPath, err) 619 } 620 } 621 if err != nil { 622 rr1, err1 := repoRootFromVCSPaths(importPath, "", security, vcsPathsAfterDynamic) 623 if err1 == nil { 624 rr = rr1 625 err = nil 626 } 627 } 628 629 if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.root, "...") { 630 // Do not allow wildcards in the repo root. 631 rr = nil 632 err = fmt.Errorf("cannot expand ... in %q", importPath) 633 } 634 return rr, err 635 } 636 637 var errUnknownSite = errors.New("dynamic lookup required to find mapping") 638 639 // repoRootFromVCSPaths attempts to map importPath to a repoRoot 640 // using the mappings defined in vcsPaths. 641 // If scheme is non-empty, that scheme is forced. 642 func repoRootFromVCSPaths(importPath, scheme string, security securityMode, vcsPaths []*vcsPath) (*repoRoot, error) { 643 // A common error is to use https://packagepath because that's what 644 // hg and git require. Diagnose this helpfully. 645 if loc := httpPrefixRE.FindStringIndex(importPath); loc != nil { 646 // The importPath has been cleaned, so has only one slash. The pattern 647 // ignores the slashes; the error message puts them back on the RHS at least. 648 return nil, fmt.Errorf("%q not allowed in import path", importPath[loc[0]:loc[1]]+"//") 649 } 650 for _, srv := range vcsPaths { 651 if !strings.HasPrefix(importPath, srv.prefix) { 652 continue 653 } 654 m := srv.regexp.FindStringSubmatch(importPath) 655 if m == nil { 656 if srv.prefix != "" { 657 return nil, fmt.Errorf("invalid %s import path %q", srv.prefix, importPath) 658 } 659 continue 660 } 661 662 // Build map of named subexpression matches for expand. 663 match := map[string]string{ 664 "prefix": srv.prefix, 665 "import": importPath, 666 } 667 for i, name := range srv.regexp.SubexpNames() { 668 if name != "" && match[name] == "" { 669 match[name] = m[i] 670 } 671 } 672 if srv.vcs != "" { 673 match["vcs"] = expand(match, srv.vcs) 674 } 675 if srv.repo != "" { 676 match["repo"] = expand(match, srv.repo) 677 } 678 if srv.check != nil { 679 if err := srv.check(match); err != nil { 680 return nil, err 681 } 682 } 683 vcs := vcsByCmd(match["vcs"]) 684 if vcs == nil { 685 return nil, fmt.Errorf("unknown version control system %q", match["vcs"]) 686 } 687 if srv.ping { 688 if scheme != "" { 689 match["repo"] = scheme + "://" + match["repo"] 690 } else { 691 for _, scheme := range vcs.scheme { 692 if security == secure && !vcs.isSecureScheme(scheme) { 693 continue 694 } 695 if vcs.ping(scheme, match["repo"]) == nil { 696 match["repo"] = scheme + "://" + match["repo"] 697 break 698 } 699 } 700 } 701 } 702 rr := &repoRoot{ 703 vcs: vcs, 704 repo: match["repo"], 705 root: match["root"], 706 } 707 return rr, nil 708 } 709 return nil, errUnknownSite 710 } 711 712 // repoRootForImportDynamic finds a *repoRoot for a custom domain that's not 713 // statically known by repoRootForImportPathStatic. 714 // 715 // This handles custom import paths like "name.tld/pkg/foo" or just "name.tld". 716 func repoRootForImportDynamic(importPath string, security securityMode) (*repoRoot, error) { 717 slash := strings.Index(importPath, "/") 718 if slash < 0 { 719 slash = len(importPath) 720 } 721 host := importPath[:slash] 722 if !strings.Contains(host, ".") { 723 return nil, errors.New("import path does not begin with hostname") 724 } 725 urlStr, body, err := httpsOrHTTP(importPath, security) 726 if err != nil { 727 msg := "https fetch: %v" 728 if security == insecure { 729 msg = "http/" + msg 730 } 731 return nil, fmt.Errorf(msg, err) 732 } 733 defer body.Close() 734 imports, err := parseMetaGoImports(body) 735 if err != nil { 736 return nil, fmt.Errorf("parsing %s: %v", importPath, err) 737 } 738 // Find the matched meta import. 739 mmi, err := matchGoImport(imports, importPath) 740 if err != nil { 741 if _, ok := err.(ImportMismatchError); !ok { 742 return nil, fmt.Errorf("parse %s: %v", urlStr, err) 743 } 744 return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", urlStr, err) 745 } 746 if buildV { 747 log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, urlStr) 748 } 749 // If the import was "uni.edu/bob/project", which said the 750 // prefix was "uni.edu" and the RepoRoot was "evilroot.com", 751 // make sure we don't trust Bob and check out evilroot.com to 752 // "uni.edu" yet (possibly overwriting/preempting another 753 // non-evil student). Instead, first verify the root and see 754 // if it matches Bob's claim. 755 if mmi.Prefix != importPath { 756 if buildV { 757 log.Printf("get %q: verifying non-authoritative meta tag", importPath) 758 } 759 urlStr0 := urlStr 760 var imports []metaImport 761 urlStr, imports, err = metaImportsForPrefix(mmi.Prefix, security) 762 if err != nil { 763 return nil, err 764 } 765 metaImport2, err := matchGoImport(imports, importPath) 766 if err != nil || mmi != metaImport2 { 767 return nil, fmt.Errorf("%s and %s disagree about go-import for %s", urlStr0, urlStr, mmi.Prefix) 768 } 769 } 770 771 if !strings.Contains(mmi.RepoRoot, "://") { 772 return nil, fmt.Errorf("%s: invalid repo root %q; no scheme", urlStr, mmi.RepoRoot) 773 } 774 rr := &repoRoot{ 775 vcs: vcsByCmd(mmi.VCS), 776 repo: mmi.RepoRoot, 777 root: mmi.Prefix, 778 isCustom: true, 779 } 780 if rr.vcs == nil { 781 return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, mmi.VCS) 782 } 783 return rr, nil 784 } 785 786 var fetchGroup singleflight.Group 787 var ( 788 fetchCacheMu sync.Mutex 789 fetchCache = map[string]fetchResult{} // key is metaImportsForPrefix's importPrefix 790 ) 791 792 // metaImportsForPrefix takes a package's root import path as declared in a <meta> tag 793 // and returns its HTML discovery URL and the parsed metaImport lines 794 // found on the page. 795 // 796 // The importPath is of the form "golang.org/x/tools". 797 // It is an error if no imports are found. 798 // urlStr will still be valid if err != nil. 799 // The returned urlStr will be of the form "https://golang.org/x/tools?go-get=1" 800 func metaImportsForPrefix(importPrefix string, security securityMode) (urlStr string, imports []metaImport, err error) { 801 setCache := func(res fetchResult) (fetchResult, error) { 802 fetchCacheMu.Lock() 803 defer fetchCacheMu.Unlock() 804 fetchCache[importPrefix] = res 805 return res, nil 806 } 807 808 resi, _, _ := fetchGroup.Do(importPrefix, func() (resi interface{}, err error) { 809 fetchCacheMu.Lock() 810 if res, ok := fetchCache[importPrefix]; ok { 811 fetchCacheMu.Unlock() 812 return res, nil 813 } 814 fetchCacheMu.Unlock() 815 816 urlStr, body, err := httpsOrHTTP(importPrefix, security) 817 if err != nil { 818 return setCache(fetchResult{urlStr: urlStr, err: fmt.Errorf("fetch %s: %v", urlStr, err)}) 819 } 820 imports, err := parseMetaGoImports(body) 821 if err != nil { 822 return setCache(fetchResult{urlStr: urlStr, err: fmt.Errorf("parsing %s: %v", urlStr, err)}) 823 } 824 if len(imports) == 0 { 825 err = fmt.Errorf("fetch %s: no go-import meta tag", urlStr) 826 } 827 return setCache(fetchResult{urlStr: urlStr, imports: imports, err: err}) 828 }) 829 res := resi.(fetchResult) 830 return res.urlStr, res.imports, res.err 831 } 832 833 type fetchResult struct { 834 urlStr string // e.g. "https://foo.com/x/bar?go-get=1" 835 imports []metaImport 836 err error 837 } 838 839 // metaImport represents the parsed <meta name="go-import" 840 // content="prefix vcs reporoot" /> tags from HTML files. 841 type metaImport struct { 842 Prefix, VCS, RepoRoot string 843 } 844 845 func splitPathHasPrefix(path, prefix []string) bool { 846 if len(path) < len(prefix) { 847 return false 848 } 849 for i, p := range prefix { 850 if path[i] != p { 851 return false 852 } 853 } 854 return true 855 } 856 857 // A ImportMismatchError is returned where metaImport/s are present 858 // but none match our import path. 859 type ImportMismatchError struct { 860 importPath string 861 mismatches []string // the meta imports that were discarded for not matching our importPath 862 } 863 864 func (m ImportMismatchError) Error() string { 865 formattedStrings := make([]string, len(m.mismatches)) 866 for i, pre := range m.mismatches { 867 formattedStrings[i] = fmt.Sprintf("meta tag %s did not match import path %s", pre, m.importPath) 868 } 869 return strings.Join(formattedStrings, ", ") 870 } 871 872 // matchGoImport returns the metaImport from imports matching importPath. 873 // An error is returned if there are multiple matches. 874 // errNoMatch is returned if none match. 875 func matchGoImport(imports []metaImport, importPath string) (metaImport, error) { 876 match := -1 877 imp := strings.Split(importPath, "/") 878 879 errImportMismatch := ImportMismatchError{importPath: importPath} 880 for i, im := range imports { 881 pre := strings.Split(im.Prefix, "/") 882 883 if !splitPathHasPrefix(imp, pre) { 884 errImportMismatch.mismatches = append(errImportMismatch.mismatches, im.Prefix) 885 continue 886 } 887 888 if match != -1 { 889 return metaImport{}, fmt.Errorf("multiple meta tags match import path %q", importPath) 890 } 891 match = i 892 } 893 894 if match == -1 { 895 return metaImport{}, errImportMismatch 896 } 897 return imports[match], nil 898 } 899 900 // expand rewrites s to replace {k} with match[k] for each key k in match. 901 func expand(match map[string]string, s string) string { 902 for k, v := range match { 903 s = strings.Replace(s, "{"+k+"}", v, -1) 904 } 905 return s 906 } 907 908 // vcsPaths defines the meaning of import paths referring to 909 // commonly-used VCS hosting sites (github.com/user/dir) 910 // and import paths referring to a fully-qualified importPath 911 // containing a VCS type (foo.com/repo.git/dir) 912 var vcsPaths = []*vcsPath{ 913 // Github 914 { 915 prefix: "github.com/", 916 re: `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`, 917 vcs: "git", 918 repo: "https://{root}", 919 check: noVCSSuffix, 920 }, 921 922 // Bitbucket 923 { 924 prefix: "bitbucket.org/", 925 re: `^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`, 926 repo: "https://{root}", 927 check: bitbucketVCS, 928 }, 929 930 // IBM DevOps Services (JazzHub) 931 { 932 prefix: "hub.jazz.net/git", 933 re: `^(?P<root>hub.jazz.net/git/[a-z0-9]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`, 934 vcs: "git", 935 repo: "https://{root}", 936 check: noVCSSuffix, 937 }, 938 939 // Git at Apache 940 { 941 prefix: "git.apache.org", 942 re: `^(?P<root>git.apache.org/[a-z0-9_.\-]+\.git)(/[A-Za-z0-9_.\-]+)*$`, 943 vcs: "git", 944 repo: "https://{root}", 945 }, 946 947 // Git at OpenStack 948 { 949 prefix: "git.openstack.org", 950 re: `^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/[A-Za-z0-9_.\-]+)*$`, 951 vcs: "git", 952 repo: "https://{root}", 953 }, 954 955 // General syntax for any server. 956 // Must be last. 957 { 958 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_.\-]+)*$`, 959 ping: true, 960 }, 961 } 962 963 // vcsPathsAfterDynamic gives additional vcsPaths entries 964 // to try after the dynamic HTML check. 965 // This gives those sites a chance to introduce <meta> tags 966 // as part of a graceful transition away from the hard-coded logic. 967 var vcsPathsAfterDynamic = []*vcsPath{ 968 // Launchpad. See golang.org/issue/11436. 969 { 970 prefix: "launchpad.net/", 971 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_.\-]+)*$`, 972 vcs: "bzr", 973 repo: "https://{root}", 974 check: launchpadVCS, 975 }, 976 } 977 978 func init() { 979 // fill in cached regexps. 980 // Doing this eagerly discovers invalid regexp syntax 981 // without having to run a command that needs that regexp. 982 for _, srv := range vcsPaths { 983 srv.regexp = regexp.MustCompile(srv.re) 984 } 985 for _, srv := range vcsPathsAfterDynamic { 986 srv.regexp = regexp.MustCompile(srv.re) 987 } 988 } 989 990 // noVCSSuffix checks that the repository name does not 991 // end in .foo for any version control system foo. 992 // The usual culprit is ".git". 993 func noVCSSuffix(match map[string]string) error { 994 repo := match["repo"] 995 for _, vcs := range vcsList { 996 if strings.HasSuffix(repo, "."+vcs.cmd) { 997 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"]) 998 } 999 } 1000 return nil 1001 } 1002 1003 // bitbucketVCS determines the version control system for a 1004 // Bitbucket repository, by using the Bitbucket API. 1005 func bitbucketVCS(match map[string]string) error { 1006 if err := noVCSSuffix(match); err != nil { 1007 return err 1008 } 1009 1010 var resp struct { 1011 SCM string `json:"scm"` 1012 } 1013 url := expand(match, "https://api.bitbucket.org/1.0/repositories/{bitname}") 1014 data, err := httpGET(url) 1015 if err != nil { 1016 if httpErr, ok := err.(*httpError); ok && httpErr.statusCode == 403 { 1017 // this may be a private repository. If so, attempt to determine which 1018 // VCS it uses. See issue 5375. 1019 root := match["root"] 1020 for _, vcs := range []string{"git", "hg"} { 1021 if vcsByCmd(vcs).ping("https", root) == nil { 1022 resp.SCM = vcs 1023 break 1024 } 1025 } 1026 } 1027 1028 if resp.SCM == "" { 1029 return err 1030 } 1031 } else { 1032 if err := json.Unmarshal(data, &resp); err != nil { 1033 return fmt.Errorf("decoding %s: %v", url, err) 1034 } 1035 } 1036 1037 if vcsByCmd(resp.SCM) != nil { 1038 match["vcs"] = resp.SCM 1039 if resp.SCM == "git" { 1040 match["repo"] += ".git" 1041 } 1042 return nil 1043 } 1044 1045 return fmt.Errorf("unable to detect version control system for bitbucket.org/ path") 1046 } 1047 1048 // launchpadVCS solves the ambiguity for "lp.net/project/foo". In this case, 1049 // "foo" could be a series name registered in Launchpad with its own branch, 1050 // and it could also be the name of a directory within the main project 1051 // branch one level up. 1052 func launchpadVCS(match map[string]string) error { 1053 if match["project"] == "" || match["series"] == "" { 1054 return nil 1055 } 1056 _, err := httpGET(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format")) 1057 if err != nil { 1058 match["root"] = expand(match, "launchpad.net/{project}") 1059 match["repo"] = expand(match, "https://{root}") 1060 } 1061 return nil 1062 }