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