1
2
3
4
5 package codehost
6
7 import (
8 "errors"
9 "fmt"
10 "internal/lazyregexp"
11 "io"
12 "io/fs"
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 ...any) 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() any {
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 doReadZip func(dst io.Writer, workDir, rev, subdir, remote string) error
139 }
140
141 var re = lazyregexp.New
142
143 var vcsCmds = map[string]*vcsCmd{
144 "hg": {
145 vcs: "hg",
146 init: func(remote string) []string {
147 return []string{"hg", "clone", "-U", "--", remote, "."}
148 },
149 tags: func(remote string) []string {
150 return []string{"hg", "tags", "-q"}
151 },
152 tagRE: re(`(?m)^[^\n]+$`),
153 branches: func(remote string) []string {
154 return []string{"hg", "branches", "-c", "-q"}
155 },
156 branchRE: re(`(?m)^[^\n]+$`),
157 badLocalRevRE: re(`(?m)^(tip)$`),
158 statLocal: func(rev, remote string) []string {
159 return []string{"hg", "log", "-l1", "-r", rev, "--template", "{node} {date|hgdate} {tags}"}
160 },
161 parseStat: hgParseStat,
162 fetch: []string{"hg", "pull", "-f"},
163 latest: "tip",
164 readFile: func(rev, file, remote string) []string {
165 return []string{"hg", "cat", "-r", rev, file}
166 },
167 readZip: func(rev, subdir, remote, target string) []string {
168 pattern := []string{}
169 if subdir != "" {
170 pattern = []string{"-I", subdir + "/**"}
171 }
172 return str.StringList("hg", "archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/", pattern, "--", target)
173 },
174 },
175
176 "svn": {
177 vcs: "svn",
178 init: nil,
179 tags: func(remote string) []string {
180 return []string{"svn", "list", "--", strings.TrimSuffix(remote, "/trunk") + "/tags"}
181 },
182 tagRE: re(`(?m)^(.*?)/?$`),
183 statLocal: func(rev, remote string) []string {
184 suffix := "@" + rev
185 if rev == "latest" {
186 suffix = ""
187 }
188 return []string{"svn", "log", "-l1", "--xml", "--", remote + suffix}
189 },
190 parseStat: svnParseStat,
191 latest: "latest",
192 readFile: func(rev, file, remote string) []string {
193 return []string{"svn", "cat", "--", remote + "/" + file + "@" + rev}
194 },
195 doReadZip: svnReadZip,
196 },
197
198 "bzr": {
199 vcs: "bzr",
200 init: func(remote string) []string {
201 return []string{"bzr", "branch", "--use-existing-dir", "--", remote, "."}
202 },
203 fetch: []string{
204 "bzr", "pull", "--overwrite-tags",
205 },
206 tags: func(remote string) []string {
207 return []string{"bzr", "tags"}
208 },
209 tagRE: re(`(?m)^\S+`),
210 badLocalRevRE: re(`^revno:-`),
211 statLocal: func(rev, remote string) []string {
212 return []string{"bzr", "log", "-l1", "--long", "--show-ids", "-r", rev}
213 },
214 parseStat: bzrParseStat,
215 latest: "revno:-1",
216 readFile: func(rev, file, remote string) []string {
217 return []string{"bzr", "cat", "-r", rev, file}
218 },
219 readZip: func(rev, subdir, remote, target string) []string {
220 extra := []string{}
221 if subdir != "" {
222 extra = []string{"./" + subdir}
223 }
224 return str.StringList("bzr", "export", "--format=zip", "-r", rev, "--root=prefix/", "--", target, extra)
225 },
226 },
227
228 "fossil": {
229 vcs: "fossil",
230 init: func(remote string) []string {
231 return []string{"fossil", "clone", "--", remote, ".fossil"}
232 },
233 fetch: []string{"fossil", "pull", "-R", ".fossil"},
234 tags: func(remote string) []string {
235 return []string{"fossil", "tag", "-R", ".fossil", "list"}
236 },
237 tagRE: re(`XXXTODO`),
238 statLocal: func(rev, remote string) []string {
239 return []string{"fossil", "info", "-R", ".fossil", rev}
240 },
241 parseStat: fossilParseStat,
242 latest: "trunk",
243 readFile: func(rev, file, remote string) []string {
244 return []string{"fossil", "cat", "-R", ".fossil", "-r", rev, file}
245 },
246 readZip: func(rev, subdir, remote, target string) []string {
247 extra := []string{}
248 if subdir != "" && !strings.ContainsAny(subdir, "*?[],") {
249 extra = []string{"--include", subdir}
250 }
251
252
253 return str.StringList("fossil", "zip", "-R", ".fossil", "--name", "prefix", extra, "--", rev, target)
254 },
255 },
256 }
257
258 func (r *vcsRepo) loadTags() {
259 out, err := Run(r.dir, r.cmd.tags(r.remote))
260 if err != nil {
261 return
262 }
263
264
265 r.tags = make(map[string]bool)
266 for _, tag := range r.cmd.tagRE.FindAllString(string(out), -1) {
267 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(tag) {
268 continue
269 }
270 r.tags[tag] = true
271 }
272 }
273
274 func (r *vcsRepo) loadBranches() {
275 if r.cmd.branches == nil {
276 return
277 }
278
279 out, err := Run(r.dir, r.cmd.branches(r.remote))
280 if err != nil {
281 return
282 }
283
284 r.branches = make(map[string]bool)
285 for _, branch := range r.cmd.branchRE.FindAllString(string(out), -1) {
286 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(branch) {
287 continue
288 }
289 r.branches[branch] = true
290 }
291 }
292
293 func (r *vcsRepo) Tags(prefix string) ([]string, error) {
294 unlock, err := r.mu.Lock()
295 if err != nil {
296 return nil, err
297 }
298 defer unlock()
299
300 r.tagsOnce.Do(r.loadTags)
301
302 tags := []string{}
303 for tag := range r.tags {
304 if strings.HasPrefix(tag, prefix) {
305 tags = append(tags, tag)
306 }
307 }
308 sort.Strings(tags)
309 return tags, nil
310 }
311
312 func (r *vcsRepo) Stat(rev string) (*RevInfo, error) {
313 unlock, err := r.mu.Lock()
314 if err != nil {
315 return nil, err
316 }
317 defer unlock()
318
319 if rev == "latest" {
320 rev = r.cmd.latest
321 }
322 r.branchesOnce.Do(r.loadBranches)
323 revOK := (r.cmd.badLocalRevRE == nil || !r.cmd.badLocalRevRE.MatchString(rev)) && !r.branches[rev]
324 if revOK {
325 if info, err := r.statLocal(rev); err == nil {
326 return info, nil
327 }
328 }
329
330 r.fetchOnce.Do(r.fetch)
331 if r.fetchErr != nil {
332 return nil, r.fetchErr
333 }
334 info, err := r.statLocal(rev)
335 if err != nil {
336 return nil, err
337 }
338 if !revOK {
339 info.Version = info.Name
340 }
341 return info, nil
342 }
343
344 func (r *vcsRepo) fetch() {
345 if len(r.cmd.fetch) > 0 {
346 _, r.fetchErr = Run(r.dir, r.cmd.fetch)
347 }
348 }
349
350 func (r *vcsRepo) statLocal(rev string) (*RevInfo, error) {
351 out, err := Run(r.dir, r.cmd.statLocal(rev, r.remote))
352 if err != nil {
353 return nil, &UnknownRevisionError{Rev: rev}
354 }
355 return r.cmd.parseStat(rev, string(out))
356 }
357
358 func (r *vcsRepo) Latest() (*RevInfo, error) {
359 return r.Stat("latest")
360 }
361
362 func (r *vcsRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
363 if rev == "latest" {
364 rev = r.cmd.latest
365 }
366 _, err := r.Stat(rev)
367 if err != nil {
368 return nil, err
369 }
370
371
372 unlock, err := r.mu.Lock()
373 if err != nil {
374 return nil, err
375 }
376 defer unlock()
377
378 out, err := Run(r.dir, r.cmd.readFile(rev, file, r.remote))
379 if err != nil {
380 return nil, fs.ErrNotExist
381 }
382 return out, nil
383 }
384
385 func (r *vcsRepo) RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error) {
386
387
388
389 unlock, err := r.mu.Lock()
390 if err != nil {
391 return "", err
392 }
393 defer unlock()
394
395 return "", vcsErrorf("RecentTag not implemented")
396 }
397
398 func (r *vcsRepo) DescendsFrom(rev, tag string) (bool, error) {
399 unlock, err := r.mu.Lock()
400 if err != nil {
401 return false, err
402 }
403 defer unlock()
404
405 return false, vcsErrorf("DescendsFrom not implemented")
406 }
407
408 func (r *vcsRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
409 if r.cmd.readZip == nil && r.cmd.doReadZip == nil {
410 return nil, vcsErrorf("ReadZip not implemented for %s", r.cmd.vcs)
411 }
412
413 unlock, err := r.mu.Lock()
414 if err != nil {
415 return nil, err
416 }
417 defer unlock()
418
419 if rev == "latest" {
420 rev = r.cmd.latest
421 }
422 f, err := os.CreateTemp("", "go-readzip-*.zip")
423 if err != nil {
424 return nil, err
425 }
426 if r.cmd.doReadZip != nil {
427 lw := &limitedWriter{
428 W: f,
429 N: maxSize,
430 ErrLimitReached: errors.New("ReadZip: encoded file exceeds allowed size"),
431 }
432 err = r.cmd.doReadZip(lw, r.dir, rev, subdir, r.remote)
433 if err == nil {
434 _, err = f.Seek(0, io.SeekStart)
435 }
436 } else if r.cmd.vcs == "fossil" {
437
438
439
440
441
442 args := r.cmd.readZip(rev, subdir, r.remote, filepath.Base(f.Name()))
443 for i := range args {
444 if args[i] == ".fossil" {
445 args[i] = filepath.Join(r.dir, ".fossil")
446 }
447 }
448 _, err = Run(filepath.Dir(f.Name()), args)
449 } else {
450 _, err = Run(r.dir, r.cmd.readZip(rev, subdir, r.remote, f.Name()))
451 }
452 if err != nil {
453 f.Close()
454 os.Remove(f.Name())
455 return nil, err
456 }
457 return &deleteCloser{f}, nil
458 }
459
460
461 type deleteCloser struct {
462 *os.File
463 }
464
465 func (d *deleteCloser) Close() error {
466 defer os.Remove(d.File.Name())
467 return d.File.Close()
468 }
469
470 func hgParseStat(rev, out string) (*RevInfo, error) {
471 f := strings.Fields(string(out))
472 if len(f) < 3 {
473 return nil, vcsErrorf("unexpected response from hg log: %q", out)
474 }
475 hash := f[0]
476 version := rev
477 if strings.HasPrefix(hash, version) {
478 version = hash
479 }
480 t, err := strconv.ParseInt(f[1], 10, 64)
481 if err != nil {
482 return nil, vcsErrorf("invalid time from hg log: %q", out)
483 }
484
485 var tags []string
486 for _, tag := range f[3:] {
487 if tag != "tip" {
488 tags = append(tags, tag)
489 }
490 }
491 sort.Strings(tags)
492
493 info := &RevInfo{
494 Name: hash,
495 Short: ShortenSHA1(hash),
496 Time: time.Unix(t, 0).UTC(),
497 Version: version,
498 Tags: tags,
499 }
500 return info, nil
501 }
502
503 func bzrParseStat(rev, out string) (*RevInfo, error) {
504 var revno int64
505 var tm time.Time
506 for _, line := range strings.Split(out, "\n") {
507 if line == "" || line[0] == ' ' || line[0] == '\t' {
508
509 break
510 }
511 if line[0] == '-' {
512 continue
513 }
514 i := strings.Index(line, ":")
515 if i < 0 {
516
517 break
518 }
519 key, val := line[:i], strings.TrimSpace(line[i+1:])
520 switch key {
521 case "revno":
522 if j := strings.Index(val, " "); j >= 0 {
523 val = val[:j]
524 }
525 i, err := strconv.ParseInt(val, 10, 64)
526 if err != nil {
527 return nil, vcsErrorf("unexpected revno from bzr log: %q", line)
528 }
529 revno = i
530 case "timestamp":
531 j := strings.Index(val, " ")
532 if j < 0 {
533 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
534 }
535 t, err := time.Parse("2006-01-02 15:04:05 -0700", val[j+1:])
536 if err != nil {
537 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
538 }
539 tm = t.UTC()
540 }
541 }
542 if revno == 0 || tm.IsZero() {
543 return nil, vcsErrorf("unexpected response from bzr log: %q", out)
544 }
545
546 info := &RevInfo{
547 Name: fmt.Sprintf("%d", revno),
548 Short: fmt.Sprintf("%012d", revno),
549 Time: tm,
550 Version: rev,
551 }
552 return info, nil
553 }
554
555 func fossilParseStat(rev, out string) (*RevInfo, error) {
556 for _, line := range strings.Split(out, "\n") {
557 if strings.HasPrefix(line, "uuid:") || strings.HasPrefix(line, "hash:") {
558 f := strings.Fields(line)
559 if len(f) != 5 || len(f[1]) != 40 || f[4] != "UTC" {
560 return nil, vcsErrorf("unexpected response from fossil info: %q", line)
561 }
562 t, err := time.Parse("2006-01-02 15:04:05", f[2]+" "+f[3])
563 if err != nil {
564 return nil, vcsErrorf("unexpected response from fossil info: %q", line)
565 }
566 hash := f[1]
567 version := rev
568 if strings.HasPrefix(hash, version) {
569 version = hash
570 }
571 info := &RevInfo{
572 Name: hash,
573 Short: ShortenSHA1(hash),
574 Time: t,
575 Version: version,
576 }
577 return info, nil
578 }
579 }
580 return nil, vcsErrorf("unexpected response from fossil info: %q", out)
581 }
582
583 type limitedWriter struct {
584 W io.Writer
585 N int64
586 ErrLimitReached error
587 }
588
589 func (l *limitedWriter) Write(p []byte) (n int, err error) {
590 if l.N > 0 {
591 max := len(p)
592 if l.N < int64(max) {
593 max = int(l.N)
594 }
595 n, err = l.W.Write(p[:max])
596 l.N -= int64(n)
597 if err != nil || n >= len(p) {
598 return n, err
599 }
600 }
601
602 return n, l.ErrLimitReached
603 }
604
View as plain text