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