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