vcs.go 22 KB

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