1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package vcs
21
22 import (
23 "bytes"
24 "encoding/json"
25 "errors"
26 "fmt"
27 exec "golang.org/x/sys/execabs"
28 "log"
29 "net/url"
30 "os"
31 "path/filepath"
32 "regexp"
33 "strconv"
34 "strings"
35 )
36
37
38 var Verbose bool
39
40
41 var ShowCmd bool
42
43
44
45 type Cmd struct {
46 Name string
47 Cmd string
48
49 CreateCmd string
50 DownloadCmd string
51
52 TagCmd []TagCmd
53 TagLookupCmd []TagCmd
54 TagSyncCmd string
55 TagSyncDefault string
56
57 LogCmd string
58
59 Scheme []string
60 PingCmd string
61 }
62
63
64
65 type TagCmd struct {
66 Cmd string
67 Pattern string
68 }
69
70
71 var vcsList = []*Cmd{
72 vcsHg,
73 vcsGit,
74 vcsSvn,
75 vcsBzr,
76 }
77
78
79
80 func ByCmd(cmd string) *Cmd {
81 for _, vcs := range vcsList {
82 if vcs.Cmd == cmd {
83 return vcs
84 }
85 }
86 return nil
87 }
88
89
90 var vcsHg = &Cmd{
91 Name: "Mercurial",
92 Cmd: "hg",
93
94 CreateCmd: "clone -U {repo} {dir}",
95 DownloadCmd: "pull",
96
97
98
99
100
101
102 TagCmd: []TagCmd{
103 {"tags", `^(\S+)`},
104 {"branches", `^(\S+)`},
105 },
106 TagSyncCmd: "update -r {tag}",
107 TagSyncDefault: "update default",
108
109 LogCmd: "log --encoding=utf-8 --limit={limit} --template={template}",
110
111 Scheme: []string{"https", "http", "ssh"},
112 PingCmd: "identify {scheme}://{repo}",
113 }
114
115
116 var vcsGit = &Cmd{
117 Name: "Git",
118 Cmd: "git",
119
120 CreateCmd: "clone {repo} {dir}",
121 DownloadCmd: "pull --ff-only",
122
123 TagCmd: []TagCmd{
124
125
126 {"show-ref", `(?:tags|origin)/(\S+)$`},
127 },
128 TagLookupCmd: []TagCmd{
129 {"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
130 },
131 TagSyncCmd: "checkout {tag}",
132 TagSyncDefault: "checkout master",
133
134 Scheme: []string{"git", "https", "http", "git+ssh"},
135 PingCmd: "ls-remote {scheme}://{repo}",
136 }
137
138
139 var vcsBzr = &Cmd{
140 Name: "Bazaar",
141 Cmd: "bzr",
142
143 CreateCmd: "branch {repo} {dir}",
144
145
146
147 DownloadCmd: "pull --overwrite",
148
149 TagCmd: []TagCmd{{"tags", `^(\S+)`}},
150 TagSyncCmd: "update -r {tag}",
151 TagSyncDefault: "update -r revno:-1",
152
153 Scheme: []string{"https", "http", "bzr", "bzr+ssh"},
154 PingCmd: "info {scheme}://{repo}",
155 }
156
157
158 var vcsSvn = &Cmd{
159 Name: "Subversion",
160 Cmd: "svn",
161
162 CreateCmd: "checkout {repo} {dir}",
163 DownloadCmd: "update",
164
165
166
167
168 LogCmd: "log --xml --limit={limit}",
169
170 Scheme: []string{"https", "http", "svn", "svn+ssh"},
171 PingCmd: "info {scheme}://{repo}",
172 }
173
174 func (v *Cmd) String() string {
175 return v.Name
176 }
177
178
179
180
181
182
183
184
185 func (v *Cmd) run(dir string, cmd string, keyval ...string) error {
186 _, err := v.run1(dir, cmd, keyval, true)
187 return err
188 }
189
190
191 func (v *Cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error {
192 _, err := v.run1(dir, cmd, keyval, false)
193 return err
194 }
195
196
197 func (v *Cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) {
198 return v.run1(dir, cmd, keyval, true)
199 }
200
201
202 func (v *Cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) {
203 m := make(map[string]string)
204 for i := 0; i < len(keyval); i += 2 {
205 m[keyval[i]] = keyval[i+1]
206 }
207 args := strings.Fields(cmdline)
208 for i, arg := range args {
209 args[i] = expand(m, arg)
210 }
211
212 _, err := exec.LookPath(v.Cmd)
213 if err != nil {
214 fmt.Fprintf(os.Stderr,
215 "go: missing %s command. See http://golang.org/s/gogetcmd\n",
216 v.Name)
217 return nil, err
218 }
219
220 cmd := exec.Command(v.Cmd, args...)
221 cmd.Dir = dir
222 cmd.Env = envForDir(cmd.Dir)
223 if ShowCmd {
224 fmt.Printf("cd %s\n", dir)
225 fmt.Printf("%s %s\n", v.Cmd, strings.Join(args, " "))
226 }
227 var buf bytes.Buffer
228 cmd.Stdout = &buf
229 cmd.Stderr = &buf
230 err = cmd.Run()
231 out := buf.Bytes()
232 if err != nil {
233 if verbose || Verbose {
234 fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.Cmd, strings.Join(args, " "))
235 os.Stderr.Write(out)
236 }
237 return nil, err
238 }
239 return out, nil
240 }
241
242
243
244 func (v *Cmd) Ping(scheme, repo string) error {
245 return v.runVerboseOnly(".", v.PingCmd, "scheme", scheme, "repo", repo)
246 }
247
248
249
250 func (v *Cmd) Create(dir, repo string) error {
251 return v.run(".", v.CreateCmd, "dir", dir, "repo", repo)
252 }
253
254
255
256
257 func (v *Cmd) CreateAtRev(dir, repo, rev string) error {
258 if err := v.Create(dir, repo); err != nil {
259 return err
260 }
261 return v.run(dir, v.TagSyncCmd, "tag", rev)
262 }
263
264
265
266 func (v *Cmd) Download(dir string) error {
267 return v.run(dir, v.DownloadCmd)
268 }
269
270
271
272 func (v *Cmd) Tags(dir string) ([]string, error) {
273 var tags []string
274 for _, tc := range v.TagCmd {
275 out, err := v.runOutput(dir, tc.Cmd)
276 if err != nil {
277 return nil, err
278 }
279 re := regexp.MustCompile(`(?m-s)` + tc.Pattern)
280 for _, m := range re.FindAllStringSubmatch(string(out), -1) {
281 tags = append(tags, m[1])
282 }
283 }
284 return tags, nil
285 }
286
287
288
289
290 func (v *Cmd) TagSync(dir, tag string) error {
291 if v.TagSyncCmd == "" {
292 return nil
293 }
294 if tag != "" {
295 for _, tc := range v.TagLookupCmd {
296 out, err := v.runOutput(dir, tc.Cmd, "tag", tag)
297 if err != nil {
298 return err
299 }
300 re := regexp.MustCompile(`(?m-s)` + tc.Pattern)
301 m := re.FindStringSubmatch(string(out))
302 if len(m) > 1 {
303 tag = m[1]
304 break
305 }
306 }
307 }
308 if tag == "" && v.TagSyncDefault != "" {
309 return v.run(dir, v.TagSyncDefault)
310 }
311 return v.run(dir, v.TagSyncCmd, "tag", tag)
312 }
313
314
315
316 func (v *Cmd) Log(dir, logTemplate string) ([]byte, error) {
317 if err := v.Download(dir); err != nil {
318 return []byte{}, err
319 }
320
321 const N = 50
322 return v.runOutput(dir, v.LogCmd, "limit", strconv.Itoa(N), "template", logTemplate)
323 }
324
325
326
327
328 func (v *Cmd) LogAtRev(dir, rev, logTemplate string) ([]byte, error) {
329 if err := v.Download(dir); err != nil {
330 return []byte{}, err
331 }
332
333
334 logAtRevCmd := v.LogCmd + " --rev=" + rev
335 return v.runOutput(dir, logAtRevCmd, "limit", strconv.Itoa(1), "template", logTemplate)
336 }
337
338
339
340 type vcsPath struct {
341 prefix string
342 re string
343 repo string
344 vcs string
345 check func(match map[string]string) error
346 ping bool
347
348 regexp *regexp.Regexp
349 }
350
351
352
353
354
355 func FromDir(dir, srcRoot string) (vcs *Cmd, root string, err error) {
356
357 dir = filepath.Clean(dir)
358 srcRoot = filepath.Clean(srcRoot)
359 if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {
360 return nil, "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot)
361 }
362
363 var vcsRet *Cmd
364 var rootRet string
365
366 origDir := dir
367 for len(dir) > len(srcRoot) {
368 for _, vcs := range vcsList {
369 if _, err := os.Stat(filepath.Join(dir, "."+vcs.Cmd)); err == nil {
370 root := filepath.ToSlash(dir[len(srcRoot)+1:])
371
372
373 if vcsRet == nil {
374 vcsRet = vcs
375 rootRet = root
376 continue
377 }
378
379 if vcsRet == vcs && vcs.Cmd == "git" {
380 continue
381 }
382
383 return nil, "", fmt.Errorf("directory %q uses %s, but parent %q uses %s",
384 filepath.Join(srcRoot, rootRet), vcsRet.Cmd, filepath.Join(srcRoot, root), vcs.Cmd)
385 }
386 }
387
388
389 ndir := filepath.Dir(dir)
390 if len(ndir) >= len(dir) {
391
392 break
393 }
394 dir = ndir
395 }
396
397 if vcsRet != nil {
398 return vcsRet, rootRet, nil
399 }
400
401 return nil, "", fmt.Errorf("directory %q is not using a known version control system", origDir)
402 }
403
404
405
406 type RepoRoot struct {
407 VCS *Cmd
408
409
410 Repo string
411
412
413
414 Root string
415 }
416
417
418
419 func RepoRootForImportPath(importPath string, verbose bool) (*RepoRoot, error) {
420 rr, err := RepoRootForImportPathStatic(importPath, "")
421 if err == errUnknownSite {
422 rr, err = RepoRootForImportDynamic(importPath, verbose)
423
424
425
426
427
428 if err != nil {
429 if Verbose {
430 log.Printf("import %q: %v", importPath, err)
431 }
432 err = fmt.Errorf("unrecognized import path %q", importPath)
433 }
434 }
435
436 if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") {
437
438 rr = nil
439 err = fmt.Errorf("cannot expand ... in %q", importPath)
440 }
441 return rr, err
442 }
443
444 var errUnknownSite = errors.New("dynamic lookup required to find mapping")
445
446
447
448
449
450
451
452 func RepoRootForImportPathStatic(importPath, scheme string) (*RepoRoot, error) {
453 if strings.Contains(importPath, "://") {
454 return nil, fmt.Errorf("invalid import path %q", importPath)
455 }
456 for _, srv := range vcsPaths {
457 if !strings.HasPrefix(importPath, srv.prefix) {
458 continue
459 }
460 m := srv.regexp.FindStringSubmatch(importPath)
461 if m == nil {
462 if srv.prefix != "" {
463 return nil, fmt.Errorf("invalid %s import path %q", srv.prefix, importPath)
464 }
465 continue
466 }
467
468
469 match := map[string]string{
470 "prefix": srv.prefix,
471 "import": importPath,
472 }
473 for i, name := range srv.regexp.SubexpNames() {
474 if name != "" && match[name] == "" {
475 match[name] = m[i]
476 }
477 }
478 if srv.vcs != "" {
479 match["vcs"] = expand(match, srv.vcs)
480 }
481 if srv.repo != "" {
482 match["repo"] = expand(match, srv.repo)
483 }
484 if srv.check != nil {
485 if err := srv.check(match); err != nil {
486 return nil, err
487 }
488 }
489 vcs := ByCmd(match["vcs"])
490 if vcs == nil {
491 return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
492 }
493 if srv.ping {
494 if scheme != "" {
495 match["repo"] = scheme + "://" + match["repo"]
496 } else {
497 for _, scheme := range vcs.Scheme {
498 if vcs.Ping(scheme, match["repo"]) == nil {
499 match["repo"] = scheme + "://" + match["repo"]
500 break
501 }
502 }
503 }
504 }
505 rr := &RepoRoot{
506 VCS: vcs,
507 Repo: match["repo"],
508 Root: match["root"],
509 }
510 return rr, nil
511 }
512 return nil, errUnknownSite
513 }
514
515
516
517
518
519 func RepoRootForImportDynamic(importPath string, verbose bool) (*RepoRoot, error) {
520 slash := strings.Index(importPath, "/")
521 if slash < 0 {
522 slash = len(importPath)
523 }
524 host := importPath[:slash]
525 if !strings.Contains(host, ".") {
526 return nil, errors.New("import path doesn't contain a hostname")
527 }
528 urlStr, body, err := httpsOrHTTP(importPath)
529 if err != nil {
530 return nil, fmt.Errorf("http/https fetch: %v", err)
531 }
532 defer body.Close()
533 imports, err := parseMetaGoImports(body)
534 if err != nil {
535 return nil, fmt.Errorf("parsing %s: %v", importPath, err)
536 }
537 metaImport, err := matchGoImport(imports, importPath)
538 if err != nil {
539 if err != errNoMatch {
540 return nil, fmt.Errorf("parse %s: %v", urlStr, err)
541 }
542 return nil, fmt.Errorf("parse %s: no go-import meta tags", urlStr)
543 }
544 if verbose {
545 log.Printf("get %q: found meta tag %#v at %s", importPath, metaImport, urlStr)
546 }
547
548
549
550
551
552
553 if metaImport.Prefix != importPath {
554 if verbose {
555 log.Printf("get %q: verifying non-authoritative meta tag", importPath)
556 }
557 urlStr0 := urlStr
558 urlStr, body, err = httpsOrHTTP(metaImport.Prefix)
559 if err != nil {
560 return nil, fmt.Errorf("fetch %s: %v", urlStr, err)
561 }
562 imports, err := parseMetaGoImports(body)
563 if err != nil {
564 return nil, fmt.Errorf("parsing %s: %v", importPath, err)
565 }
566 if len(imports) == 0 {
567 return nil, fmt.Errorf("fetch %s: no go-import meta tag", urlStr)
568 }
569 metaImport2, err := matchGoImport(imports, importPath)
570 if err != nil || metaImport != metaImport2 {
571 return nil, fmt.Errorf("%s and %s disagree about go-import for %s", urlStr0, urlStr, metaImport.Prefix)
572 }
573 }
574
575 if err := validateRepoRoot(metaImport.RepoRoot); err != nil {
576 return nil, fmt.Errorf("%s: invalid repo root %q: %v", urlStr, metaImport.RepoRoot, err)
577 }
578 rr := &RepoRoot{
579 VCS: ByCmd(metaImport.VCS),
580 Repo: metaImport.RepoRoot,
581 Root: metaImport.Prefix,
582 }
583 if rr.VCS == nil {
584 return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, metaImport.VCS)
585 }
586 return rr, nil
587 }
588
589
590
591 func validateRepoRoot(repoRoot string) error {
592 url, err := url.Parse(repoRoot)
593 if err != nil {
594 return err
595 }
596 if url.Scheme == "" {
597 return errors.New("no scheme")
598 }
599 return nil
600 }
601
602
603
604 type metaImport struct {
605 Prefix, VCS, RepoRoot string
606 }
607
608
609 var errNoMatch = errors.New("no import match")
610
611
612
613 func pathPrefix(s, sub string) bool {
614
615 if !strings.HasPrefix(s, sub) {
616 return false
617 }
618
619 rem := s[len(sub):]
620 return rem == "" || rem[0] == '/'
621 }
622
623
624
625
626 func matchGoImport(imports []metaImport, importPath string) (_ metaImport, err error) {
627 match := -1
628 for i, im := range imports {
629 if !pathPrefix(importPath, im.Prefix) {
630 continue
631 }
632
633 if match != -1 {
634 err = fmt.Errorf("multiple meta tags match import path %q", importPath)
635 return
636 }
637 match = i
638 }
639 if match == -1 {
640 err = errNoMatch
641 return
642 }
643 return imports[match], nil
644 }
645
646
647 func expand(match map[string]string, s string) string {
648 for k, v := range match {
649 s = strings.Replace(s, "{"+k+"}", v, -1)
650 }
651 return s
652 }
653
654
655 var vcsPaths = []*vcsPath{
656
657 {
658 prefix: "github.com/",
659 re: `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[\p{L}0-9_.\-]+)*$`,
660 vcs: "git",
661 repo: "https://{root}",
662 check: noVCSSuffix,
663 },
664
665
666 {
667 prefix: "bitbucket.org/",
668 re: `^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
669 repo: "https://{root}",
670 check: bitbucketVCS,
671 },
672
673
674 {
675 prefix: "launchpad.net/",
676 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_.\-]+)*$`,
677 vcs: "bzr",
678 repo: "https://{root}",
679 check: launchpadVCS,
680 },
681
682
683 {
684 prefix: "git.openstack.org",
685 re: `^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/[A-Za-z0-9_.\-]+)*$`,
686 vcs: "git",
687 repo: "https://{root}",
688 check: noVCSSuffix,
689 },
690
691
692 {
693 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_.\-]+)*$`,
694 ping: true,
695 },
696 }
697
698 func init() {
699
700
701
702 for _, srv := range vcsPaths {
703 srv.regexp = regexp.MustCompile(srv.re)
704 }
705 }
706
707
708
709
710 func noVCSSuffix(match map[string]string) error {
711 repo := match["repo"]
712 for _, vcs := range vcsList {
713 if strings.HasSuffix(repo, "."+vcs.Cmd) {
714 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"])
715 }
716 }
717 return nil
718 }
719
720
721
722 func bitbucketVCS(match map[string]string) error {
723 if err := noVCSSuffix(match); err != nil {
724 return err
725 }
726
727 var resp struct {
728 SCM string `json:"scm"`
729 }
730 url := expand(match, "https://api.bitbucket.org/2.0/repositories/{bitname}?fields=scm")
731 data, err := httpGET(url)
732 if err != nil {
733 return err
734 }
735 if err := json.Unmarshal(data, &resp); err != nil {
736 return fmt.Errorf("decoding %s: %v", url, err)
737 }
738
739 if ByCmd(resp.SCM) != nil {
740 match["vcs"] = resp.SCM
741 if resp.SCM == "git" {
742 match["repo"] += ".git"
743 }
744 return nil
745 }
746
747 return fmt.Errorf("unable to detect version control system for bitbucket.org/ path")
748 }
749
750
751
752
753
754 func launchpadVCS(match map[string]string) error {
755 if match["project"] == "" || match["series"] == "" {
756 return nil
757 }
758 _, err := httpGET(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format"))
759 if err != nil {
760 match["root"] = expand(match, "launchpad.net/{project}")
761 match["repo"] = expand(match, "https://{root}")
762 }
763 return nil
764 }
765
View as plain text