You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

731 lines
21 KiB

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