1
2
3
4
5 package codehost
6
7 import (
8 "context"
9 "errors"
10 "fmt"
11 "internal/lazyregexp"
12 "io"
13 "io/fs"
14 "os"
15 "path/filepath"
16 "sort"
17 "strconv"
18 "strings"
19 "sync"
20 "sync/atomic"
21 "time"
22
23 "cmd/go/internal/base"
24 "cmd/go/internal/cfg"
25 "cmd/go/internal/lockedfile"
26 "cmd/go/internal/str"
27 "cmd/internal/par"
28
29 "golang.org/x/mod/semver"
30 )
31
32
33
34
35
36
37
38
39
40
41 type VCSError struct {
42 Err error
43 }
44
45 func (e *VCSError) Error() string { return e.Err.Error() }
46
47 func (e *VCSError) Unwrap() error { return e.Err }
48
49 func vcsErrorf(format string, a ...any) error {
50 return &VCSError{Err: fmt.Errorf(format, a...)}
51 }
52
53 type vcsCacheKey struct {
54 vcs string
55 remote string
56 local bool
57 }
58
59 func NewRepo(ctx context.Context, vcs, remote string, local bool) (Repo, error) {
60 return vcsRepoCache.Do(vcsCacheKey{vcs, remote, local}, func() (Repo, error) {
61 repo, err := newVCSRepo(ctx, vcs, remote, local)
62 if err != nil {
63 return nil, &VCSError{err}
64 }
65 return repo, nil
66 })
67 }
68
69 var vcsRepoCache par.ErrCache[vcsCacheKey, Repo]
70
71 type vcsRepo struct {
72 mu lockedfile.Mutex
73
74 remote string
75 cmd *vcsCmd
76 dir string
77 local bool
78
79 tagsOnce sync.Once
80 tags map[string]bool
81
82 branchesOnce sync.Once
83 branches map[string]bool
84
85 fetchOnce sync.Once
86 fetchErr error
87 fetched atomic.Bool
88
89 repoSumOnce sync.Once
90 repoSum string
91 }
92
93 func newVCSRepo(ctx context.Context, vcs, remote string, local bool) (Repo, error) {
94 if vcs == "git" {
95 return newGitRepo(ctx, remote, local)
96 }
97 r := &vcsRepo{remote: remote, local: local}
98 cmd := vcsCmds[vcs]
99 if cmd == nil {
100 return nil, fmt.Errorf("unknown vcs: %s %s", vcs, remote)
101 }
102 r.cmd = cmd
103 if local {
104 info, err := os.Stat(remote)
105 if err != nil {
106 return nil, err
107 }
108 if !info.IsDir() {
109 return nil, fmt.Errorf("%s exists but is not a directory", remote)
110 }
111 r.dir = remote
112 r.mu.Path = r.dir + ".lock"
113 return r, nil
114 }
115 if !strings.Contains(remote, "://") {
116 return nil, fmt.Errorf("invalid vcs remote: %s %s", vcs, remote)
117 }
118 var err error
119 r.dir, r.mu.Path, err = WorkDir(ctx, vcsWorkDirType+vcs, r.remote)
120 if err != nil {
121 return nil, err
122 }
123
124 if cmd.init == nil {
125 return r, nil
126 }
127
128 unlock, err := r.mu.Lock()
129 if err != nil {
130 return nil, err
131 }
132 defer unlock()
133
134 if _, err := os.Stat(filepath.Join(r.dir, "."+vcs)); err != nil {
135 release, err := base.AcquireNet()
136 if err != nil {
137 return nil, err
138 }
139 _, err = Run(ctx, r.dir, cmd.init(r.remote))
140 if err == nil && cmd.postInit != nil {
141 err = cmd.postInit(ctx, r)
142 }
143 release()
144
145 if err != nil {
146 os.RemoveAll(r.dir)
147 return nil, err
148 }
149 }
150 return r, nil
151 }
152
153 const vcsWorkDirType = "vcs1."
154
155 type vcsCmd struct {
156 vcs string
157 init func(remote string) []string
158 postInit func(context.Context, *vcsRepo) error
159 repoSum func(remote string) []string
160 lookupRef func(remote, ref string) []string
161 tags func(remote string) []string
162 tagsNeedsFetch bool
163 tagRE *lazyregexp.Regexp
164 branches func(remote string) []string
165 branchesNeedsFetch bool
166 branchRE *lazyregexp.Regexp
167 badLocalRevRE *lazyregexp.Regexp
168 statLocal func(rev, remote string) []string
169 parseStat func(rev, out string) (*RevInfo, error)
170 fetch []string
171 latest string
172 descendsFrom func(rev, tag string) []string
173 recentTags func(rev string) []string
174 readFile func(rev, file, remote string) []string
175 readZip func(rev, subdir, remote, target string) []string
176
177
178 doReadZip func(ctx context.Context, dst io.Writer, workDir, rev, subdir, remote string) error
179 }
180
181 var re = lazyregexp.New
182
183 var vcsCmds = map[string]*vcsCmd{
184 "hg": {
185 vcs: "hg",
186 repoSum: func(remote string) []string {
187 return []string{
188 "hg",
189 "--config=extensions.goreposum=" + filepath.Join(cfg.GOROOT, "lib/hg/goreposum.py"),
190 "goreposum",
191 remote,
192 }
193 },
194 lookupRef: func(remote, ref string) []string {
195 return []string{
196 "hg",
197 "--config=extensions.goreposum=" + filepath.Join(cfg.GOROOT, "lib/hg/goreposum.py"),
198 "golookup",
199 remote,
200 ref,
201 }
202 },
203 init: func(remote string) []string {
204 return []string{"hg", "init", "."}
205 },
206 postInit: hgAddRemote,
207 tags: func(remote string) []string {
208 return []string{"hg", "tags", "-q"}
209 },
210 tagsNeedsFetch: true,
211 tagRE: re(`(?m)^[^\n]+$`),
212 branches: func(remote string) []string {
213 return []string{"hg", "branches", "-c", "-q"}
214 },
215 branchesNeedsFetch: true,
216 branchRE: re(`(?m)^[^\n]+$`),
217 badLocalRevRE: re(`(?m)^(tip)$`),
218 statLocal: func(rev, remote string) []string {
219 return []string{"hg", "log", "-l1", "-r", rev, "--template", "{node} {date|hgdate} {tags}"}
220 },
221 parseStat: hgParseStat,
222 fetch: []string{"hg", "pull", "-f"},
223 latest: "tip",
224 descendsFrom: func(rev, tag string) []string {
225 return []string{"hg", "log", "-r", "ancestors(" + rev + ") and " + tag}
226 },
227 recentTags: func(rev string) []string {
228 return []string{"hg", "log", "-r", "ancestors(" + rev + ") and tag()", "--template", "{tags}\n"}
229 },
230 readFile: func(rev, file, remote string) []string {
231 return []string{"hg", "cat", "-r", rev, file}
232 },
233 readZip: func(rev, subdir, remote, target string) []string {
234 pattern := []string{}
235 if subdir != "" {
236 pattern = []string{"-I", subdir + "/**"}
237 }
238 return str.StringList("hg", "archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/", pattern, "--", target)
239 },
240 },
241
242 "svn": {
243 vcs: "svn",
244 init: nil,
245 tags: func(remote string) []string {
246 return []string{"svn", "list", "--", strings.TrimSuffix(remote, "/trunk") + "/tags"}
247 },
248 tagRE: re(`(?m)^(.*?)/?$`),
249 statLocal: func(rev, remote string) []string {
250 suffix := "@" + rev
251 if rev == "latest" {
252 suffix = ""
253 }
254 return []string{"svn", "log", "-l1", "--xml", "--", remote + suffix}
255 },
256 parseStat: svnParseStat,
257 latest: "latest",
258 readFile: func(rev, file, remote string) []string {
259 return []string{"svn", "cat", "--", remote + "/" + file + "@" + rev}
260 },
261 doReadZip: svnReadZip,
262 },
263
264 "bzr": {
265 vcs: "bzr",
266 init: func(remote string) []string {
267 return []string{"bzr", "branch", "--use-existing-dir", "--", remote, "."}
268 },
269 fetch: []string{
270 "bzr", "pull", "--overwrite-tags",
271 },
272 tags: func(remote string) []string {
273 return []string{"bzr", "tags"}
274 },
275 tagRE: re(`(?m)^\S+`),
276 badLocalRevRE: re(`^revno:-`),
277 statLocal: func(rev, remote string) []string {
278 return []string{"bzr", "log", "-l1", "--long", "--show-ids", "-r", rev}
279 },
280 parseStat: bzrParseStat,
281 latest: "revno:-1",
282 readFile: func(rev, file, remote string) []string {
283 return []string{"bzr", "cat", "-r", rev, file}
284 },
285 readZip: func(rev, subdir, remote, target string) []string {
286 extra := []string{}
287 if subdir != "" {
288 extra = []string{"./" + subdir}
289 }
290 return str.StringList("bzr", "export", "--format=zip", "-r", rev, "--root=prefix/", "--", target, extra)
291 },
292 },
293
294 "fossil": {
295 vcs: "fossil",
296 init: func(remote string) []string {
297 return []string{"fossil", "clone", "--", remote, ".fossil"}
298 },
299 fetch: []string{"fossil", "pull", "-R", ".fossil"},
300 tags: func(remote string) []string {
301 return []string{"fossil", "tag", "-R", ".fossil", "list"}
302 },
303 tagRE: re(`XXXTODO`),
304 statLocal: func(rev, remote string) []string {
305 return []string{"fossil", "info", "-R", ".fossil", rev}
306 },
307 parseStat: fossilParseStat,
308 latest: "trunk",
309 readFile: func(rev, file, remote string) []string {
310 return []string{"fossil", "cat", "-R", ".fossil", "-r", rev, file}
311 },
312 readZip: func(rev, subdir, remote, target string) []string {
313 extra := []string{}
314 if subdir != "" && !strings.ContainsAny(subdir, "*?[],") {
315 extra = []string{"--include", subdir}
316 }
317
318
319 return str.StringList("fossil", "zip", "-R", ".fossil", "--name", "prefix", extra, "--", rev, target)
320 },
321 },
322 }
323
324 func (r *vcsRepo) loadTags(ctx context.Context) {
325 if r.cmd.tagsNeedsFetch {
326 r.fetchOnce.Do(func() { r.fetch(ctx) })
327 }
328
329 out, err := Run(ctx, r.dir, r.cmd.tags(r.remote))
330 if err != nil {
331 return
332 }
333
334
335 r.tags = make(map[string]bool)
336 for _, tag := range r.cmd.tagRE.FindAllString(string(out), -1) {
337 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(tag) {
338 continue
339 }
340 r.tags[tag] = true
341 }
342 }
343
344 func (r *vcsRepo) loadBranches(ctx context.Context) {
345 if r.cmd.branches == nil {
346 return
347 }
348
349 if r.cmd.branchesNeedsFetch {
350 r.fetchOnce.Do(func() { r.fetch(ctx) })
351 }
352
353 out, err := Run(ctx, r.dir, r.cmd.branches(r.remote))
354 if err != nil {
355 return
356 }
357
358 r.branches = make(map[string]bool)
359 for _, branch := range r.cmd.branchRE.FindAllString(string(out), -1) {
360 if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(branch) {
361 continue
362 }
363 r.branches[branch] = true
364 }
365 }
366
367 func (r *vcsRepo) loadRepoSum(ctx context.Context) {
368 if r.cmd.repoSum == nil {
369 return
370 }
371 where := r.remote
372 if r.fetched.Load() {
373 where = "."
374 }
375 out, err := Run(ctx, r.dir, r.cmd.repoSum(where))
376 if err != nil {
377 return
378 }
379 r.repoSum = strings.TrimSpace(string(out))
380 }
381
382 func (r *vcsRepo) lookupRef(ctx context.Context, ref string) (string, error) {
383 if r.cmd.lookupRef == nil {
384 return "", fmt.Errorf("no lookupRef")
385 }
386 out, err := Run(ctx, r.dir, r.cmd.lookupRef(r.remote, ref))
387 if err != nil {
388 return "", err
389 }
390 return strings.TrimSpace(string(out)), nil
391 }
392
393
394 func (r *vcsRepo) repoSumOrigin(ctx context.Context) *Origin {
395 origin := &Origin{
396 VCS: r.cmd.vcs,
397 URL: r.remote,
398 RepoSum: r.repoSum,
399 }
400 r.repoSumOnce.Do(func() { r.loadRepoSum(ctx) })
401 origin.RepoSum = r.repoSum
402 return origin
403 }
404
405 func (r *vcsRepo) CheckReuse(ctx context.Context, old *Origin, subdir string) error {
406 if old == nil {
407 return fmt.Errorf("missing origin")
408 }
409 if old.VCS != r.cmd.vcs || old.URL != r.remote {
410 return fmt.Errorf("origin moved from %v %q to %v %q", old.VCS, old.URL, r.cmd.vcs, r.remote)
411 }
412 if old.Subdir != subdir {
413 return fmt.Errorf("origin moved from %v %q %q to %v %q %q", old.VCS, old.URL, old.Subdir, r.cmd.vcs, r.remote, subdir)
414 }
415
416 if old.Ref == "" && old.RepoSum == "" && old.Hash != "" {
417
418 hash, err := r.lookupRef(ctx, old.Hash)
419 if err == nil && hash == old.Hash {
420 return nil
421 }
422 if err != nil {
423 return fmt.Errorf("looking up hash: %v", err)
424 }
425 return fmt.Errorf("hash changed")
426 }
427
428 if old.Ref != "" && old.RepoSum == "" {
429 hash, err := r.lookupRef(ctx, old.Ref)
430 if err == nil && hash != "" && hash == old.Hash {
431 return nil
432 }
433 }
434
435 r.repoSumOnce.Do(func() { r.loadRepoSum(ctx) })
436 if r.repoSum != "" {
437 if old.RepoSum == "" {
438 return fmt.Errorf("non-specific origin")
439 }
440 if old.RepoSum != r.repoSum {
441 return fmt.Errorf("repo changed")
442 }
443 return nil
444 }
445 return fmt.Errorf("vcs %s: CheckReuse: %w", r.cmd.vcs, errors.ErrUnsupported)
446 }
447
448 func (r *vcsRepo) Tags(ctx context.Context, prefix string) (*Tags, error) {
449 unlock, err := r.mu.Lock()
450 if err != nil {
451 return nil, err
452 }
453 defer unlock()
454
455 r.tagsOnce.Do(func() { r.loadTags(ctx) })
456 tags := &Tags{
457 Origin: r.repoSumOrigin(ctx),
458 List: []Tag{},
459 }
460 for tag := range r.tags {
461 if strings.HasPrefix(tag, prefix) {
462 tags.List = append(tags.List, Tag{tag, ""})
463 }
464 }
465 sort.Slice(tags.List, func(i, j int) bool {
466 return tags.List[i].Name < tags.List[j].Name
467 })
468 return tags, nil
469 }
470
471 func (r *vcsRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
472 unlock, err := r.mu.Lock()
473 if err != nil {
474 return nil, err
475 }
476 defer unlock()
477
478 if rev == "latest" {
479 rev = r.cmd.latest
480 }
481 r.branchesOnce.Do(func() { r.loadBranches(ctx) })
482 if r.local {
483
484
485 return r.statLocal(ctx, rev)
486 }
487 revOK := (r.cmd.badLocalRevRE == nil || !r.cmd.badLocalRevRE.MatchString(rev)) && !r.branches[rev]
488 if revOK {
489 if info, err := r.statLocal(ctx, rev); err == nil {
490 return info, nil
491 }
492 }
493
494 r.fetchOnce.Do(func() { r.fetch(ctx) })
495 if r.fetchErr != nil {
496 return nil, r.fetchErr
497 }
498 info, err := r.statLocal(ctx, rev)
499 if err != nil {
500 return info, err
501 }
502 if !revOK {
503 info.Version = info.Name
504 }
505 return info, nil
506 }
507
508 func (r *vcsRepo) fetch(ctx context.Context) {
509 if len(r.cmd.fetch) > 0 {
510 release, err := base.AcquireNet()
511 if err != nil {
512 r.fetchErr = err
513 return
514 }
515 _, r.fetchErr = Run(ctx, r.dir, r.cmd.fetch)
516 release()
517 r.fetched.Store(true)
518 }
519 }
520
521 func (r *vcsRepo) statLocal(ctx context.Context, rev string) (*RevInfo, error) {
522 out, err := Run(ctx, r.dir, r.cmd.statLocal(rev, r.remote))
523 if err != nil {
524 info := &RevInfo{Origin: r.repoSumOrigin(ctx)}
525 return info, &UnknownRevisionError{Rev: rev}
526 }
527 info, err := r.cmd.parseStat(rev, string(out))
528 if err != nil {
529 return nil, err
530 }
531 if info.Origin == nil {
532 info.Origin = new(Origin)
533 }
534 info.Origin.VCS = r.cmd.vcs
535 info.Origin.URL = r.remote
536 info.Origin.Ref = rev
537 if strings.HasPrefix(info.Name, rev) && len(rev) >= 12 {
538 info.Origin.Ref = ""
539 }
540 return info, nil
541 }
542
543 func (r *vcsRepo) Latest(ctx context.Context) (*RevInfo, error) {
544 return r.Stat(ctx, "latest")
545 }
546
547 func (r *vcsRepo) ReadFile(ctx context.Context, rev, file string, maxSize int64) ([]byte, error) {
548 if rev == "latest" {
549 rev = r.cmd.latest
550 }
551 _, err := r.Stat(ctx, rev)
552 if err != nil {
553 return nil, err
554 }
555
556
557 unlock, err := r.mu.Lock()
558 if err != nil {
559 return nil, err
560 }
561 defer unlock()
562
563 out, err := Run(ctx, r.dir, r.cmd.readFile(rev, file, r.remote))
564 if err != nil {
565 return nil, fs.ErrNotExist
566 }
567 return out, nil
568 }
569
570 func (r *vcsRepo) RecentTag(ctx context.Context, rev, prefix string, allowed func(string) bool) (tag string, err error) {
571
572
573 unlock, err := r.mu.Lock()
574 if err != nil {
575 return "", err
576 }
577
578 if r.cmd.recentTags == nil {
579 unlock()
580 return "", vcsErrorf("vcs %s: RecentTag: %w", r.cmd.vcs, errors.ErrUnsupported)
581 }
582 out, err := Run(ctx, r.dir, r.cmd.recentTags(rev))
583 unlock()
584 if err != nil {
585 return "", err
586 }
587
588 highest := ""
589 for _, tag := range strings.Fields(string(out)) {
590 if !strings.HasPrefix(tag, prefix) || !allowed(tag) {
591 continue
592 }
593 semtag := tag[len(prefix):]
594 if semver.Compare(semtag, highest) > 0 {
595 highest = semtag
596 }
597 }
598 if highest != "" {
599 return prefix + highest, nil
600 }
601 return "", nil
602 }
603
604 func (r *vcsRepo) DescendsFrom(ctx context.Context, rev, tag string) (bool, error) {
605 unlock, err := r.mu.Lock()
606 if err != nil {
607 return false, err
608 }
609 defer unlock()
610
611 if r.cmd.descendsFrom == nil {
612 return false, vcsErrorf("vcs %s: DescendsFrom: %w", r.cmd.vcs, errors.ErrUnsupported)
613 }
614
615 out, err := Run(ctx, r.dir, r.cmd.descendsFrom(rev, tag))
616 if err != nil {
617 return false, err
618 }
619 return strings.TrimSpace(string(out)) != "", nil
620 }
621
622 func (r *vcsRepo) ReadZip(ctx context.Context, rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
623 if r.cmd.readZip == nil && r.cmd.doReadZip == nil {
624 return nil, vcsErrorf("vcs %s: ReadZip: %w", r.cmd.vcs, errors.ErrUnsupported)
625 }
626
627 if rev == "latest" {
628 rev = r.cmd.latest
629 }
630 _, err = r.Stat(ctx, rev)
631 if err != nil {
632 return nil, err
633 }
634
635 unlock, err := r.mu.Lock()
636 if err != nil {
637 return nil, err
638 }
639 defer unlock()
640
641 f, err := os.CreateTemp("", "go-readzip-*.zip")
642 if err != nil {
643 return nil, err
644 }
645 if r.cmd.doReadZip != nil {
646 lw := &limitedWriter{
647 W: f,
648 N: maxSize,
649 ErrLimitReached: errors.New("ReadZip: encoded file exceeds allowed size"),
650 }
651 err = r.cmd.doReadZip(ctx, lw, r.dir, rev, subdir, r.remote)
652 if err == nil {
653 _, err = f.Seek(0, io.SeekStart)
654 }
655 } else if r.cmd.vcs == "fossil" {
656
657
658
659
660
661 args := r.cmd.readZip(rev, subdir, r.remote, filepath.Base(f.Name()))
662 for i := range args {
663 if args[i] == ".fossil" {
664 args[i] = filepath.Join(r.dir, ".fossil")
665 }
666 }
667 _, err = Run(ctx, filepath.Dir(f.Name()), args)
668 } else {
669 _, err = Run(ctx, r.dir, r.cmd.readZip(rev, subdir, r.remote, f.Name()))
670 }
671 if err != nil {
672 f.Close()
673 os.Remove(f.Name())
674 return nil, err
675 }
676 return &deleteCloser{f}, nil
677 }
678
679
680 type deleteCloser struct {
681 *os.File
682 }
683
684 func (d *deleteCloser) Close() error {
685 defer os.Remove(d.File.Name())
686 return d.File.Close()
687 }
688
689 func hgAddRemote(ctx context.Context, r *vcsRepo) error {
690
691 return os.WriteFile(filepath.Join(r.dir, ".hg/hgrc"), []byte(fmt.Sprintf("[paths]\ndefault = %s\n", r.remote)), 0666)
692 }
693
694 func hgParseStat(rev, out string) (*RevInfo, error) {
695 f := strings.Fields(out)
696 if len(f) < 3 {
697 return nil, vcsErrorf("unexpected response from hg log: %q", out)
698 }
699 hash := f[0]
700 version := rev
701 if strings.HasPrefix(hash, version) {
702 version = hash
703 }
704 t, err := strconv.ParseInt(f[1], 10, 64)
705 if err != nil {
706 return nil, vcsErrorf("invalid time from hg log: %q", out)
707 }
708
709 var tags []string
710 for _, tag := range f[3:] {
711 if tag != "tip" {
712 tags = append(tags, tag)
713 }
714 }
715 sort.Strings(tags)
716
717 info := &RevInfo{
718 Origin: &Origin{Hash: hash},
719 Name: hash,
720 Short: ShortenSHA1(hash),
721 Time: time.Unix(t, 0).UTC(),
722 Version: version,
723 Tags: tags,
724 }
725 return info, nil
726 }
727
728 func bzrParseStat(rev, out string) (*RevInfo, error) {
729 var revno int64
730 var tm time.Time
731 var tags []string
732 for line := range strings.SplitSeq(out, "\n") {
733 if line == "" || line[0] == ' ' || line[0] == '\t' {
734
735 break
736 }
737 if line[0] == '-' {
738 continue
739 }
740 before, after, found := strings.Cut(line, ":")
741 if !found {
742
743 break
744 }
745 key, val := before, strings.TrimSpace(after)
746 switch key {
747 case "revno":
748 if j := strings.Index(val, " "); j >= 0 {
749 val = val[:j]
750 }
751 i, err := strconv.ParseInt(val, 10, 64)
752 if err != nil {
753 return nil, vcsErrorf("unexpected revno from bzr log: %q", line)
754 }
755 revno = i
756 case "timestamp":
757 j := strings.Index(val, " ")
758 if j < 0 {
759 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
760 }
761 t, err := time.Parse("2006-01-02 15:04:05 -0700", val[j+1:])
762 if err != nil {
763 return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
764 }
765 tm = t.UTC()
766 case "tags":
767 tags = strings.Split(val, ", ")
768 }
769 }
770 if revno == 0 || tm.IsZero() {
771 return nil, vcsErrorf("unexpected response from bzr log: %q", out)
772 }
773
774 info := &RevInfo{
775 Name: strconv.FormatInt(revno, 10),
776 Short: fmt.Sprintf("%012d", revno),
777 Time: tm,
778 Version: rev,
779 Tags: tags,
780 }
781 return info, nil
782 }
783
784 func fossilParseStat(rev, out string) (*RevInfo, error) {
785 for line := range strings.SplitSeq(out, "\n") {
786 if strings.HasPrefix(line, "uuid:") || strings.HasPrefix(line, "hash:") {
787 f := strings.Fields(line)
788 if len(f) != 5 || len(f[1]) != 40 || f[4] != "UTC" {
789 return nil, vcsErrorf("unexpected response from fossil info: %q", line)
790 }
791 t, err := time.Parse(time.DateTime, f[2]+" "+f[3])
792 if err != nil {
793 return nil, vcsErrorf("unexpected response from fossil info: %q", line)
794 }
795 hash := f[1]
796 version := rev
797 if strings.HasPrefix(hash, version) {
798 version = hash
799 }
800 info := &RevInfo{
801 Origin: &Origin{Hash: hash},
802 Name: hash,
803 Short: ShortenSHA1(hash),
804 Time: t,
805 Version: version,
806 }
807 return info, nil
808 }
809 }
810 return nil, vcsErrorf("unexpected response from fossil info: %q", out)
811 }
812
813 type limitedWriter struct {
814 W io.Writer
815 N int64
816 ErrLimitReached error
817 }
818
819 func (l *limitedWriter) Write(p []byte) (n int, err error) {
820 if l.N > 0 {
821 max := len(p)
822 if l.N < int64(max) {
823 max = int(l.N)
824 }
825 n, err = l.W.Write(p[:max])
826 l.N -= int64(n)
827 if err != nil || n >= len(p) {
828 return n, err
829 }
830 }
831
832 return n, l.ErrLimitReached
833 }
834
View as plain text