github.com/jd-ly/tools@v0.5.7/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 // 15 package vcs // import "github.com/jd-ly/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 // pathPrefix reports whether sub is a prefix of s, 607 // only considering entire path components. 608 func pathPrefix(s, sub string) bool { 609 // strings.HasPrefix is necessary but not sufficient. 610 if !strings.HasPrefix(s, sub) { 611 return false 612 } 613 // The remainder after the prefix must either be empty or start with a slash. 614 rem := s[len(sub):] 615 return rem == "" || rem[0] == '/' 616 } 617 618 // matchGoImport returns the metaImport from imports matching importPath. 619 // An error is returned if there are multiple matches. 620 // errNoMatch is returned if none match. 621 func matchGoImport(imports []metaImport, importPath string) (_ metaImport, err error) { 622 match := -1 623 for i, im := range imports { 624 if !pathPrefix(importPath, im.Prefix) { 625 continue 626 } 627 628 if match != -1 { 629 err = fmt.Errorf("multiple meta tags match import path %q", importPath) 630 return 631 } 632 match = i 633 } 634 if match == -1 { 635 err = errNoMatch 636 return 637 } 638 return imports[match], nil 639 } 640 641 // expand rewrites s to replace {k} with match[k] for each key k in match. 642 func expand(match map[string]string, s string) string { 643 for k, v := range match { 644 s = strings.Replace(s, "{"+k+"}", v, -1) 645 } 646 return s 647 } 648 649 // vcsPaths lists the known vcs paths. 650 var vcsPaths = []*vcsPath{ 651 // Github 652 { 653 prefix: "github.com/", 654 re: `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[\p{L}0-9_.\-]+)*$`, 655 vcs: "git", 656 repo: "https://{root}", 657 check: noVCSSuffix, 658 }, 659 660 // Bitbucket 661 { 662 prefix: "bitbucket.org/", 663 re: `^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`, 664 repo: "https://{root}", 665 check: bitbucketVCS, 666 }, 667 668 // Launchpad 669 { 670 prefix: "launchpad.net/", 671 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_.\-]+)*$`, 672 vcs: "bzr", 673 repo: "https://{root}", 674 check: launchpadVCS, 675 }, 676 677 // Git at OpenStack 678 { 679 prefix: "git.openstack.org", 680 re: `^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/[A-Za-z0-9_.\-]+)*$`, 681 vcs: "git", 682 repo: "https://{root}", 683 check: noVCSSuffix, 684 }, 685 686 // General syntax for any server. 687 { 688 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_.\-]+)*$`, 689 ping: true, 690 }, 691 } 692 693 func init() { 694 // fill in cached regexps. 695 // Doing this eagerly discovers invalid regexp syntax 696 // without having to run a command that needs that regexp. 697 for _, srv := range vcsPaths { 698 srv.regexp = regexp.MustCompile(srv.re) 699 } 700 } 701 702 // noVCSSuffix checks that the repository name does not 703 // end in .foo for any version control system foo. 704 // The usual culprit is ".git". 705 func noVCSSuffix(match map[string]string) error { 706 repo := match["repo"] 707 for _, vcs := range vcsList { 708 if strings.HasSuffix(repo, "."+vcs.Cmd) { 709 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"]) 710 } 711 } 712 return nil 713 } 714 715 // bitbucketVCS determines the version control system for a 716 // Bitbucket repository, by using the Bitbucket API. 717 func bitbucketVCS(match map[string]string) error { 718 if err := noVCSSuffix(match); err != nil { 719 return err 720 } 721 722 var resp struct { 723 SCM string `json:"scm"` 724 } 725 url := expand(match, "https://api.bitbucket.org/2.0/repositories/{bitname}?fields=scm") 726 data, err := httpGET(url) 727 if err != nil { 728 return err 729 } 730 if err := json.Unmarshal(data, &resp); err != nil { 731 return fmt.Errorf("decoding %s: %v", url, err) 732 } 733 734 if ByCmd(resp.SCM) != nil { 735 match["vcs"] = resp.SCM 736 if resp.SCM == "git" { 737 match["repo"] += ".git" 738 } 739 return nil 740 } 741 742 return fmt.Errorf("unable to detect version control system for bitbucket.org/ path") 743 } 744 745 // launchpadVCS solves the ambiguity for "lp.net/project/foo". In this case, 746 // "foo" could be a series name registered in Launchpad with its own branch, 747 // and it could also be the name of a directory within the main project 748 // branch one level up. 749 func launchpadVCS(match map[string]string) error { 750 if match["project"] == "" || match["series"] == "" { 751 return nil 752 } 753 _, err := httpGET(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format")) 754 if err != nil { 755 match["root"] = expand(match, "launchpad.net/{project}") 756 match["repo"] = expand(match, "https://{root}") 757 } 758 return nil 759 }