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