github.com/jgarto/itcv@v0.0.0-20180826224514-4eea09c1aa0d/_vendor/src/golang.org/x/tools/go/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 exposes functions for resolving import paths 6 // and using version control systems, which can be used to 7 // implement behavior similar to the standard "go get" command. 8 // 9 // This package is a copy of internal code in package cmd/go/internal/get, 10 // modified to make the identifiers exported. It's provided here 11 // for developers who want to write tools with similar semantics. 12 // It needs to be manually kept in sync with upstream when changes are 13 // made to cmd/go/internal/get; see https://golang.org/issues/11490. 14 // 15 package vcs // import "golang.org/x/tools/go/vcs" 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "log" 23 "net/url" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "regexp" 28 "strconv" 29 "strings" 30 ) 31 32 // Verbose enables verbose operation logging. 33 var Verbose bool 34 35 // ShowCmd controls whether VCS commands are printed. 36 var ShowCmd bool 37 38 // A Cmd describes how to use a version control system 39 // like Mercurial, Git, or Subversion. 40 type Cmd struct { 41 Name string 42 Cmd string // name of binary to invoke command 43 44 CreateCmd string // command to download a fresh copy of a repository 45 DownloadCmd string // command to download updates into an existing repository 46 47 TagCmd []TagCmd // commands to list tags 48 TagLookupCmd []TagCmd // commands to lookup tags before running tagSyncCmd 49 TagSyncCmd string // command to sync to specific tag 50 TagSyncDefault string // command to sync to default tag 51 52 LogCmd string // command to list repository changelogs in an XML format 53 54 Scheme []string 55 PingCmd string 56 } 57 58 // A TagCmd describes a command to list available tags 59 // that can be passed to Cmd.TagSyncCmd. 60 type TagCmd struct { 61 Cmd string // command to list tags 62 Pattern string // regexp to extract tags from list 63 } 64 65 // vcsList lists the known version control systems 66 var vcsList = []*Cmd{ 67 vcsHg, 68 vcsGit, 69 vcsSvn, 70 vcsBzr, 71 } 72 73 // ByCmd returns the version control system for the given 74 // command name (hg, git, svn, bzr). 75 func ByCmd(cmd string) *Cmd { 76 for _, vcs := range vcsList { 77 if vcs.Cmd == cmd { 78 return vcs 79 } 80 } 81 return nil 82 } 83 84 // vcsHg describes how to use Mercurial. 85 var vcsHg = &Cmd{ 86 Name: "Mercurial", 87 Cmd: "hg", 88 89 CreateCmd: "clone -U {repo} {dir}", 90 DownloadCmd: "pull", 91 92 // We allow both tag and branch names as 'tags' 93 // for selecting a version. This lets people have 94 // a go.release.r60 branch and a go1 branch 95 // and make changes in both, without constantly 96 // editing .hgtags. 97 TagCmd: []TagCmd{ 98 {"tags", `^(\S+)`}, 99 {"branches", `^(\S+)`}, 100 }, 101 TagSyncCmd: "update -r {tag}", 102 TagSyncDefault: "update default", 103 104 LogCmd: "log --encoding=utf-8 --limit={limit} --template={template}", 105 106 Scheme: []string{"https", "http", "ssh"}, 107 PingCmd: "identify {scheme}://{repo}", 108 } 109 110 // vcsGit describes how to use Git. 111 var vcsGit = &Cmd{ 112 Name: "Git", 113 Cmd: "git", 114 115 CreateCmd: "clone {repo} {dir}", 116 DownloadCmd: "pull --ff-only", 117 118 TagCmd: []TagCmd{ 119 // tags/xxx matches a git tag named xxx 120 // origin/xxx matches a git branch named xxx on the default remote repository 121 {"show-ref", `(?:tags|origin)/(\S+)$`}, 122 }, 123 TagLookupCmd: []TagCmd{ 124 {"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`}, 125 }, 126 TagSyncCmd: "checkout {tag}", 127 TagSyncDefault: "checkout master", 128 129 Scheme: []string{"git", "https", "http", "git+ssh"}, 130 PingCmd: "ls-remote {scheme}://{repo}", 131 } 132 133 // vcsBzr describes how to use Bazaar. 134 var vcsBzr = &Cmd{ 135 Name: "Bazaar", 136 Cmd: "bzr", 137 138 CreateCmd: "branch {repo} {dir}", 139 140 // Without --overwrite bzr will not pull tags that changed. 141 // Replace by --overwrite-tags after http://pad.lv/681792 goes in. 142 DownloadCmd: "pull --overwrite", 143 144 TagCmd: []TagCmd{{"tags", `^(\S+)`}}, 145 TagSyncCmd: "update -r {tag}", 146 TagSyncDefault: "update -r revno:-1", 147 148 Scheme: []string{"https", "http", "bzr", "bzr+ssh"}, 149 PingCmd: "info {scheme}://{repo}", 150 } 151 152 // vcsSvn describes how to use Subversion. 153 var vcsSvn = &Cmd{ 154 Name: "Subversion", 155 Cmd: "svn", 156 157 CreateCmd: "checkout {repo} {dir}", 158 DownloadCmd: "update", 159 160 // There is no tag command in subversion. 161 // The branch information is all in the path names. 162 163 LogCmd: "log --xml --limit={limit}", 164 165 Scheme: []string{"https", "http", "svn", "svn+ssh"}, 166 PingCmd: "info {scheme}://{repo}", 167 } 168 169 func (v *Cmd) String() string { 170 return v.Name 171 } 172 173 // run runs the command line cmd in the given directory. 174 // keyval is a list of key, value pairs. run expands 175 // instances of {key} in cmd into value, but only after 176 // splitting cmd into individual arguments. 177 // If an error occurs, run prints the command line and the 178 // command's combined stdout+stderr to standard error. 179 // Otherwise run discards the command's output. 180 func (v *Cmd) run(dir string, cmd string, keyval ...string) error { 181 _, err := v.run1(dir, cmd, keyval, true) 182 return err 183 } 184 185 // runVerboseOnly is like run but only generates error output to standard error in verbose mode. 186 func (v *Cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error { 187 _, err := v.run1(dir, cmd, keyval, false) 188 return err 189 } 190 191 // runOutput is like run but returns the output of the command. 192 func (v *Cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) { 193 return v.run1(dir, cmd, keyval, true) 194 } 195 196 // run1 is the generalized implementation of run and runOutput. 197 func (v *Cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) { 198 m := make(map[string]string) 199 for i := 0; i < len(keyval); i += 2 { 200 m[keyval[i]] = keyval[i+1] 201 } 202 args := strings.Fields(cmdline) 203 for i, arg := range args { 204 args[i] = expand(m, arg) 205 } 206 207 _, err := exec.LookPath(v.Cmd) 208 if err != nil { 209 fmt.Fprintf(os.Stderr, 210 "go: missing %s command. See http://golang.org/s/gogetcmd\n", 211 v.Name) 212 return nil, err 213 } 214 215 cmd := exec.Command(v.Cmd, args...) 216 cmd.Dir = dir 217 cmd.Env = envForDir(cmd.Dir) 218 if ShowCmd { 219 fmt.Printf("cd %s\n", dir) 220 fmt.Printf("%s %s\n", v.Cmd, strings.Join(args, " ")) 221 } 222 var buf bytes.Buffer 223 cmd.Stdout = &buf 224 cmd.Stderr = &buf 225 err = cmd.Run() 226 out := buf.Bytes() 227 if err != nil { 228 if verbose || Verbose { 229 fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.Cmd, strings.Join(args, " ")) 230 os.Stderr.Write(out) 231 } 232 return nil, err 233 } 234 return out, nil 235 } 236 237 // Ping pings the repo to determine if scheme used is valid. 238 // This repo must be pingable with this scheme and VCS. 239 func (v *Cmd) Ping(scheme, repo string) error { 240 return v.runVerboseOnly(".", v.PingCmd, "scheme", scheme, "repo", repo) 241 } 242 243 // Create creates a new copy of repo in dir. 244 // The parent of dir must exist; dir must not. 245 func (v *Cmd) Create(dir, repo string) error { 246 return v.run(".", v.CreateCmd, "dir", dir, "repo", repo) 247 } 248 249 // CreateAtRev creates a new copy of repo in dir at revision rev. 250 // The parent of dir must exist; dir must not. 251 // rev must be a valid revision in repo. 252 func (v *Cmd) CreateAtRev(dir, repo, rev string) error { 253 if err := v.Create(dir, repo); err != nil { 254 return err 255 } 256 return v.run(dir, v.TagSyncCmd, "tag", rev) 257 } 258 259 // Download downloads any new changes for the repo in dir. 260 // dir must be a valid VCS repo compatible with v. 261 func (v *Cmd) Download(dir string) error { 262 return v.run(dir, v.DownloadCmd) 263 } 264 265 // Tags returns the list of available tags for the repo in dir. 266 // dir must be a valid VCS repo compatible with v. 267 func (v *Cmd) Tags(dir string) ([]string, error) { 268 var tags []string 269 for _, tc := range v.TagCmd { 270 out, err := v.runOutput(dir, tc.Cmd) 271 if err != nil { 272 return nil, err 273 } 274 re := regexp.MustCompile(`(?m-s)` + tc.Pattern) 275 for _, m := range re.FindAllStringSubmatch(string(out), -1) { 276 tags = append(tags, m[1]) 277 } 278 } 279 return tags, nil 280 } 281 282 // TagSync syncs the repo in dir to the named tag, which is either a 283 // tag returned by Tags or the empty string (the default tag). 284 // dir must be a valid VCS repo compatible with v and the tag must exist. 285 func (v *Cmd) TagSync(dir, tag string) error { 286 if v.TagSyncCmd == "" { 287 return nil 288 } 289 if tag != "" { 290 for _, tc := range v.TagLookupCmd { 291 out, err := v.runOutput(dir, tc.Cmd, "tag", tag) 292 if err != nil { 293 return err 294 } 295 re := regexp.MustCompile(`(?m-s)` + tc.Pattern) 296 m := re.FindStringSubmatch(string(out)) 297 if len(m) > 1 { 298 tag = m[1] 299 break 300 } 301 } 302 } 303 if tag == "" && v.TagSyncDefault != "" { 304 return v.run(dir, v.TagSyncDefault) 305 } 306 return v.run(dir, v.TagSyncCmd, "tag", tag) 307 } 308 309 // Log logs the changes for the repo in dir. 310 // dir must be a valid VCS repo compatible with v. 311 func (v *Cmd) Log(dir, logTemplate string) ([]byte, error) { 312 if err := v.Download(dir); err != nil { 313 return []byte{}, err 314 } 315 316 const N = 50 // how many revisions to grab 317 return v.runOutput(dir, v.LogCmd, "limit", strconv.Itoa(N), "template", logTemplate) 318 } 319 320 // LogAtRev logs the change for repo in dir at the rev revision. 321 // dir must be a valid VCS repo compatible with v. 322 // rev must be a valid revision for the repo in dir. 323 func (v *Cmd) LogAtRev(dir, rev, logTemplate string) ([]byte, error) { 324 if err := v.Download(dir); err != nil { 325 return []byte{}, err 326 } 327 328 // Append revision flag to LogCmd. 329 logAtRevCmd := v.LogCmd + " --rev=" + rev 330 return v.runOutput(dir, logAtRevCmd, "limit", strconv.Itoa(1), "template", logTemplate) 331 } 332 333 // A vcsPath describes how to convert an import path into a 334 // version control system and repository name. 335 type vcsPath struct { 336 prefix string // prefix this description applies to 337 re string // pattern for import path 338 repo string // repository to use (expand with match of re) 339 vcs string // version control system to use (expand with match of re) 340 check func(match map[string]string) error // additional checks 341 ping bool // ping for scheme to use to download repo 342 343 regexp *regexp.Regexp // cached compiled form of re 344 } 345 346 // FromDir inspects dir and its parents to determine the 347 // version control system and code repository to use. 348 // On return, root is the import path 349 // corresponding to the root of the repository. 350 func FromDir(dir, srcRoot string) (vcs *Cmd, root string, err error) { 351 // Clean and double-check that dir is in (a subdirectory of) srcRoot. 352 dir = filepath.Clean(dir) 353 srcRoot = filepath.Clean(srcRoot) 354 if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator { 355 return nil, "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot) 356 } 357 358 var vcsRet *Cmd 359 var rootRet string 360 361 origDir := dir 362 for len(dir) > len(srcRoot) { 363 for _, vcs := range vcsList { 364 if _, err := os.Stat(filepath.Join(dir, "."+vcs.Cmd)); err == nil { 365 root := filepath.ToSlash(dir[len(srcRoot)+1:]) 366 // Record first VCS we find, but keep looking, 367 // to detect mistakes like one kind of VCS inside another. 368 if vcsRet == nil { 369 vcsRet = vcs 370 rootRet = root 371 continue 372 } 373 // Allow .git inside .git, which can arise due to submodules. 374 if vcsRet == vcs && vcs.Cmd == "git" { 375 continue 376 } 377 // Otherwise, we have one VCS inside a different VCS. 378 return nil, "", fmt.Errorf("directory %q uses %s, but parent %q uses %s", 379 filepath.Join(srcRoot, rootRet), vcsRet.Cmd, filepath.Join(srcRoot, root), vcs.Cmd) 380 } 381 } 382 383 // Move to parent. 384 ndir := filepath.Dir(dir) 385 if len(ndir) >= len(dir) { 386 // Shouldn't happen, but just in case, stop. 387 break 388 } 389 dir = ndir 390 } 391 392 if vcsRet != nil { 393 return vcsRet, rootRet, nil 394 } 395 396 return nil, "", fmt.Errorf("directory %q is not using a known version control system", origDir) 397 } 398 399 // RepoRoot represents a version control system, a repo, and a root of 400 // where to put it on disk. 401 type RepoRoot struct { 402 VCS *Cmd 403 404 // Repo is the repository URL, including scheme. 405 Repo string 406 407 // Root is the import path corresponding to the root of the 408 // repository. 409 Root string 410 } 411 412 // RepoRootForImportPath analyzes importPath to determine the 413 // version control system, and code repository to use. 414 func RepoRootForImportPath(importPath string, verbose bool) (*RepoRoot, error) { 415 rr, err := RepoRootForImportPathStatic(importPath, "") 416 if err == errUnknownSite { 417 rr, err = RepoRootForImportDynamic(importPath, verbose) 418 419 // RepoRootForImportDynamic returns error detail 420 // that is irrelevant if the user didn't intend to use a 421 // dynamic import in the first place. 422 // Squelch it. 423 if err != nil { 424 if Verbose { 425 log.Printf("import %q: %v", importPath, err) 426 } 427 err = fmt.Errorf("unrecognized import path %q", importPath) 428 } 429 } 430 431 if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") { 432 // Do not allow wildcards in the repo root. 433 rr = nil 434 err = fmt.Errorf("cannot expand ... in %q", importPath) 435 } 436 return rr, err 437 } 438 439 var errUnknownSite = errors.New("dynamic lookup required to find mapping") 440 441 // RepoRootForImportPathStatic attempts to map importPath to a 442 // RepoRoot using the commonly-used VCS hosting sites in vcsPaths 443 // (github.com/user/dir), or from a fully-qualified importPath already 444 // containing its VCS type (foo.com/repo.git/dir) 445 // 446 // If scheme is non-empty, that scheme is forced. 447 func RepoRootForImportPathStatic(importPath, scheme string) (*RepoRoot, error) { 448 if strings.Contains(importPath, "://") { 449 return nil, fmt.Errorf("invalid import path %q", importPath) 450 } 451 for _, srv := range vcsPaths { 452 if !strings.HasPrefix(importPath, srv.prefix) { 453 continue 454 } 455 m := srv.regexp.FindStringSubmatch(importPath) 456 if m == nil { 457 if srv.prefix != "" { 458 return nil, fmt.Errorf("invalid %s import path %q", srv.prefix, importPath) 459 } 460 continue 461 } 462 463 // Build map of named subexpression matches for expand. 464 match := map[string]string{ 465 "prefix": srv.prefix, 466 "import": importPath, 467 } 468 for i, name := range srv.regexp.SubexpNames() { 469 if name != "" && match[name] == "" { 470 match[name] = m[i] 471 } 472 } 473 if srv.vcs != "" { 474 match["vcs"] = expand(match, srv.vcs) 475 } 476 if srv.repo != "" { 477 match["repo"] = expand(match, srv.repo) 478 } 479 if srv.check != nil { 480 if err := srv.check(match); err != nil { 481 return nil, err 482 } 483 } 484 vcs := ByCmd(match["vcs"]) 485 if vcs == nil { 486 return nil, fmt.Errorf("unknown version control system %q", match["vcs"]) 487 } 488 if srv.ping { 489 if scheme != "" { 490 match["repo"] = scheme + "://" + match["repo"] 491 } else { 492 for _, scheme := range vcs.Scheme { 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, verbose bool) (*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 doesn't contain a hostname") 522 } 523 urlStr, body, err := httpsOrHTTP(importPath) 524 if err != nil { 525 return nil, fmt.Errorf("http/https fetch: %v", err) 526 } 527 defer body.Close() 528 imports, err := parseMetaGoImports(body) 529 if err != nil { 530 return nil, fmt.Errorf("parsing %s: %v", importPath, err) 531 } 532 metaImport, err := matchGoImport(imports, importPath) 533 if err != nil { 534 if err != errNoMatch { 535 return nil, fmt.Errorf("parse %s: %v", urlStr, err) 536 } 537 return nil, fmt.Errorf("parse %s: no go-import meta tags", urlStr) 538 } 539 if verbose { 540 log.Printf("get %q: found meta tag %#v at %s", importPath, metaImport, urlStr) 541 } 542 // If the import was "uni.edu/bob/project", which said the 543 // prefix was "uni.edu" and the RepoRoot was "evilroot.com", 544 // make sure we don't trust Bob and check out evilroot.com to 545 // "uni.edu" yet (possibly overwriting/preempting another 546 // non-evil student). Instead, first verify the root and see 547 // if it matches Bob's claim. 548 if metaImport.Prefix != importPath { 549 if verbose { 550 log.Printf("get %q: verifying non-authoritative meta tag", importPath) 551 } 552 urlStr0 := urlStr 553 urlStr, body, err = httpsOrHTTP(metaImport.Prefix) 554 if err != nil { 555 return nil, fmt.Errorf("fetch %s: %v", urlStr, err) 556 } 557 imports, err := parseMetaGoImports(body) 558 if err != nil { 559 return nil, fmt.Errorf("parsing %s: %v", importPath, err) 560 } 561 if len(imports) == 0 { 562 return nil, fmt.Errorf("fetch %s: no go-import meta tag", urlStr) 563 } 564 metaImport2, err := matchGoImport(imports, importPath) 565 if err != nil || metaImport != metaImport2 { 566 return nil, fmt.Errorf("%s and %s disagree about go-import for %s", urlStr0, urlStr, metaImport.Prefix) 567 } 568 } 569 570 if err := validateRepoRoot(metaImport.RepoRoot); err != nil { 571 return nil, fmt.Errorf("%s: invalid repo root %q: %v", urlStr, metaImport.RepoRoot, err) 572 } 573 rr := &RepoRoot{ 574 VCS: ByCmd(metaImport.VCS), 575 Repo: metaImport.RepoRoot, 576 Root: metaImport.Prefix, 577 } 578 if rr.VCS == nil { 579 return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, metaImport.VCS) 580 } 581 return rr, nil 582 } 583 584 // validateRepoRoot returns an error if repoRoot does not seem to be 585 // a valid URL with scheme. 586 func validateRepoRoot(repoRoot string) error { 587 url, err := url.Parse(repoRoot) 588 if err != nil { 589 return err 590 } 591 if url.Scheme == "" { 592 return errors.New("no scheme") 593 } 594 return nil 595 } 596 597 // metaImport represents the parsed <meta name="go-import" 598 // content="prefix vcs reporoot" /> tags from HTML files. 599 type metaImport struct { 600 Prefix, VCS, RepoRoot string 601 } 602 603 // errNoMatch is returned from matchGoImport when there's no applicable match. 604 var errNoMatch = errors.New("no import match") 605 606 // matchGoImport returns the metaImport from imports matching importPath. 607 // An error is returned if there are multiple matches. 608 // errNoMatch is returned if none match. 609 func matchGoImport(imports []metaImport, importPath string) (_ metaImport, err error) { 610 match := -1 611 for i, im := range imports { 612 if !strings.HasPrefix(importPath, im.Prefix) { 613 continue 614 } 615 if match != -1 { 616 err = fmt.Errorf("multiple meta tags match import path %q", importPath) 617 return 618 } 619 match = i 620 } 621 if match == -1 { 622 err = errNoMatch 623 return 624 } 625 return imports[match], nil 626 } 627 628 // expand rewrites s to replace {k} with match[k] for each key k in match. 629 func expand(match map[string]string, s string) string { 630 for k, v := range match { 631 s = strings.Replace(s, "{"+k+"}", v, -1) 632 } 633 return s 634 } 635 636 // vcsPaths lists the known vcs paths. 637 var vcsPaths = []*vcsPath{ 638 // go.googlesource.com 639 { 640 prefix: "go.googlesource.com", 641 re: `^(?P<root>go\.googlesource\.com/[A-Za-z0-9_.\-]+/?)$`, 642 vcs: "git", 643 repo: "https://{root}", 644 check: noVCSSuffix, 645 }, 646 647 // Github 648 { 649 prefix: "github.com/", 650 re: `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[\p{L}0-9_.\-]+)*$`, 651 vcs: "git", 652 repo: "https://{root}", 653 check: noVCSSuffix, 654 }, 655 656 // Bitbucket 657 { 658 prefix: "bitbucket.org/", 659 re: `^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`, 660 repo: "https://{root}", 661 check: bitbucketVCS, 662 }, 663 664 // Launchpad 665 { 666 prefix: "launchpad.net/", 667 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_.\-]+)*$`, 668 vcs: "bzr", 669 repo: "https://{root}", 670 check: launchpadVCS, 671 }, 672 673 // Git at OpenStack 674 { 675 prefix: "git.openstack.org", 676 re: `^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/[A-Za-z0-9_.\-]+)*$`, 677 vcs: "git", 678 repo: "https://{root}", 679 check: noVCSSuffix, 680 }, 681 682 // General syntax for any server. 683 { 684 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_.\-]+)*$`, 685 ping: true, 686 }, 687 } 688 689 func init() { 690 // fill in cached regexps. 691 // Doing this eagerly discovers invalid regexp syntax 692 // without having to run a command that needs that regexp. 693 for _, srv := range vcsPaths { 694 srv.regexp = regexp.MustCompile(srv.re) 695 } 696 } 697 698 // noVCSSuffix checks that the repository name does not 699 // end in .foo for any version control system foo. 700 // The usual culprit is ".git". 701 func noVCSSuffix(match map[string]string) error { 702 repo := match["repo"] 703 for _, vcs := range vcsList { 704 if strings.HasSuffix(repo, "."+vcs.Cmd) { 705 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"]) 706 } 707 } 708 return nil 709 } 710 711 // bitbucketVCS determines the version control system for a 712 // Bitbucket repository, by using the Bitbucket API. 713 func bitbucketVCS(match map[string]string) error { 714 if err := noVCSSuffix(match); err != nil { 715 return err 716 } 717 718 var resp struct { 719 SCM string `json:"scm"` 720 } 721 url := expand(match, "https://api.bitbucket.org/1.0/repositories/{bitname}") 722 data, err := httpGET(url) 723 if err != nil { 724 return err 725 } 726 if err := json.Unmarshal(data, &resp); err != nil { 727 return fmt.Errorf("decoding %s: %v", url, err) 728 } 729 730 if ByCmd(resp.SCM) != nil { 731 match["vcs"] = resp.SCM 732 if resp.SCM == "git" { 733 match["repo"] += ".git" 734 } 735 return nil 736 } 737 738 return fmt.Errorf("unable to detect version control system for bitbucket.org/ path") 739 } 740 741 // launchpadVCS solves the ambiguity for "lp.net/project/foo". In this case, 742 // "foo" could be a series name registered in Launchpad with its own branch, 743 // and it could also be the name of a directory within the main project 744 // branch one level up. 745 func launchpadVCS(match map[string]string) error { 746 if match["project"] == "" || match["series"] == "" { 747 return nil 748 } 749 _, err := httpGET(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format")) 750 if err != nil { 751 match["root"] = expand(match, "launchpad.net/{project}") 752 match["repo"] = expand(match, "https://{root}") 753 } 754 return nil 755 }