github.com/octohelm/cuemod@v0.9.4/internal/cmd/go/internals/vcs/vcs.go (about) 1 // Copyright 2012 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package vcs 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "github.com/octohelm/cuemod/internal/internals/lazyregexp" 12 "github.com/octohelm/cuemod/internal/internals/singleflight" 13 "io/fs" 14 "log" 15 urlpkg "net/url" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "regexp" 20 "strconv" 21 "strings" 22 "sync" 23 "time" 24 25 "github.com/octohelm/cuemod/internal/cmd/go/internals/base" 26 "github.com/octohelm/cuemod/internal/cmd/go/internals/cfg" 27 "github.com/octohelm/cuemod/internal/cmd/go/internals/search" 28 "github.com/octohelm/cuemod/internal/cmd/go/internals/str" 29 "github.com/octohelm/cuemod/internal/cmd/go/internals/web" 30 31 "golang.org/x/mod/module" 32 ) 33 34 // A Cmd describes how to use a version control system 35 // like Mercurial, Git, or Subversion. 36 type Cmd struct { 37 Name string 38 Cmd string // name of binary to invoke command 39 RootNames []rootName // filename and mode indicating the root of a checkout directory 40 41 CreateCmd []string // commands to download a fresh copy of a repository 42 DownloadCmd []string // commands to download updates into an existing repository 43 44 TagCmd []tagCmd // commands to list tags 45 TagLookupCmd []tagCmd // commands to lookup tags before running tagSyncCmd 46 TagSyncCmd []string // commands to sync to specific tag 47 TagSyncDefault []string // commands to sync to default tag 48 49 Scheme []string 50 PingCmd string 51 52 RemoteRepo func(v *Cmd, rootDir string) (remoteRepo string, err error) 53 ResolveRepo func(v *Cmd, rootDir, remoteRepo string) (realRepo string, err error) 54 Status func(v *Cmd, rootDir string) (Status, error) 55 } 56 57 // Status is the current state of a local repository. 58 type Status struct { 59 Revision string // Optional. 60 CommitTime time.Time // Optional. 61 Uncommitted bool // Required. 62 } 63 64 var ( 65 // VCSTestRepoURL is the URL of the HTTP server that serves the repos for 66 // vcs-test.golang.org. 67 // 68 // In tests, this is set to the URL of an httptest.Server hosting a 69 // cmd/go/internal/vcweb.Server. 70 VCSTestRepoURL string 71 72 // VCSTestHosts is the set of hosts supported by the vcs-test server. 73 VCSTestHosts []string 74 75 // VCSTestIsLocalHost reports whether the given URL refers to a local 76 // (loopback) host, such as "localhost" or "127.0.0.1:8080". 77 VCSTestIsLocalHost func(*urlpkg.URL) bool 78 ) 79 80 var defaultSecureScheme = map[string]bool{ 81 "https": true, 82 "git+ssh": true, 83 "bzr+ssh": true, 84 "svn+ssh": true, 85 "ssh": true, 86 } 87 88 func (v *Cmd) IsSecure(repo string) bool { 89 u, err := urlpkg.Parse(repo) 90 if err != nil { 91 // If repo is not a URL, it's not secure. 92 return false 93 } 94 if VCSTestRepoURL != "" && web.IsLocalHost(u) { 95 // If the vcstest server is in use, it may redirect to other local ports for 96 // other protocols (such as svn). Assume that all loopback addresses are 97 // secure during testing. 98 return true 99 } 100 return v.isSecureScheme(u.Scheme) 101 } 102 103 func (v *Cmd) isSecureScheme(scheme string) bool { 104 switch v.Cmd { 105 case "git": 106 // GIT_ALLOW_PROTOCOL is an environment variable defined by Git. It is a 107 // colon-separated list of schemes that are allowed to be used with git 108 // fetch/clone. Any scheme not mentioned will be considered insecure. 109 if allow := os.Getenv("GIT_ALLOW_PROTOCOL"); allow != "" { 110 for _, s := range strings.Split(allow, ":") { 111 if s == scheme { 112 return true 113 } 114 } 115 return false 116 } 117 } 118 return defaultSecureScheme[scheme] 119 } 120 121 // A tagCmd describes a command to list available tags 122 // that can be passed to tagSyncCmd. 123 type tagCmd struct { 124 cmd string // command to list tags 125 pattern string // regexp to extract tags from list 126 } 127 128 // vcsList lists the known version control systems 129 var vcsList = []*Cmd{ 130 vcsHg, 131 vcsGit, 132 vcsSvn, 133 vcsBzr, 134 vcsFossil, 135 } 136 137 // vcsMod is a stub for the "mod" scheme. It's returned by 138 // repoRootForImportPathDynamic, but is otherwise not treated as a VCS command. 139 var vcsMod = &Cmd{Name: "mod"} 140 141 // vcsByCmd returns the version control system for the given 142 // command name (hg, git, svn, bzr). 143 func vcsByCmd(cmd string) *Cmd { 144 for _, vcs := range vcsList { 145 if vcs.Cmd == cmd { 146 return vcs 147 } 148 } 149 return nil 150 } 151 152 // vcsHg describes how to use Mercurial. 153 var vcsHg = &Cmd{ 154 Name: "Mercurial", 155 Cmd: "hg", 156 RootNames: []rootName{ 157 {filename: ".hg", isDir: true}, 158 }, 159 160 CreateCmd: []string{"clone -U -- {repo} {dir}"}, 161 DownloadCmd: []string{"pull"}, 162 163 // We allow both tag and branch names as 'tags' 164 // for selecting a version. This lets people have 165 // a go.release.r60 branch and a go1 branch 166 // and make changes in both, without constantly 167 // editing .hgtags. 168 TagCmd: []tagCmd{ 169 {"tags", `^(\S+)`}, 170 {"branches", `^(\S+)`}, 171 }, 172 TagSyncCmd: []string{"update -r {tag}"}, 173 TagSyncDefault: []string{"update default"}, 174 175 Scheme: []string{"https", "http", "ssh"}, 176 PingCmd: "identify -- {scheme}://{repo}", 177 RemoteRepo: hgRemoteRepo, 178 Status: hgStatus, 179 } 180 181 func hgRemoteRepo(vcsHg *Cmd, rootDir string) (remoteRepo string, err error) { 182 out, err := vcsHg.runOutput(rootDir, "paths default") 183 if err != nil { 184 return "", err 185 } 186 return strings.TrimSpace(string(out)), nil 187 } 188 189 func hgStatus(vcsHg *Cmd, rootDir string) (Status, error) { 190 // Output changeset ID and seconds since epoch. 191 out, err := vcsHg.runOutputVerboseOnly(rootDir, `log -l1 -T {node}:{date|hgdate}`) 192 if err != nil { 193 return Status{}, err 194 } 195 196 // Successful execution without output indicates an empty repo (no commits). 197 var rev string 198 var commitTime time.Time 199 if len(out) > 0 { 200 // Strip trailing timezone offset. 201 if i := bytes.IndexByte(out, ' '); i > 0 { 202 out = out[:i] 203 } 204 rev, commitTime, err = parseRevTime(out) 205 if err != nil { 206 return Status{}, err 207 } 208 } 209 210 // Also look for untracked files. 211 out, err = vcsHg.runOutputVerboseOnly(rootDir, "status") 212 if err != nil { 213 return Status{}, err 214 } 215 uncommitted := len(out) > 0 216 217 return Status{ 218 Revision: rev, 219 CommitTime: commitTime, 220 Uncommitted: uncommitted, 221 }, nil 222 } 223 224 // parseRevTime parses commit details in "revision:seconds" format. 225 func parseRevTime(out []byte) (string, time.Time, error) { 226 buf := string(bytes.TrimSpace(out)) 227 228 i := strings.IndexByte(buf, ':') 229 if i < 1 { 230 return "", time.Time{}, errors.New("unrecognized VCS tool output") 231 } 232 rev := buf[:i] 233 234 secs, err := strconv.ParseInt(string(buf[i+1:]), 10, 64) 235 if err != nil { 236 return "", time.Time{}, fmt.Errorf("unrecognized VCS tool output: %v", err) 237 } 238 239 return rev, time.Unix(secs, 0), nil 240 } 241 242 // vcsGit describes how to use Git. 243 var vcsGit = &Cmd{ 244 Name: "Git", 245 Cmd: "git", 246 RootNames: []rootName{ 247 {filename: ".git", isDir: true}, 248 }, 249 250 CreateCmd: []string{"clone -- {repo} {dir}", "-go-internal-cd {dir} submodule update --init --recursive"}, 251 DownloadCmd: []string{"pull --ff-only", "submodule update --init --recursive"}, 252 253 TagCmd: []tagCmd{ 254 // tags/xxx matches a git tag named xxx 255 // origin/xxx matches a git branch named xxx on the default remote repository 256 {"show-ref", `(?:tags|origin)/(\S+)$`}, 257 }, 258 TagLookupCmd: []tagCmd{ 259 {"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`}, 260 }, 261 TagSyncCmd: []string{"checkout {tag}", "submodule update --init --recursive"}, 262 // both createCmd and downloadCmd update the working dir. 263 // No need to do more here. We used to 'checkout master' 264 // but that doesn't work if the default branch is not named master. 265 // DO NOT add 'checkout master' here. 266 // See golang.org/issue/9032. 267 TagSyncDefault: []string{"submodule update --init --recursive"}, 268 269 Scheme: []string{"git", "https", "http", "git+ssh", "ssh"}, 270 271 // Leave out the '--' separator in the ls-remote command: git 2.7.4 does not 272 // support such a separator for that command, and this use should be safe 273 // without it because the {scheme} value comes from the predefined list above. 274 // See golang.org/issue/33836. 275 PingCmd: "ls-remote {scheme}://{repo}", 276 277 RemoteRepo: gitRemoteRepo, 278 Status: gitStatus, 279 } 280 281 // scpSyntaxRe matches the SCP-like addresses used by Git to access 282 // repositories by SSH. 283 var scpSyntaxRe = lazyregexp.New(`^(\w+)@([\w.-]+):(.*)$`) 284 285 func gitRemoteRepo(vcsGit *Cmd, rootDir string) (remoteRepo string, err error) { 286 const cmd = "config remote.origin.url" 287 outb, err := vcsGit.run1(rootDir, cmd, nil, false) 288 if err != nil { 289 // if it doesn't output any message, it means the config argument is correct, 290 // but the config value itself doesn't exist 291 if outb != nil && len(outb) == 0 { 292 return "", errors.New("remote origin not found") 293 } 294 return "", err 295 } 296 out := strings.TrimSpace(string(outb)) 297 298 var repoURL *urlpkg.URL 299 if m := scpSyntaxRe.FindStringSubmatch(out); m != nil { 300 // Match SCP-like syntax and convert it to a URL. 301 // Eg, "git@github.com:user/repo" becomes 302 // "ssh://git@github.com/user/repo". 303 repoURL = &urlpkg.URL{ 304 Scheme: "ssh", 305 User: urlpkg.User(m[1]), 306 Host: m[2], 307 Path: m[3], 308 } 309 } else { 310 repoURL, err = urlpkg.Parse(out) 311 if err != nil { 312 return "", err 313 } 314 } 315 316 // Iterate over insecure schemes too, because this function simply 317 // reports the state of the repo. If we can't see insecure schemes then 318 // we can't report the actual repo URL. 319 for _, s := range vcsGit.Scheme { 320 if repoURL.Scheme == s { 321 return repoURL.String(), nil 322 } 323 } 324 return "", errors.New("unable to parse output of git " + cmd) 325 } 326 327 func gitStatus(vcsGit *Cmd, rootDir string) (Status, error) { 328 out, err := vcsGit.runOutputVerboseOnly(rootDir, "status --porcelain") 329 if err != nil { 330 return Status{}, err 331 } 332 uncommitted := len(out) > 0 333 334 // "git status" works for empty repositories, but "git show" does not. 335 // Assume there are no commits in the repo when "git show" fails with 336 // uncommitted files and skip tagging revision / committime. 337 var rev string 338 var commitTime time.Time 339 out, err = vcsGit.runOutputVerboseOnly(rootDir, "-c log.showsignature=false show -s --format=%H:%ct") 340 if err != nil && !uncommitted { 341 return Status{}, err 342 } else if err == nil { 343 rev, commitTime, err = parseRevTime(out) 344 if err != nil { 345 return Status{}, err 346 } 347 } 348 349 return Status{ 350 Revision: rev, 351 CommitTime: commitTime, 352 Uncommitted: uncommitted, 353 }, nil 354 } 355 356 // vcsBzr describes how to use Bazaar. 357 var vcsBzr = &Cmd{ 358 Name: "Bazaar", 359 Cmd: "bzr", 360 RootNames: []rootName{ 361 {filename: ".bzr", isDir: true}, 362 }, 363 364 CreateCmd: []string{"branch -- {repo} {dir}"}, 365 366 // Without --overwrite bzr will not pull tags that changed. 367 // Replace by --overwrite-tags after http://pad.lv/681792 goes in. 368 DownloadCmd: []string{"pull --overwrite"}, 369 370 TagCmd: []tagCmd{{"tags", `^(\S+)`}}, 371 TagSyncCmd: []string{"update -r {tag}"}, 372 TagSyncDefault: []string{"update -r revno:-1"}, 373 374 Scheme: []string{"https", "http", "bzr", "bzr+ssh"}, 375 PingCmd: "info -- {scheme}://{repo}", 376 RemoteRepo: bzrRemoteRepo, 377 ResolveRepo: bzrResolveRepo, 378 Status: bzrStatus, 379 } 380 381 func bzrRemoteRepo(vcsBzr *Cmd, rootDir string) (remoteRepo string, err error) { 382 outb, err := vcsBzr.runOutput(rootDir, "config parent_location") 383 if err != nil { 384 return "", err 385 } 386 return strings.TrimSpace(string(outb)), nil 387 } 388 389 func bzrResolveRepo(vcsBzr *Cmd, rootDir, remoteRepo string) (realRepo string, err error) { 390 outb, err := vcsBzr.runOutput(rootDir, "info "+remoteRepo) 391 if err != nil { 392 return "", err 393 } 394 out := string(outb) 395 396 // Expect: 397 // ... 398 // (branch root|repository branch): <URL> 399 // ... 400 401 found := false 402 for _, prefix := range []string{"\n branch root: ", "\n repository branch: "} { 403 i := strings.Index(out, prefix) 404 if i >= 0 { 405 out = out[i+len(prefix):] 406 found = true 407 break 408 } 409 } 410 if !found { 411 return "", fmt.Errorf("unable to parse output of bzr info") 412 } 413 414 i := strings.Index(out, "\n") 415 if i < 0 { 416 return "", fmt.Errorf("unable to parse output of bzr info") 417 } 418 out = out[:i] 419 return strings.TrimSpace(out), nil 420 } 421 422 func bzrStatus(vcsBzr *Cmd, rootDir string) (Status, error) { 423 outb, err := vcsBzr.runOutputVerboseOnly(rootDir, "version-info") 424 if err != nil { 425 return Status{}, err 426 } 427 out := string(outb) 428 429 // Expect (non-empty repositories only): 430 // 431 // revision-id: gopher@gopher.net-20211021072330-qshok76wfypw9lpm 432 // date: 2021-09-21 12:00:00 +1000 433 // ... 434 var rev string 435 var commitTime time.Time 436 437 for _, line := range strings.Split(out, "\n") { 438 i := strings.IndexByte(line, ':') 439 if i < 0 { 440 continue 441 } 442 key := line[:i] 443 value := strings.TrimSpace(line[i+1:]) 444 445 switch key { 446 case "revision-id": 447 rev = value 448 case "date": 449 var err error 450 commitTime, err = time.Parse("2006-01-02 15:04:05 -0700", value) 451 if err != nil { 452 return Status{}, errors.New("unable to parse output of bzr version-info") 453 } 454 } 455 } 456 457 outb, err = vcsBzr.runOutputVerboseOnly(rootDir, "status") 458 if err != nil { 459 return Status{}, err 460 } 461 462 // Skip warning when working directory is set to an older revision. 463 if bytes.HasPrefix(outb, []byte("working tree is out of date")) { 464 i := bytes.IndexByte(outb, '\n') 465 if i < 0 { 466 i = len(outb) 467 } 468 outb = outb[:i] 469 } 470 uncommitted := len(outb) > 0 471 472 return Status{ 473 Revision: rev, 474 CommitTime: commitTime, 475 Uncommitted: uncommitted, 476 }, nil 477 } 478 479 // vcsSvn describes how to use Subversion. 480 var vcsSvn = &Cmd{ 481 Name: "Subversion", 482 Cmd: "svn", 483 RootNames: []rootName{ 484 {filename: ".svn", isDir: true}, 485 }, 486 487 CreateCmd: []string{"checkout -- {repo} {dir}"}, 488 DownloadCmd: []string{"update"}, 489 490 // There is no tag command in subversion. 491 // The branch information is all in the path names. 492 493 Scheme: []string{"https", "http", "svn", "svn+ssh"}, 494 PingCmd: "info -- {scheme}://{repo}", 495 RemoteRepo: svnRemoteRepo, 496 } 497 498 func svnRemoteRepo(vcsSvn *Cmd, rootDir string) (remoteRepo string, err error) { 499 outb, err := vcsSvn.runOutput(rootDir, "info") 500 if err != nil { 501 return "", err 502 } 503 out := string(outb) 504 505 // Expect: 506 // 507 // ... 508 // URL: <URL> 509 // ... 510 // 511 // Note that we're not using the Repository Root line, 512 // because svn allows checking out subtrees. 513 // The URL will be the URL of the subtree (what we used with 'svn co') 514 // while the Repository Root may be a much higher parent. 515 i := strings.Index(out, "\nURL: ") 516 if i < 0 { 517 return "", fmt.Errorf("unable to parse output of svn info") 518 } 519 out = out[i+len("\nURL: "):] 520 i = strings.Index(out, "\n") 521 if i < 0 { 522 return "", fmt.Errorf("unable to parse output of svn info") 523 } 524 out = out[:i] 525 return strings.TrimSpace(out), nil 526 } 527 528 // fossilRepoName is the name go get associates with a fossil repository. In the 529 // real world the file can be named anything. 530 const fossilRepoName = ".fossil" 531 532 // vcsFossil describes how to use Fossil (fossil-scm.org) 533 var vcsFossil = &Cmd{ 534 Name: "Fossil", 535 Cmd: "fossil", 536 RootNames: []rootName{ 537 {filename: ".fslckout", isDir: false}, 538 {filename: "_FOSSIL_", isDir: false}, 539 }, 540 541 CreateCmd: []string{"-go-internal-mkdir {dir} clone -- {repo} " + filepath.Join("{dir}", fossilRepoName), "-go-internal-cd {dir} open .fossil"}, 542 DownloadCmd: []string{"up"}, 543 544 TagCmd: []tagCmd{{"tag ls", `(.*)`}}, 545 TagSyncCmd: []string{"up tag:{tag}"}, 546 TagSyncDefault: []string{"up trunk"}, 547 548 Scheme: []string{"https", "http"}, 549 RemoteRepo: fossilRemoteRepo, 550 Status: fossilStatus, 551 } 552 553 func fossilRemoteRepo(vcsFossil *Cmd, rootDir string) (remoteRepo string, err error) { 554 out, err := vcsFossil.runOutput(rootDir, "remote-url") 555 if err != nil { 556 return "", err 557 } 558 return strings.TrimSpace(string(out)), nil 559 } 560 561 var errFossilInfo = errors.New("unable to parse output of fossil info") 562 563 func fossilStatus(vcsFossil *Cmd, rootDir string) (Status, error) { 564 outb, err := vcsFossil.runOutputVerboseOnly(rootDir, "info") 565 if err != nil { 566 return Status{}, err 567 } 568 out := string(outb) 569 570 // Expect: 571 // ... 572 // checkout: 91ed71f22c77be0c3e250920f47bfd4e1f9024d2 2021-09-21 12:00:00 UTC 573 // ... 574 575 // Extract revision and commit time. 576 // Ensure line ends with UTC (known timezone offset). 577 const prefix = "\ncheckout:" 578 const suffix = " UTC" 579 i := strings.Index(out, prefix) 580 if i < 0 { 581 return Status{}, errFossilInfo 582 } 583 checkout := out[i+len(prefix):] 584 i = strings.Index(checkout, suffix) 585 if i < 0 { 586 return Status{}, errFossilInfo 587 } 588 checkout = strings.TrimSpace(checkout[:i]) 589 590 i = strings.IndexByte(checkout, ' ') 591 if i < 0 { 592 return Status{}, errFossilInfo 593 } 594 rev := checkout[:i] 595 596 commitTime, err := time.ParseInLocation(time.DateTime, checkout[i+1:], time.UTC) 597 if err != nil { 598 return Status{}, fmt.Errorf("%v: %v", errFossilInfo, err) 599 } 600 601 // Also look for untracked changes. 602 outb, err = vcsFossil.runOutputVerboseOnly(rootDir, "changes --differ") 603 if err != nil { 604 return Status{}, err 605 } 606 uncommitted := len(outb) > 0 607 608 return Status{ 609 Revision: rev, 610 CommitTime: commitTime, 611 Uncommitted: uncommitted, 612 }, nil 613 } 614 615 func (v *Cmd) String() string { 616 return v.Name 617 } 618 619 // run runs the command line cmd in the given directory. 620 // keyval is a list of key, value pairs. run expands 621 // instances of {key} in cmd into value, but only after 622 // splitting cmd into individual arguments. 623 // If an error occurs, run prints the command line and the 624 // command's combined stdout+stderr to standard error. 625 // Otherwise run discards the command's output. 626 func (v *Cmd) run(dir string, cmd string, keyval ...string) error { 627 _, err := v.run1(dir, cmd, keyval, true) 628 return err 629 } 630 631 // runVerboseOnly is like run but only generates error output to standard error in verbose mode. 632 func (v *Cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error { 633 _, err := v.run1(dir, cmd, keyval, false) 634 return err 635 } 636 637 // runOutput is like run but returns the output of the command. 638 func (v *Cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) { 639 return v.run1(dir, cmd, keyval, true) 640 } 641 642 // runOutputVerboseOnly is like runOutput but only generates error output to 643 // standard error in verbose mode. 644 func (v *Cmd) runOutputVerboseOnly(dir string, cmd string, keyval ...string) ([]byte, error) { 645 return v.run1(dir, cmd, keyval, false) 646 } 647 648 // run1 is the generalized implementation of run and runOutput. 649 func (v *Cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) { 650 m := make(map[string]string) 651 for i := 0; i < len(keyval); i += 2 { 652 m[keyval[i]] = keyval[i+1] 653 } 654 args := strings.Fields(cmdline) 655 for i, arg := range args { 656 args[i] = expand(m, arg) 657 } 658 659 if len(args) >= 2 && args[0] == "-go-internal-mkdir" { 660 var err error 661 if filepath.IsAbs(args[1]) { 662 err = os.Mkdir(args[1], fs.ModePerm) 663 } else { 664 err = os.Mkdir(filepath.Join(dir, args[1]), fs.ModePerm) 665 } 666 if err != nil { 667 return nil, err 668 } 669 args = args[2:] 670 } 671 672 if len(args) >= 2 && args[0] == "-go-internal-cd" { 673 if filepath.IsAbs(args[1]) { 674 dir = args[1] 675 } else { 676 dir = filepath.Join(dir, args[1]) 677 } 678 args = args[2:] 679 } 680 681 _, err := cfg.LookPath(v.Cmd) 682 if err != nil { 683 fmt.Fprintf(os.Stderr, 684 "go: missing %s command. See https://golang.org/s/gogetcmd\n", 685 v.Name) 686 return nil, err 687 } 688 689 cmd := exec.Command(v.Cmd, args...) 690 cmd.Dir = dir 691 if cfg.BuildX { 692 fmt.Fprintf(os.Stderr, "cd %s\n", dir) 693 fmt.Fprintf(os.Stderr, "%s %s\n", v.Cmd, strings.Join(args, " ")) 694 } 695 out, err := cmd.Output() 696 if err != nil { 697 if verbose || cfg.BuildV { 698 fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.Cmd, strings.Join(args, " ")) 699 if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { 700 os.Stderr.Write(ee.Stderr) 701 } else { 702 fmt.Fprintln(os.Stderr, err.Error()) 703 } 704 } 705 } 706 return out, err 707 } 708 709 // Ping pings to determine scheme to use. 710 func (v *Cmd) Ping(scheme, repo string) error { 711 // Run the ping command in an arbitrary working directory, 712 // but don't let the current working directory pollute the results. 713 // In module mode, we expect GOMODCACHE to exist and be a safe place for 714 // commands; in GOPATH mode, we expect that to be true of GOPATH/src. 715 dir := cfg.GOMODCACHE 716 if !cfg.ModulesEnabled { 717 dir = filepath.Join(cfg.BuildContext.GOPATH, "src") 718 } 719 os.MkdirAll(dir, 0777) // Ignore errors — if unsuccessful, the command will likely fail. 720 721 release, err := base.AcquireNet() 722 if err != nil { 723 return err 724 } 725 defer release() 726 727 return v.runVerboseOnly(dir, v.PingCmd, "scheme", scheme, "repo", repo) 728 } 729 730 // Create creates a new copy of repo in dir. 731 // The parent of dir must exist; dir must not. 732 func (v *Cmd) Create(dir, repo string) error { 733 release, err := base.AcquireNet() 734 if err != nil { 735 return err 736 } 737 defer release() 738 739 for _, cmd := range v.CreateCmd { 740 if err := v.run(filepath.Dir(dir), cmd, "dir", dir, "repo", repo); err != nil { 741 return err 742 } 743 } 744 return nil 745 } 746 747 // Download downloads any new changes for the repo in dir. 748 func (v *Cmd) Download(dir string) error { 749 release, err := base.AcquireNet() 750 if err != nil { 751 return err 752 } 753 defer release() 754 755 for _, cmd := range v.DownloadCmd { 756 if err := v.run(dir, cmd); err != nil { 757 return err 758 } 759 } 760 return nil 761 } 762 763 // Tags returns the list of available tags for the repo in dir. 764 func (v *Cmd) Tags(dir string) ([]string, error) { 765 var tags []string 766 for _, tc := range v.TagCmd { 767 out, err := v.runOutput(dir, tc.cmd) 768 if err != nil { 769 return nil, err 770 } 771 re := regexp.MustCompile(`(?m-s)` + tc.pattern) 772 for _, m := range re.FindAllStringSubmatch(string(out), -1) { 773 tags = append(tags, m[1]) 774 } 775 } 776 return tags, nil 777 } 778 779 // TagSync syncs the repo in dir to the named tag, 780 // which either is a tag returned by tags or is v.tagDefault. 781 func (v *Cmd) TagSync(dir, tag string) error { 782 if v.TagSyncCmd == nil { 783 return nil 784 } 785 if tag != "" { 786 for _, tc := range v.TagLookupCmd { 787 out, err := v.runOutput(dir, tc.cmd, "tag", tag) 788 if err != nil { 789 return err 790 } 791 re := regexp.MustCompile(`(?m-s)` + tc.pattern) 792 m := re.FindStringSubmatch(string(out)) 793 if len(m) > 1 { 794 tag = m[1] 795 break 796 } 797 } 798 } 799 800 release, err := base.AcquireNet() 801 if err != nil { 802 return err 803 } 804 defer release() 805 806 if tag == "" && v.TagSyncDefault != nil { 807 for _, cmd := range v.TagSyncDefault { 808 if err := v.run(dir, cmd); err != nil { 809 return err 810 } 811 } 812 return nil 813 } 814 815 for _, cmd := range v.TagSyncCmd { 816 if err := v.run(dir, cmd, "tag", tag); err != nil { 817 return err 818 } 819 } 820 return nil 821 } 822 823 // A vcsPath describes how to convert an import path into a 824 // version control system and repository name. 825 type vcsPath struct { 826 pathPrefix string // prefix this description applies to 827 regexp *lazyregexp.Regexp // compiled pattern for import path 828 repo string // repository to use (expand with match of re) 829 vcs string // version control system to use (expand with match of re) 830 check func(match map[string]string) error // additional checks 831 schemelessRepo bool // if true, the repo pattern lacks a scheme 832 } 833 834 // FromDir inspects dir and its parents to determine the 835 // version control system and code repository to use. 836 // If no repository is found, FromDir returns an error 837 // equivalent to os.ErrNotExist. 838 func FromDir(dir, srcRoot string, allowNesting bool) (repoDir string, vcsCmd *Cmd, err error) { 839 // Clean and double-check that dir is in (a subdirectory of) srcRoot. 840 dir = filepath.Clean(dir) 841 if srcRoot != "" { 842 srcRoot = filepath.Clean(srcRoot) 843 if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator { 844 return "", nil, fmt.Errorf("directory %q is outside source root %q", dir, srcRoot) 845 } 846 } 847 848 origDir := dir 849 for len(dir) > len(srcRoot) { 850 for _, vcs := range vcsList { 851 if isVCSRoot(dir, vcs.RootNames) { 852 // Record first VCS we find. 853 // If allowNesting is false (as it is in GOPATH), keep looking for 854 // repositories in parent directories and report an error if one is 855 // found to mitigate VCS injection attacks. 856 if vcsCmd == nil { 857 vcsCmd = vcs 858 repoDir = dir 859 if allowNesting { 860 return repoDir, vcsCmd, nil 861 } 862 continue 863 } 864 // Otherwise, we have one VCS inside a different VCS. 865 return "", nil, fmt.Errorf("directory %q uses %s, but parent %q uses %s", 866 repoDir, vcsCmd.Cmd, dir, vcs.Cmd) 867 } 868 } 869 870 // Move to parent. 871 ndir := filepath.Dir(dir) 872 if len(ndir) >= len(dir) { 873 break 874 } 875 dir = ndir 876 } 877 if vcsCmd == nil { 878 return "", nil, &vcsNotFoundError{dir: origDir} 879 } 880 return repoDir, vcsCmd, nil 881 } 882 883 // isVCSRoot identifies a VCS root by checking whether the directory contains 884 // any of the listed root names. 885 func isVCSRoot(dir string, rootNames []rootName) bool { 886 for _, root := range rootNames { 887 fi, err := os.Stat(filepath.Join(dir, root.filename)) 888 if err == nil && fi.IsDir() == root.isDir { 889 return true 890 } 891 } 892 893 return false 894 } 895 896 type rootName struct { 897 filename string 898 isDir bool 899 } 900 901 type vcsNotFoundError struct { 902 dir string 903 } 904 905 func (e *vcsNotFoundError) Error() string { 906 return fmt.Sprintf("directory %q is not using a known version control system", e.dir) 907 } 908 909 func (e *vcsNotFoundError) Is(err error) bool { 910 return err == os.ErrNotExist 911 } 912 913 // A govcsRule is a single GOVCS rule like private:hg|svn. 914 type govcsRule struct { 915 pattern string 916 allowed []string 917 } 918 919 // A govcsConfig is a full GOVCS configuration. 920 type govcsConfig []govcsRule 921 922 func parseGOVCS(s string) (govcsConfig, error) { 923 s = strings.TrimSpace(s) 924 if s == "" { 925 return nil, nil 926 } 927 var cfg govcsConfig 928 have := make(map[string]string) 929 for _, item := range strings.Split(s, ",") { 930 item = strings.TrimSpace(item) 931 if item == "" { 932 return nil, fmt.Errorf("empty entry in GOVCS") 933 } 934 pattern, list, found := strings.Cut(item, ":") 935 if !found { 936 return nil, fmt.Errorf("malformed entry in GOVCS (missing colon): %q", item) 937 } 938 pattern, list = strings.TrimSpace(pattern), strings.TrimSpace(list) 939 if pattern == "" { 940 return nil, fmt.Errorf("empty pattern in GOVCS: %q", item) 941 } 942 if list == "" { 943 return nil, fmt.Errorf("empty VCS list in GOVCS: %q", item) 944 } 945 if search.IsRelativePath(pattern) { 946 return nil, fmt.Errorf("relative pattern not allowed in GOVCS: %q", pattern) 947 } 948 if old := have[pattern]; old != "" { 949 return nil, fmt.Errorf("unreachable pattern in GOVCS: %q after %q", item, old) 950 } 951 have[pattern] = item 952 allowed := strings.Split(list, "|") 953 for i, a := range allowed { 954 a = strings.TrimSpace(a) 955 if a == "" { 956 return nil, fmt.Errorf("empty VCS name in GOVCS: %q", item) 957 } 958 allowed[i] = a 959 } 960 cfg = append(cfg, govcsRule{pattern, allowed}) 961 } 962 return cfg, nil 963 } 964 965 func (c *govcsConfig) allow(path string, private bool, vcs string) bool { 966 for _, rule := range *c { 967 match := false 968 switch rule.pattern { 969 case "private": 970 match = private 971 case "public": 972 match = !private 973 default: 974 // Note: rule.pattern is known to be comma-free, 975 // so MatchPrefixPatterns is only matching a single pattern for us. 976 match = module.MatchPrefixPatterns(rule.pattern, path) 977 } 978 if !match { 979 continue 980 } 981 for _, allow := range rule.allowed { 982 if allow == vcs || allow == "all" { 983 return true 984 } 985 } 986 return false 987 } 988 989 // By default, nothing is allowed. 990 return false 991 } 992 993 var ( 994 govcs govcsConfig 995 govcsErr error 996 govcsOnce sync.Once 997 ) 998 999 // defaultGOVCS is the default setting for GOVCS. 1000 // Setting GOVCS adds entries ahead of these but does not remove them. 1001 // (They are appended to the parsed GOVCS setting.) 1002 // 1003 // The rationale behind allowing only Git and Mercurial is that 1004 // these two systems have had the most attention to issues 1005 // of being run as clients of untrusted servers. In contrast, 1006 // Bazaar, Fossil, and Subversion have primarily been used 1007 // in trusted, authenticated environments and are not as well 1008 // scrutinized as attack surfaces. 1009 // 1010 // See golang.org/issue/41730 for details. 1011 var defaultGOVCS = govcsConfig{ 1012 {"private", []string{"all"}}, 1013 {"public", []string{"git", "hg"}}, 1014 } 1015 1016 // checkGOVCS checks whether the policy defined by the environment variable 1017 // GOVCS allows the given vcs command to be used with the given repository 1018 // root path. Note that root may not be a real package or module path; it's 1019 // the same as the root path in the go-import meta tag. 1020 func checkGOVCS(vcs *Cmd, root string) error { 1021 if vcs == vcsMod { 1022 // Direct module (proxy protocol) fetches don't 1023 // involve an external version control system 1024 // and are always allowed. 1025 return nil 1026 } 1027 1028 govcsOnce.Do(func() { 1029 govcs, govcsErr = parseGOVCS(os.Getenv("GOVCS")) 1030 govcs = append(govcs, defaultGOVCS...) 1031 }) 1032 if govcsErr != nil { 1033 return govcsErr 1034 } 1035 1036 private := module.MatchPrefixPatterns(cfg.GOPRIVATE, root) 1037 if !govcs.allow(root, private, vcs.Cmd) { 1038 what := "public" 1039 if private { 1040 what = "private" 1041 } 1042 return fmt.Errorf("GOVCS disallows using %s for %s %s; see 'go help vcs'", vcs.Cmd, what, root) 1043 } 1044 1045 return nil 1046 } 1047 1048 // RepoRoot describes the repository root for a tree of source code. 1049 type RepoRoot struct { 1050 Repo string // repository URL, including scheme 1051 Root string // import path corresponding to root of repo 1052 IsCustom bool // defined by served <meta> tags (as opposed to hard-coded pattern) 1053 VCS *Cmd 1054 } 1055 1056 func httpPrefix(s string) string { 1057 for _, prefix := range [...]string{"http:", "https:"} { 1058 if strings.HasPrefix(s, prefix) { 1059 return prefix 1060 } 1061 } 1062 return "" 1063 } 1064 1065 // ModuleMode specifies whether to prefer modules when looking up code sources. 1066 type ModuleMode int 1067 1068 const ( 1069 IgnoreMod ModuleMode = iota 1070 PreferMod 1071 ) 1072 1073 // RepoRootForImportPath analyzes importPath to determine the 1074 // version control system, and code repository to use. 1075 func RepoRootForImportPath(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) { 1076 rr, err := repoRootFromVCSPaths(importPath, security, vcsPaths) 1077 if err == errUnknownSite { 1078 rr, err = repoRootForImportDynamic(importPath, mod, security) 1079 if err != nil { 1080 err = importErrorf(importPath, "unrecognized import path %q: %v", importPath, err) 1081 } 1082 } 1083 if err != nil { 1084 rr1, err1 := repoRootFromVCSPaths(importPath, security, vcsPathsAfterDynamic) 1085 if err1 == nil { 1086 rr = rr1 1087 err = nil 1088 } 1089 } 1090 1091 // Should have been taken care of above, but make sure. 1092 if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") { 1093 // Do not allow wildcards in the repo root. 1094 rr = nil 1095 err = importErrorf(importPath, "cannot expand ... in %q", importPath) 1096 } 1097 return rr, err 1098 } 1099 1100 var errUnknownSite = errors.New("dynamic lookup required to find mapping") 1101 1102 // repoRootFromVCSPaths attempts to map importPath to a repoRoot 1103 // using the mappings defined in vcsPaths. 1104 func repoRootFromVCSPaths(importPath string, security web.SecurityMode, vcsPaths []*vcsPath) (*RepoRoot, error) { 1105 if str.HasPathPrefix(importPath, "example.net") { 1106 // TODO(rsc): This should not be necessary, but it's required to keep 1107 // tests like ../../testdata/script/mod_get_extra.txt from using the network. 1108 // That script has everything it needs in the replacement set, but it is still 1109 // doing network calls. 1110 return nil, fmt.Errorf("no modules on example.net") 1111 } 1112 if importPath == "rsc.io" { 1113 // This special case allows tests like ../../testdata/script/govcs.txt 1114 // to avoid making any network calls. The module lookup for a path 1115 // like rsc.io/nonexist.svn/foo needs to not make a network call for 1116 // a lookup on rsc.io. 1117 return nil, fmt.Errorf("rsc.io is not a module") 1118 } 1119 // A common error is to use https://packagepath because that's what 1120 // hg and git require. Diagnose this helpfully. 1121 if prefix := httpPrefix(importPath); prefix != "" { 1122 // The importPath has been cleaned, so has only one slash. The pattern 1123 // ignores the slashes; the error message puts them back on the RHS at least. 1124 return nil, fmt.Errorf("%q not allowed in import path", prefix+"//") 1125 } 1126 for _, srv := range vcsPaths { 1127 if !str.HasPathPrefix(importPath, srv.pathPrefix) { 1128 continue 1129 } 1130 m := srv.regexp.FindStringSubmatch(importPath) 1131 if m == nil { 1132 if srv.pathPrefix != "" { 1133 return nil, importErrorf(importPath, "invalid %s import path %q", srv.pathPrefix, importPath) 1134 } 1135 continue 1136 } 1137 1138 // Build map of named subexpression matches for expand. 1139 match := map[string]string{ 1140 "prefix": srv.pathPrefix + "/", 1141 "import": importPath, 1142 } 1143 for i, name := range srv.regexp.SubexpNames() { 1144 if name != "" && match[name] == "" { 1145 match[name] = m[i] 1146 } 1147 } 1148 if srv.vcs != "" { 1149 match["vcs"] = expand(match, srv.vcs) 1150 } 1151 if srv.repo != "" { 1152 match["repo"] = expand(match, srv.repo) 1153 } 1154 if srv.check != nil { 1155 if err := srv.check(match); err != nil { 1156 return nil, err 1157 } 1158 } 1159 vcs := vcsByCmd(match["vcs"]) 1160 if vcs == nil { 1161 return nil, fmt.Errorf("unknown version control system %q", match["vcs"]) 1162 } 1163 if err := checkGOVCS(vcs, match["root"]); err != nil { 1164 return nil, err 1165 } 1166 var repoURL string 1167 if !srv.schemelessRepo { 1168 repoURL = match["repo"] 1169 } else { 1170 repo := match["repo"] 1171 var ok bool 1172 repoURL, ok = interceptVCSTest(repo, vcs, security) 1173 if !ok { 1174 scheme, err := func() (string, error) { 1175 for _, s := range vcs.Scheme { 1176 if security == web.SecureOnly && !vcs.isSecureScheme(s) { 1177 continue 1178 } 1179 1180 // If we know how to ping URL schemes for this VCS, 1181 // check that this repo works. 1182 // Otherwise, default to the first scheme 1183 // that meets the requested security level. 1184 if vcs.PingCmd == "" { 1185 return s, nil 1186 } 1187 if err := vcs.Ping(s, repo); err == nil { 1188 return s, nil 1189 } 1190 } 1191 securityFrag := "" 1192 if security == web.SecureOnly { 1193 securityFrag = "secure " 1194 } 1195 return "", fmt.Errorf("no %sprotocol found for repository", securityFrag) 1196 }() 1197 if err != nil { 1198 return nil, err 1199 } 1200 repoURL = scheme + "://" + repo 1201 } 1202 } 1203 rr := &RepoRoot{ 1204 Repo: repoURL, 1205 Root: match["root"], 1206 VCS: vcs, 1207 } 1208 return rr, nil 1209 } 1210 return nil, errUnknownSite 1211 } 1212 1213 func interceptVCSTest(repo string, vcs *Cmd, security web.SecurityMode) (repoURL string, ok bool) { 1214 if VCSTestRepoURL == "" { 1215 return "", false 1216 } 1217 if vcs == vcsMod { 1218 // Since the "mod" protocol is implemented internally, 1219 // requests will be intercepted at a lower level (in cmd/go/internal/web). 1220 return "", false 1221 } 1222 1223 if scheme, path, ok := strings.Cut(repo, "://"); ok { 1224 if security == web.SecureOnly && !vcs.isSecureScheme(scheme) { 1225 return "", false // Let the caller reject the original URL. 1226 } 1227 repo = path // Remove leading URL scheme if present. 1228 } 1229 for _, host := range VCSTestHosts { 1230 if !str.HasPathPrefix(repo, host) { 1231 continue 1232 } 1233 1234 httpURL := VCSTestRepoURL + strings.TrimPrefix(repo, host) 1235 1236 if vcs == vcsSvn { 1237 // Ping the vcweb HTTP server to tell it to initialize the SVN repository 1238 // and get the SVN server URL. 1239 u, err := urlpkg.Parse(httpURL + "?vcwebsvn=1") 1240 if err != nil { 1241 panic(fmt.Sprintf("invalid vcs-test repo URL: %v", err)) 1242 } 1243 svnURL, err := web.GetBytes(u) 1244 svnURL = bytes.TrimSpace(svnURL) 1245 if err == nil && len(svnURL) > 0 { 1246 return string(svnURL) + strings.TrimPrefix(repo, host), true 1247 } 1248 1249 // vcs-test doesn't have a svn handler for the given path, 1250 // so resolve the repo to HTTPS instead. 1251 } 1252 1253 return httpURL, true 1254 } 1255 return "", false 1256 } 1257 1258 // urlForImportPath returns a partially-populated URL for the given Go import path. 1259 // 1260 // The URL leaves the Scheme field blank so that web.Get will try any scheme 1261 // allowed by the selected security mode. 1262 func urlForImportPath(importPath string) (*urlpkg.URL, error) { 1263 slash := strings.Index(importPath, "/") 1264 if slash < 0 { 1265 slash = len(importPath) 1266 } 1267 host, path := importPath[:slash], importPath[slash:] 1268 if !strings.Contains(host, ".") { 1269 return nil, errors.New("import path does not begin with hostname") 1270 } 1271 if len(path) == 0 { 1272 path = "/" 1273 } 1274 return &urlpkg.URL{Host: host, Path: path, RawQuery: "go-get=1"}, nil 1275 } 1276 1277 // repoRootForImportDynamic finds a *RepoRoot for a custom domain that's not 1278 // statically known by repoRootFromVCSPaths. 1279 // 1280 // This handles custom import paths like "name.tld/pkg/foo" or just "name.tld". 1281 func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) { 1282 url, err := urlForImportPath(importPath) 1283 if err != nil { 1284 return nil, err 1285 } 1286 resp, err := web.Get(security, url) 1287 if err != nil { 1288 msg := "https fetch: %v" 1289 if security == web.Insecure { 1290 msg = "http/" + msg 1291 } 1292 return nil, fmt.Errorf(msg, err) 1293 } 1294 body := resp.Body 1295 defer body.Close() 1296 imports, err := parseMetaGoImports(body, mod) 1297 if len(imports) == 0 { 1298 if respErr := resp.Err(); respErr != nil { 1299 // If the server's status was not OK, prefer to report that instead of 1300 // an XML parse error. 1301 return nil, respErr 1302 } 1303 } 1304 if err != nil { 1305 return nil, fmt.Errorf("parsing %s: %v", importPath, err) 1306 } 1307 // Find the matched meta import. 1308 mmi, err := matchGoImport(imports, importPath) 1309 if err != nil { 1310 if _, ok := err.(ImportMismatchError); !ok { 1311 return nil, fmt.Errorf("parse %s: %v", url, err) 1312 } 1313 return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", resp.URL, err) 1314 } 1315 if cfg.BuildV { 1316 log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, url) 1317 } 1318 // If the import was "uni.edu/bob/project", which said the 1319 // prefix was "uni.edu" and the RepoRoot was "evilroot.com", 1320 // make sure we don't trust Bob and check out evilroot.com to 1321 // "uni.edu" yet (possibly overwriting/preempting another 1322 // non-evil student). Instead, first verify the root and see 1323 // if it matches Bob's claim. 1324 if mmi.Prefix != importPath { 1325 if cfg.BuildV { 1326 log.Printf("get %q: verifying non-authoritative meta tag", importPath) 1327 } 1328 var imports []metaImport 1329 url, imports, err = metaImportsForPrefix(mmi.Prefix, mod, security) 1330 if err != nil { 1331 return nil, err 1332 } 1333 metaImport2, err := matchGoImport(imports, importPath) 1334 if err != nil || mmi != metaImport2 { 1335 return nil, fmt.Errorf("%s and %s disagree about go-import for %s", resp.URL, url, mmi.Prefix) 1336 } 1337 } 1338 1339 if err := validateRepoRoot(mmi.RepoRoot); err != nil { 1340 return nil, fmt.Errorf("%s: invalid repo root %q: %v", resp.URL, mmi.RepoRoot, err) 1341 } 1342 var vcs *Cmd 1343 if mmi.VCS == "mod" { 1344 vcs = vcsMod 1345 } else { 1346 vcs = vcsByCmd(mmi.VCS) 1347 if vcs == nil { 1348 return nil, fmt.Errorf("%s: unknown vcs %q", resp.URL, mmi.VCS) 1349 } 1350 } 1351 1352 if err := checkGOVCS(vcs, mmi.Prefix); err != nil { 1353 return nil, err 1354 } 1355 1356 repoURL, ok := interceptVCSTest(mmi.RepoRoot, vcs, security) 1357 if !ok { 1358 repoURL = mmi.RepoRoot 1359 } 1360 rr := &RepoRoot{ 1361 Repo: repoURL, 1362 Root: mmi.Prefix, 1363 IsCustom: true, 1364 VCS: vcs, 1365 } 1366 return rr, nil 1367 } 1368 1369 // validateRepoRoot returns an error if repoRoot does not seem to be 1370 // a valid URL with scheme. 1371 func validateRepoRoot(repoRoot string) error { 1372 url, err := urlpkg.Parse(repoRoot) 1373 if err != nil { 1374 return err 1375 } 1376 if url.Scheme == "" { 1377 return errors.New("no scheme") 1378 } 1379 if url.Scheme == "file" { 1380 return errors.New("file scheme disallowed") 1381 } 1382 return nil 1383 } 1384 1385 var fetchGroup singleflight.Group 1386 var ( 1387 fetchCacheMu sync.Mutex 1388 fetchCache = map[string]fetchResult{} // key is metaImportsForPrefix's importPrefix 1389 ) 1390 1391 // metaImportsForPrefix takes a package's root import path as declared in a <meta> tag 1392 // and returns its HTML discovery URL and the parsed metaImport lines 1393 // found on the page. 1394 // 1395 // The importPath is of the form "golang.org/x/tools". 1396 // It is an error if no imports are found. 1397 // url will still be valid if err != nil. 1398 // The returned url will be of the form "https://golang.org/x/tools?go-get=1" 1399 func metaImportsForPrefix(importPrefix string, mod ModuleMode, security web.SecurityMode) (*urlpkg.URL, []metaImport, error) { 1400 setCache := func(res fetchResult) (fetchResult, error) { 1401 fetchCacheMu.Lock() 1402 defer fetchCacheMu.Unlock() 1403 fetchCache[importPrefix] = res 1404 return res, nil 1405 } 1406 1407 resi, _, _ := fetchGroup.Do(importPrefix, func() (resi any, err error) { 1408 fetchCacheMu.Lock() 1409 if res, ok := fetchCache[importPrefix]; ok { 1410 fetchCacheMu.Unlock() 1411 return res, nil 1412 } 1413 fetchCacheMu.Unlock() 1414 1415 url, err := urlForImportPath(importPrefix) 1416 if err != nil { 1417 return setCache(fetchResult{err: err}) 1418 } 1419 resp, err := web.Get(security, url) 1420 if err != nil { 1421 return setCache(fetchResult{url: url, err: fmt.Errorf("fetching %s: %v", importPrefix, err)}) 1422 } 1423 body := resp.Body 1424 defer body.Close() 1425 imports, err := parseMetaGoImports(body, mod) 1426 if len(imports) == 0 { 1427 if respErr := resp.Err(); respErr != nil { 1428 // If the server's status was not OK, prefer to report that instead of 1429 // an XML parse error. 1430 return setCache(fetchResult{url: url, err: respErr}) 1431 } 1432 } 1433 if err != nil { 1434 return setCache(fetchResult{url: url, err: fmt.Errorf("parsing %s: %v", resp.URL, err)}) 1435 } 1436 if len(imports) == 0 { 1437 err = fmt.Errorf("fetching %s: no go-import meta tag found in %s", importPrefix, resp.URL) 1438 } 1439 return setCache(fetchResult{url: url, imports: imports, err: err}) 1440 }) 1441 res := resi.(fetchResult) 1442 return res.url, res.imports, res.err 1443 } 1444 1445 type fetchResult struct { 1446 url *urlpkg.URL 1447 imports []metaImport 1448 err error 1449 } 1450 1451 // metaImport represents the parsed <meta name="go-import" 1452 // content="prefix vcs reporoot" /> tags from HTML files. 1453 type metaImport struct { 1454 Prefix, VCS, RepoRoot string 1455 } 1456 1457 // An ImportMismatchError is returned where metaImport/s are present 1458 // but none match our import path. 1459 type ImportMismatchError struct { 1460 importPath string 1461 mismatches []string // the meta imports that were discarded for not matching our importPath 1462 } 1463 1464 func (m ImportMismatchError) Error() string { 1465 formattedStrings := make([]string, len(m.mismatches)) 1466 for i, pre := range m.mismatches { 1467 formattedStrings[i] = fmt.Sprintf("meta tag %s did not match import path %s", pre, m.importPath) 1468 } 1469 return strings.Join(formattedStrings, ", ") 1470 } 1471 1472 // matchGoImport returns the metaImport from imports matching importPath. 1473 // An error is returned if there are multiple matches. 1474 // An ImportMismatchError is returned if none match. 1475 func matchGoImport(imports []metaImport, importPath string) (metaImport, error) { 1476 match := -1 1477 1478 errImportMismatch := ImportMismatchError{importPath: importPath} 1479 for i, im := range imports { 1480 if !str.HasPathPrefix(importPath, im.Prefix) { 1481 errImportMismatch.mismatches = append(errImportMismatch.mismatches, im.Prefix) 1482 continue 1483 } 1484 1485 if match >= 0 { 1486 if imports[match].VCS == "mod" && im.VCS != "mod" { 1487 // All the mod entries precede all the non-mod entries. 1488 // We have a mod entry and don't care about the rest, 1489 // matching or not. 1490 break 1491 } 1492 return metaImport{}, fmt.Errorf("multiple meta tags match import path %q", importPath) 1493 } 1494 match = i 1495 } 1496 1497 if match == -1 { 1498 return metaImport{}, errImportMismatch 1499 } 1500 return imports[match], nil 1501 } 1502 1503 // expand rewrites s to replace {k} with match[k] for each key k in match. 1504 func expand(match map[string]string, s string) string { 1505 // We want to replace each match exactly once, and the result of expansion 1506 // must not depend on the iteration order through the map. 1507 // A strings.Replacer has exactly the properties we're looking for. 1508 oldNew := make([]string, 0, 2*len(match)) 1509 for k, v := range match { 1510 oldNew = append(oldNew, "{"+k+"}", v) 1511 } 1512 return strings.NewReplacer(oldNew...).Replace(s) 1513 } 1514 1515 // vcsPaths defines the meaning of import paths referring to 1516 // commonly-used VCS hosting sites (github.com/user/dir) 1517 // and import paths referring to a fully-qualified importPath 1518 // containing a VCS type (foo.com/repo.git/dir) 1519 var vcsPaths = []*vcsPath{ 1520 // GitHub 1521 { 1522 pathPrefix: "github.com", 1523 regexp: lazyregexp.New(`^(?P<root>github\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`), 1524 vcs: "git", 1525 repo: "https://{root}", 1526 check: noVCSSuffix, 1527 }, 1528 1529 // Bitbucket 1530 { 1531 pathPrefix: "bitbucket.org", 1532 regexp: lazyregexp.New(`^(?P<root>bitbucket\.org/(?P<bitname>[\w.\-]+/[\w.\-]+))(/[\w.\-]+)*$`), 1533 vcs: "git", 1534 repo: "https://{root}", 1535 check: noVCSSuffix, 1536 }, 1537 1538 // IBM DevOps Services (JazzHub) 1539 { 1540 pathPrefix: "hub.jazz.net/git", 1541 regexp: lazyregexp.New(`^(?P<root>hub\.jazz\.net/git/[a-z0-9]+/[\w.\-]+)(/[\w.\-]+)*$`), 1542 vcs: "git", 1543 repo: "https://{root}", 1544 check: noVCSSuffix, 1545 }, 1546 1547 // Git at Apache 1548 { 1549 pathPrefix: "git.apache.org", 1550 regexp: lazyregexp.New(`^(?P<root>git\.apache\.org/[a-z0-9_.\-]+\.git)(/[\w.\-]+)*$`), 1551 vcs: "git", 1552 repo: "https://{root}", 1553 }, 1554 1555 // Git at OpenStack 1556 { 1557 pathPrefix: "git.openstack.org", 1558 regexp: lazyregexp.New(`^(?P<root>git\.openstack\.org/[\w.\-]+/[\w.\-]+)(\.git)?(/[\w.\-]+)*$`), 1559 vcs: "git", 1560 repo: "https://{root}", 1561 }, 1562 1563 // chiselapp.com for fossil 1564 { 1565 pathPrefix: "chiselapp.com", 1566 regexp: lazyregexp.New(`^(?P<root>chiselapp\.com/user/[A-Za-z0-9]+/repository/[\w.\-]+)$`), 1567 vcs: "fossil", 1568 repo: "https://{root}", 1569 }, 1570 1571 // General syntax for any server. 1572 // Must be last. 1573 { 1574 regexp: lazyregexp.New(`(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?(/~?[\w.\-]+)+?)\.(?P<vcs>bzr|fossil|git|hg|svn))(/~?[\w.\-]+)*$`), 1575 schemelessRepo: true, 1576 }, 1577 } 1578 1579 // vcsPathsAfterDynamic gives additional vcsPaths entries 1580 // to try after the dynamic HTML check. 1581 // This gives those sites a chance to introduce <meta> tags 1582 // as part of a graceful transition away from the hard-coded logic. 1583 var vcsPathsAfterDynamic = []*vcsPath{ 1584 // Launchpad. See golang.org/issue/11436. 1585 { 1586 pathPrefix: "launchpad.net", 1587 regexp: lazyregexp.New(`^(?P<root>launchpad\.net/((?P<project>[\w.\-]+)(?P<series>/[\w.\-]+)?|~[\w.\-]+/(\+junk|[\w.\-]+)/[\w.\-]+))(/[\w.\-]+)*$`), 1588 vcs: "bzr", 1589 repo: "https://{root}", 1590 check: launchpadVCS, 1591 }, 1592 } 1593 1594 // noVCSSuffix checks that the repository name does not 1595 // end in .foo for any version control system foo. 1596 // The usual culprit is ".git". 1597 func noVCSSuffix(match map[string]string) error { 1598 repo := match["repo"] 1599 for _, vcs := range vcsList { 1600 if strings.HasSuffix(repo, "."+vcs.Cmd) { 1601 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"]) 1602 } 1603 } 1604 return nil 1605 } 1606 1607 // launchpadVCS solves the ambiguity for "lp.net/project/foo". In this case, 1608 // "foo" could be a series name registered in Launchpad with its own branch, 1609 // and it could also be the name of a directory within the main project 1610 // branch one level up. 1611 func launchpadVCS(match map[string]string) error { 1612 if match["project"] == "" || match["series"] == "" { 1613 return nil 1614 } 1615 url := &urlpkg.URL{ 1616 Scheme: "https", 1617 Host: "code.launchpad.net", 1618 Path: expand(match, "/{project}{series}/.bzr/branch-format"), 1619 } 1620 _, err := web.GetBytes(url) 1621 if err != nil { 1622 match["root"] = expand(match, "launchpad.net/{project}") 1623 match["repo"] = expand(match, "https://{root}") 1624 } 1625 return nil 1626 } 1627 1628 // importError is a copy of load.importError, made to avoid a dependency cycle 1629 // on cmd/go/internal/load. It just needs to satisfy load.ImportPathError. 1630 type importError struct { 1631 importPath string 1632 err error 1633 } 1634 1635 func importErrorf(path, format string, args ...any) error { 1636 err := &importError{importPath: path, err: fmt.Errorf(format, args...)} 1637 if errStr := err.Error(); !strings.Contains(errStr, path) { 1638 panic(fmt.Sprintf("path %q not in error %q", path, errStr)) 1639 } 1640 return err 1641 } 1642 1643 func (e *importError) Error() string { 1644 return e.err.Error() 1645 } 1646 1647 func (e *importError) Unwrap() error { 1648 // Don't return e.err directly, since we're only wrapping an error if %w 1649 // was passed to ImportErrorf. 1650 return errors.Unwrap(e.err) 1651 } 1652 1653 func (e *importError) ImportPath() string { 1654 return e.importPath 1655 }