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