Source file src/pkg/cmd/go/internal/modfetch/codehost/vcs.go
1
2
3
4
5 package codehost
6
7 import (
8 "encoding/xml"
9 "fmt"
10 "internal/lazyregexp"
11 "io"
12 "io/ioutil"
13 "os"
14 "path/filepath"
15 "sort"
16 "strconv"
17 "strings"
18 "sync"
19 "time"
20
21 "cmd/go/internal/lockedfile"
22 "cmd/go/internal/par"
23 "cmd/go/internal/str"
24 )
25
26
27
28
29
30
31
32
33
34
35 type VCSError struct {
36 Err error
37 }
38
39 func (e *VCSError) Error() string { return e.Err.Error() }
40
41 func vcsErrorf(format string, a ...interface{}) error {
42 return &VCSError{Err: fmt.Errorf(format, a...)}
43 }
44
45 func NewRepo(vcs, remote string) (Repo, error) {
46 type key struct {
47 vcs string
48 remote string
49 }
50 type cached struct {
51 repo Repo
52 err error
53 }
54 c := vcsRepoCache.Do(key{vcs, remote}, func() interface{} {
55 repo, err := newVCSRepo(vcs, remote)
56 if err != nil {
57 err = &VCSError{err}
58 }
59 return cached{repo, err}
60 }).(cached)
61
62 return c.repo, c.err
63 }
64
65 var vcsRepoCache par.Cache
66
67 type vcsRepo struct {
68 mu lockedfile.Mutex
69
70 remote string
71 cmd *vcsCmd
72 dir string
73
74 tagsOnce sync.Once
75 tags map[string]bool
76
77 branchesOnce sync.Once
78 branches map[string]bool
79
80 fetchOnce sync.Once
81 fetchErr error
82 }
83
84 func newVCSRepo(vcs, remote string) (Repo, error) {
85 if vcs == "git" {
86 return newGitRepo(remote, false)
87 }
88 cmd := vcsCmds[vcs]
89 if cmd == nil {
90 return nil, fmt.Errorf("unknown vcs: %s %s", vcs, remote)
91 }
92 if !strings.Contains(remote, "://") {
93 return nil, fmt.Errorf("invalid vcs remote: %s %s", vcs, remote)
94 }
95
96 r := &vcsRepo{remote: remote, cmd: cmd}
97 var err error
98 r.dir, r.mu.Path, err = WorkDir(vcsWorkDirType+vcs, r.remote)
99 if err != nil {
100 return nil, err
101 }
102
103 if cmd.init == nil {
104 return r, nil
105 }
106
107 unlock, err := r.mu.Lock()
108 if err != nil {
109 return nil, err
110 }
111 defer unlock()
112
113 if _, err := os.Stat(filepath.Join(r.dir, "."+vcs)); err != nil {
114 if _, err := Run(r.dir, cmd.init(r.remote)); err != nil {
115 os.RemoveAll(r.dir)
116 return nil, err
117 }
118 }
119 return r, nil
120 }
121
122 const vcsWorkDirType = "vcs1."
123
124 type vcsCmd struct {
125 vcs string
126 init func(remote string) []string
127 tags func(remote string) []string
128 tagRE *lazyregexp.Regexp
129 branches func(remote string) []string
130 branchRE *lazyregexp.Regexp
131 badLocalRevRE *lazyregexp.Regexp
132 statLocal func(rev, remote string) []string
133 parseStat func(rev, out string) (*RevInfo, error)
134 fetch []string
135 latest string
136 readFile func(rev, file, remote string) []string
137 readZip func(rev, subdir, remote, target string) []string
138 }
139
140 var re = lazyregexp.New
141
142 var vcsCmds = map[string]*vcsCmd{
143 "hg": {
144 vcs: "hg",
145 init: func(remote string) []string {
146 return []string{"hg", "clone", "-U", "--", remote, "."}
147 },
148 tags: func(remote string) []string {
149 return []string{"hg", "tags", "-q"}
150 },
151 tagRE: re(`(?m)^[^\n]+$`),
152 branches: func(remote string) []string {
153 return []string{"hg", "branches", "-c", "-q"}
154 },
155 branchRE: re(`(?m)^[^\n]+$`),
156 badLocalRevRE: re(`(?m)^(tip)$`),
157 statLocal: func(rev, remote string) []string {
158 return []string{"hg", "log", "-l1", "-r", rev, "--template", "{node} {date|hgdate} {tags}"}
159 },
160 parseStat: hgParseStat,
161 fetch: []string{"hg", "pull", "-f"},
162 latest: "tip",
163 readFile: func(rev, file, remote string) []string {
164 return []string{"hg", "cat", "-r", rev, file}
165 },
166 readZip: func(rev, subdir, remote, target string) []string {
167 pattern := []string{}
168 if subdir != "" {
169 pattern = []string{"-I", subdir + "/**"}
170 }
171 return str.StringList("hg", "archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/", pattern, "--", target)
172 },
173 },
174
175 "svn": {
176 vcs: "svn",
177 init: nil,
178 tags: func(remote string) []string {
179 return []string{"svn", "list", "--", strings.TrimSuffix(remote, "/trunk") + "/tags"}
180 },
181 tagRE: re(`(?m)^(.*?)/?$`),
182 statLocal: func(rev, remote string) []string {
183 suffix := "@" + rev
184 if rev == "latest" {
185 suffix = ""
186 }
187 return []string{"svn", "log", "-l1", "--xml", "--", remote + suffix}
188 },
189 parseStat: svnParseStat,
190 latest: "latest",
191 readFile: func(rev, file, remote string) []string {
192 return []string{"svn", "cat", "--", remote + "/" + file + "@" + rev}
193 },
194
195 },
196
197 "bzr": {
198 vcs: "bzr",
199 init: func(remote string) []string {
200 return []string{"bzr", "branch", "--use-existing-dir", "--", remote, "."}
201 },
202 fetch: []string{
203 "bzr", "pull", "--overwrite-tags",
204 },
205 tags: func(remote string) []string {
206 return []string{"bzr", "tags"}
207 },
208 tagRE: re(`(?m)^\S+`),
209 badLocalRevRE: re(`^revno:-`),
210 statLocal: func(rev, remote string) []string {
211 return []string{"bzr", "log", "-l1", "--long", "--show-ids", "-r", rev}
212 },
213 parseStat: bzrParseStat,
214 latest: "revno:-1",
215 readFile: func(rev, file, remote string) []string {
216 return []string{"bzr", "cat", "-r", rev, file}
217 },
218 readZip: func(rev, subdir, remote, target string) []string {
219 extra := []string{}
220 if subdir != "" {
221 extra = []string{"./" + subdir}
222 }
223 return str.StringList("bzr", "export", "--format=zip", "-r", rev, "--root=prefix/", "--", target, extra)
224 },
225 },
226
227 "fossil": {
228 vcs: "fossil",
229 init: func(remote string) []string {
230 return []string{"fossil", "clone", "--", remote, ".fossil"}
231 },
232 fetch: []string{"fossil", "pull", "-R", ".fossil"},
233 tags: func(remote string) []string {
234 return []string{"fossil", "tag", "-R", ".fossil", "list"}
235 },
236 tagRE: re(`XXXTODO`),
237 statLocal: func(rev, remote string) []string {
238 return []string{"fossil", "info", "-R", ".fossil", rev}
239 },
240 parseStat: fossilParseStat,
241 latest: "trunk",
242 readFile: func(rev, file, remote string) []string {
243 return []string{"fossil", "cat", "-R", ".fossil", "-r", rev, file}
244 },
245 readZip: func(rev, subdir, remote, target string) []string {
246 extra := []string{}
247 if subdir != "" && !strings.ContainsAny(subdir, "*?[],") {
248 extra = []string{"--include", subdir}
249 }
250
251
252 return str.StringList("fossil", "zip", "-R", ".fossil", "--name", "prefix", extra, "--", rev, target)
253 },
254 },
255 }
256
257 func (r *vcsRepo) loadTags() {
258 out, err := Run(r.dir, r.cmd.tags(r.remote))
259 if err != nil {
260 return
261 }
262
263
264 r.tags = make(map[string]bool)
265 for _, tag := range r.cmd.tagRE.FindAllString(string(out), -1) {
266 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(tag) {
267 continue
268 }
269 r.tags[tag] = true
270 }
271 }
272
273 func (r *vcsRepo) loadBranches() {
274 if r.cmd.branches == nil {
275 return
276 }
277
278 out, err := Run(r.dir, r.cmd.branches(r.remote))
279 if err != nil {
280 return
281 }
282
283 r.branches = make(map[string]bool)
284 for _, branch := range r.cmd.branchRE.FindAllString(string(out), -1) {
285 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(branch) {
286 continue
287 }
288 r.branches[branch] = true
289 }
290 }
291
292 func (r *vcsRepo) Tags(prefix string) ([]string, error) {
293 unlock, err := r.mu.Lock()
294 if err != nil {
295 return nil, err
296 }
297 defer unlock()
298
299 r.tagsOnce.Do(r.loadTags)
300
301 tags := []string{}
302 for tag := range r.tags {
303 if strings.HasPrefix(tag, prefix) {
304 tags = append(tags, tag)
305 }
306 }
307 sort.Strings(tags)
308 return tags, nil
309 }
310
311 func (r *vcsRepo) Stat(rev string) (*RevInfo, error) {
312 unlock, err := r.mu.Lock()
313 if err != nil {
314 return nil, err
315 }
316 defer unlock()
317
318 if rev == "latest" {
319 rev = r.cmd.latest
320 }
321 r.branchesOnce.Do(r.loadBranches)
322 revOK := (r.cmd.badLocalRevRE == nil || !r.cmd.badLocalRevRE.MatchString(rev)) && !r.branches[rev]
323 if revOK {
324 if info, err := r.statLocal(rev); err == nil {
325 return info, nil
326 }
327 }
328
329 r.fetchOnce.Do(r.fetch)
330 if r.fetchErr != nil {
331 return nil, r.fetchErr
332 }
333 info, err := r.statLocal(rev)
334 if err != nil {
335 return nil, err
336 }
337 if !revOK {
338 info.Version = info.Name
339 }
340 return info, nil
341 }
342
343 func (r *vcsRepo) fetch() {
344 if len(r.cmd.fetch) > 0 {
345 _, r.fetchErr = Run(r.dir, r.cmd.fetch)
346 }
347 }
348
349 func (r *vcsRepo) statLocal(rev string) (*RevInfo, error) {
350 out, err := Run(r.dir, r.cmd.statLocal(rev, r.remote))
351 if err != nil {
352 return nil, &UnknownRevisionError{Rev: rev}
353 }
354 return r.cmd.parseStat(rev, string(out))
355 }
356
357 func (r *vcsRepo) Latest() (*RevInfo, error) {
358 return r.Stat("latest")
359 }
360
361 func (r *vcsRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
362 if rev == "latest" {
363 rev = r.cmd.latest
364 }
365 _, err := r.Stat(rev)
366 if err != nil {
367 return nil, err
368 }
369
370
371 unlock, err := r.mu.Lock()
372 if err != nil {
373 return nil, err
374 }
375 defer unlock()
376
377 out, err := Run(r.dir, r.cmd.readFile(rev, file, r.remote))
378 if err != nil {
379 return nil, os.ErrNotExist
380 }
381 return out, nil
382 }
383
384 func (r *vcsRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[string]*FileRev, error) {
385
386
387
388 unlock, err := r.mu.Lock()
389 if err != nil {
390 return nil, err
391 }
392 defer unlock()
393
394 return nil, vcsErrorf("ReadFileRevs not implemented")
395 }
396
397 func (r *vcsRepo) RecentTag(rev, prefix, major string) (tag string, err error) {
398
399
400
401 unlock, err := r.mu.Lock()
402 if err != nil {
403 return "", err
404 }
405 defer unlock()
406
407 return "", vcsErrorf("RecentTag not implemented")
408 }
409
410 func (r *vcsRepo) DescendsFrom(rev, tag string) (bool, error) {
411 unlock, err := r.mu.Lock()
412 if err != nil {
413 return false, err
414 }
415 defer unlock()
416
417 return false, vcsErrorf("DescendsFrom not implemented")
418 }
419
420 func (r *vcsRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error) {
421 if r.cmd.readZip == nil {
422 return nil, "", vcsErrorf("ReadZip not implemented for %s", r.cmd.vcs)
423 }
424
425 unlock, err := r.mu.Lock()
426 if err != nil {
427 return nil, "", err
428 }
429 defer unlock()
430
431 if rev == "latest" {
432 rev = r.cmd.latest
433 }
434 f, err := ioutil.TempFile("", "go-readzip-*.zip")
435 if err != nil {
436 return nil, "", err
437 }
438 if r.cmd.vcs == "fossil" {
439
440
441
442
443
444 args := r.cmd.readZip(rev, subdir, r.remote, filepath.Base(f.Name()))
445 for i := range args {
446 if args[i] == ".fossil" {
447 args[i] = filepath.Join(r.dir, ".fossil")
448 }
449 }
450 _, err = Run(filepath.Dir(f.Name()), args)
451 } else {
452 _, err = Run(r.dir, r.cmd.readZip(rev, subdir, r.remote, f.Name()))
453 }
454 if err != nil {
455 f.Close()
456 os.Remove(f.Name())
457 return nil, "", err
458 }
459 return &deleteCloser{f}, "", nil
460 }
461
462
463 type deleteCloser struct {
464 *os.File
465 }
466
467 func (d *deleteCloser) Close() error {
468 defer os.Remove(d.File.Name())
469 return d.File.Close()
470 }
471
472 func hgParseStat(rev, out string) (*RevInfo, error) {
473 f := strings.Fields(string(out))
474 if len(f) < 3 {
475 return nil, vcsErrorf("unexpected response from hg log: %q", out)
476 }
477 hash := f[0]
478 version := rev
479 if strings.HasPrefix(hash, version) {
480 version = hash
481 }
482 t, err := strconv.ParseInt(f[1], 10, 64)
483 if err != nil {
484 return nil, vcsErrorf("invalid time from hg log: %q", out)
485 }
486
487 var tags []string
488 for _, tag := range f[3:] {
489 if tag != "tip" {
490 tags = append(tags, tag)
491 }
492 }
493 sort.Strings(tags)
494
495 info := &RevInfo{
496 Name: hash,
497 Short: ShortenSHA1(hash),
498 Time: time.Unix(t, 0).UTC(),
499 Version: version,
500 Tags: tags,
501 }
502 return info, nil
503 }
504
505 func svnParseStat(rev, out string) (*RevInfo, error) {
506 var log struct {
507 Logentry struct {
508 Revision int64 `xml:"revision,attr"`
509 Date string `xml:"date"`
510 } `xml:"logentry"`
511 }
512 if err := xml.Unmarshal([]byte(out), &log); err != nil {
513 return nil, vcsErrorf("unexpected response from svn log --xml: %v\n%s", err, out)
514 }
515
516 t, err := time.Parse(time.RFC3339, log.Logentry.Date)
517 if err != nil {
518 return nil, vcsErrorf("unexpected response from svn log --xml: %v\n%s", err, out)
519 }
520
521 info := &RevInfo{
522 Name: fmt.Sprintf("%d", log.Logentry.Revision),
523 Short: fmt.Sprintf("%012d", log.Logentry.Revision),
524 Time: t.UTC(),
525 Version: rev,
526 }
527 return info, nil
528 }
529
530 func bzrParseStat(rev, out string) (*RevInfo, error) {
531 var revno int64
532 var tm time.Time
533 for _, line := range strings.Split(out, "\n") {
534 if line == "" || line[0] == ' ' || line[0] == '\t' {
535
536 break
537 }
538 if line[0] == '-' {
539 continue
540 }
541 i := strings.Index(line, ":")
542 if i < 0 {
543
544 break
545 }
546 key, val := line[:i], strings.TrimSpace(line[i+1:])
547 switch key {
548 case "revno":
549 if j := strings.Index(val, " "); j >= 0 {
550 val = val[:j]
551 }
552 i, err := strconv.ParseInt(val, 10, 64)
553 if err != nil {
554 return nil, vcsErrorf("unexpected revno from bzr log: %q", line)
555 }
556 revno = i
557 case "timestamp":
558 j := strings.Index(val, " ")
559 if j < 0 {
560 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
561 }
562 t, err := time.Parse("2006-01-02 15:04:05 -0700", val[j+1:])
563 if err != nil {
564 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
565 }
566 tm = t.UTC()
567 }
568 }
569 if revno == 0 || tm.IsZero() {
570 return nil, vcsErrorf("unexpected response from bzr log: %q", out)
571 }
572
573 info := &RevInfo{
574 Name: fmt.Sprintf("%d", revno),
575 Short: fmt.Sprintf("%012d", revno),
576 Time: tm,
577 Version: rev,
578 }
579 return info, nil
580 }
581
582 func fossilParseStat(rev, out string) (*RevInfo, error) {
583 for _, line := range strings.Split(out, "\n") {
584 if strings.HasPrefix(line, "uuid:") {
585 f := strings.Fields(line)
586 if len(f) != 5 || len(f[1]) != 40 || f[4] != "UTC" {
587 return nil, vcsErrorf("unexpected response from fossil info: %q", line)
588 }
589 t, err := time.Parse("2006-01-02 15:04:05", f[2]+" "+f[3])
590 if err != nil {
591 return nil, vcsErrorf("unexpected response from fossil info: %q", line)
592 }
593 hash := f[1]
594 version := rev
595 if strings.HasPrefix(hash, version) {
596 version = hash
597 }
598 info := &RevInfo{
599 Name: hash,
600 Short: ShortenSHA1(hash),
601 Time: t,
602 Version: version,
603 }
604 return info, nil
605 }
606 }
607 return nil, vcsErrorf("unexpected response from fossil info: %q", out)
608 }
609
View as plain text