1
2
3
4
5 package codehost
6
7 import (
8 "bytes"
9 "errors"
10 "fmt"
11 exec "internal/execabs"
12 "io"
13 "io/fs"
14 "net/url"
15 "os"
16 "path/filepath"
17 "sort"
18 "strconv"
19 "strings"
20 "sync"
21 "time"
22
23 "cmd/go/internal/lockedfile"
24 "cmd/go/internal/par"
25 "cmd/go/internal/web"
26
27 "golang.org/x/mod/semver"
28 )
29
30
31
32 func LocalGitRepo(remote string) (Repo, error) {
33 return newGitRepoCached(remote, true)
34 }
35
36
37
38 type notExistError struct {
39 err error
40 }
41
42 func (e notExistError) Error() string { return e.err.Error() }
43 func (notExistError) Is(err error) bool { return err == fs.ErrNotExist }
44
45 const gitWorkDirType = "git3"
46
47 var gitRepoCache par.Cache
48
49 func newGitRepoCached(remote string, localOK bool) (Repo, error) {
50 type key struct {
51 remote string
52 localOK bool
53 }
54 type cached struct {
55 repo Repo
56 err error
57 }
58
59 c := gitRepoCache.Do(key{remote, localOK}, func() any {
60 repo, err := newGitRepo(remote, localOK)
61 return cached{repo, err}
62 }).(cached)
63
64 return c.repo, c.err
65 }
66
67 func newGitRepo(remote string, localOK bool) (Repo, error) {
68 r := &gitRepo{remote: remote}
69 if strings.Contains(remote, "://") {
70
71 var err error
72 r.dir, r.mu.Path, err = WorkDir(gitWorkDirType, r.remote)
73 if err != nil {
74 return nil, err
75 }
76
77 unlock, err := r.mu.Lock()
78 if err != nil {
79 return nil, err
80 }
81 defer unlock()
82
83 if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
84 if _, err := Run(r.dir, "git", "init", "--bare"); err != nil {
85 os.RemoveAll(r.dir)
86 return nil, err
87 }
88
89
90
91
92 if _, err := Run(r.dir, "git", "remote", "add", "origin", "--", r.remote); err != nil {
93 os.RemoveAll(r.dir)
94 return nil, err
95 }
96 }
97 r.remoteURL = r.remote
98 r.remote = "origin"
99 } else {
100
101
102
103
104 if strings.Contains(remote, ":") {
105 return nil, fmt.Errorf("git remote cannot use host:path syntax")
106 }
107 if !localOK {
108 return nil, fmt.Errorf("git remote must not be local directory")
109 }
110 r.local = true
111 info, err := os.Stat(remote)
112 if err != nil {
113 return nil, err
114 }
115 if !info.IsDir() {
116 return nil, fmt.Errorf("%s exists but is not a directory", remote)
117 }
118 r.dir = remote
119 r.mu.Path = r.dir + ".lock"
120 }
121 return r, nil
122 }
123
124 type gitRepo struct {
125 remote, remoteURL string
126 local bool
127 dir string
128
129 mu lockedfile.Mutex
130
131 fetchLevel int
132
133 statCache par.Cache
134
135 refsOnce sync.Once
136
137
138 refs map[string]string
139 refsErr error
140
141 localTagsOnce sync.Once
142 localTags map[string]bool
143 }
144
145 const (
146
147 fetchNone = iota
148 fetchSome
149 fetchAll
150 )
151
152
153
154
155 func (r *gitRepo) loadLocalTags() {
156
157
158
159 out, err := Run(r.dir, "git", "tag", "-l")
160 if err != nil {
161 return
162 }
163
164 r.localTags = make(map[string]bool)
165 for _, line := range strings.Split(string(out), "\n") {
166 if line != "" {
167 r.localTags[line] = true
168 }
169 }
170 }
171
172
173
174 func (r *gitRepo) loadRefs() (map[string]string, error) {
175 r.refsOnce.Do(func() {
176
177
178
179 out, gitErr := Run(r.dir, "git", "ls-remote", "-q", r.remote)
180 if gitErr != nil {
181 if rerr, ok := gitErr.(*RunError); ok {
182 if bytes.Contains(rerr.Stderr, []byte("fatal: could not read Username")) {
183 rerr.HelpText = "Confirm the import path was entered correctly.\nIf this is a private repository, see https://golang.org/doc/faq#git_https for additional information."
184 }
185 }
186
187
188
189
190
191 if u, err := url.Parse(r.remoteURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
192 if _, err := web.GetBytes(u); errors.Is(err, fs.ErrNotExist) {
193 gitErr = notExistError{gitErr}
194 }
195 }
196
197 r.refsErr = gitErr
198 return
199 }
200
201 refs := make(map[string]string)
202 for _, line := range strings.Split(string(out), "\n") {
203 f := strings.Fields(line)
204 if len(f) != 2 {
205 continue
206 }
207 if f[1] == "HEAD" || strings.HasPrefix(f[1], "refs/heads/") || strings.HasPrefix(f[1], "refs/tags/") {
208 refs[f[1]] = f[0]
209 }
210 }
211 for ref, hash := range refs {
212 if strings.HasSuffix(ref, "^{}") {
213 refs[strings.TrimSuffix(ref, "^{}")] = hash
214 delete(refs, ref)
215 }
216 }
217 r.refs = refs
218 })
219 return r.refs, r.refsErr
220 }
221
222 func (r *gitRepo) Tags(prefix string) ([]string, error) {
223 refs, err := r.loadRefs()
224 if err != nil {
225 return nil, err
226 }
227
228 tags := []string{}
229 for ref := range refs {
230 if !strings.HasPrefix(ref, "refs/tags/") {
231 continue
232 }
233 tag := ref[len("refs/tags/"):]
234 if !strings.HasPrefix(tag, prefix) {
235 continue
236 }
237 tags = append(tags, tag)
238 }
239 sort.Strings(tags)
240 return tags, nil
241 }
242
243 func (r *gitRepo) Latest() (*RevInfo, error) {
244 refs, err := r.loadRefs()
245 if err != nil {
246 return nil, err
247 }
248 if refs["HEAD"] == "" {
249 return nil, ErrNoCommits
250 }
251 return r.Stat(refs["HEAD"])
252 }
253
254
255
256
257
258 func (r *gitRepo) findRef(hash string) (ref string, ok bool) {
259 refs, err := r.loadRefs()
260 if err != nil {
261 return "", false
262 }
263 for ref, h := range refs {
264 if h == hash {
265 return ref, true
266 }
267 }
268 return "", false
269 }
270
271
272
273
274
275
276
277 const minHashDigits = 7
278
279
280
281 func (r *gitRepo) stat(rev string) (*RevInfo, error) {
282 if r.local {
283 return r.statLocal(rev, rev)
284 }
285
286
287 didStatLocal := false
288 if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) {
289 if info, err := r.statLocal(rev, rev); err == nil {
290 return info, nil
291 }
292 didStatLocal = true
293 }
294
295
296
297 r.localTagsOnce.Do(r.loadLocalTags)
298 if r.localTags[rev] {
299 return r.statLocal(rev, "refs/tags/"+rev)
300 }
301
302
303
304
305 refs, err := r.loadRefs()
306 if err != nil {
307 return nil, err
308 }
309
310
311
312 var ref, hash string
313 if refs["refs/tags/"+rev] != "" {
314 ref = "refs/tags/" + rev
315 hash = refs[ref]
316
317 } else if refs["refs/heads/"+rev] != "" {
318 ref = "refs/heads/" + rev
319 hash = refs[ref]
320 rev = hash
321 } else if rev == "HEAD" && refs["HEAD"] != "" {
322 ref = "HEAD"
323 hash = refs[ref]
324 rev = hash
325 } else if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) {
326
327
328 prefix := rev
329
330 for k, h := range refs {
331 if strings.HasPrefix(h, prefix) {
332 if hash != "" && hash != h {
333
334
335 return nil, fmt.Errorf("ambiguous revision %s", rev)
336 }
337 if ref == "" || ref > k {
338 ref = k
339 }
340 rev = h
341 hash = h
342 }
343 }
344 if hash == "" && len(rev) == 40 {
345 hash = rev
346 }
347 } else {
348 return nil, &UnknownRevisionError{Rev: rev}
349 }
350
351
352 unlock, err := r.mu.Lock()
353 if err != nil {
354 return nil, err
355 }
356 defer unlock()
357
358
359
360
361
362 if !didStatLocal {
363 if info, err := r.statLocal(rev, hash); err == nil {
364 if strings.HasPrefix(ref, "refs/tags/") {
365
366 Run(r.dir, "git", "tag", strings.TrimPrefix(ref, "refs/tags/"), hash)
367 }
368 return info, nil
369 }
370 }
371
372
373
374
375
376
377
378
379 if r.fetchLevel <= fetchSome && ref != "" && hash != "" && !r.local {
380 r.fetchLevel = fetchSome
381 var refspec string
382 if ref != "" && ref != "HEAD" {
383
384
385
386
387 refspec = ref + ":" + ref
388 } else {
389
390
391
392
393
394 ref = hash
395 refspec = hash + ":refs/dummy"
396 }
397 _, err := Run(r.dir, "git", "fetch", "-f", "--depth=1", r.remote, refspec)
398 if err == nil {
399 return r.statLocal(rev, ref)
400 }
401
402
403
404 }
405
406
407
408 if err := r.fetchRefsLocked(); err != nil {
409 return nil, err
410 }
411
412 return r.statLocal(rev, rev)
413 }
414
415
416
417
418
419
420
421
422
423
424 func (r *gitRepo) fetchRefsLocked() error {
425 if r.fetchLevel < fetchAll {
426
427
428
429
430
431
432 if _, err := Run(r.dir, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
433 return err
434 }
435
436 if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil {
437 if _, err := Run(r.dir, "git", "fetch", "--unshallow", "-f", r.remote); err != nil {
438 return err
439 }
440 }
441
442 r.fetchLevel = fetchAll
443 }
444 return nil
445 }
446
447
448
449 func (r *gitRepo) statLocal(version, rev string) (*RevInfo, error) {
450 out, err := Run(r.dir, "git", "-c", "log.showsignature=false", "log", "-n1", "--format=format:%H %ct %D", rev, "--")
451 if err != nil {
452 return nil, &UnknownRevisionError{Rev: rev}
453 }
454 f := strings.Fields(string(out))
455 if len(f) < 2 {
456 return nil, fmt.Errorf("unexpected response from git log: %q", out)
457 }
458 hash := f[0]
459 if strings.HasPrefix(hash, version) {
460 version = hash
461 }
462 t, err := strconv.ParseInt(f[1], 10, 64)
463 if err != nil {
464 return nil, fmt.Errorf("invalid time from git log: %q", out)
465 }
466
467 info := &RevInfo{
468 Name: hash,
469 Short: ShortenSHA1(hash),
470 Time: time.Unix(t, 0).UTC(),
471 Version: hash,
472 }
473
474
475
476 for i := 2; i < len(f); i++ {
477 if f[i] == "tag:" {
478 i++
479 if i < len(f) {
480 info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ","))
481 }
482 }
483 }
484 sort.Strings(info.Tags)
485
486
487
488
489 for _, tag := range info.Tags {
490 if version == tag {
491 info.Version = version
492 }
493 }
494
495 return info, nil
496 }
497
498 func (r *gitRepo) Stat(rev string) (*RevInfo, error) {
499 if rev == "latest" {
500 return r.Latest()
501 }
502 type cached struct {
503 info *RevInfo
504 err error
505 }
506 c := r.statCache.Do(rev, func() any {
507 info, err := r.stat(rev)
508 return cached{info, err}
509 }).(cached)
510 return c.info, c.err
511 }
512
513 func (r *gitRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
514
515 info, err := r.Stat(rev)
516 if err != nil {
517 return nil, err
518 }
519 out, err := Run(r.dir, "git", "cat-file", "blob", info.Name+":"+file)
520 if err != nil {
521 return nil, fs.ErrNotExist
522 }
523 return out, nil
524 }
525
526 func (r *gitRepo) RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error) {
527 info, err := r.Stat(rev)
528 if err != nil {
529 return "", err
530 }
531 rev = info.Name
532
533
534
535 describe := func() (definitive bool) {
536 var out []byte
537 out, err = Run(r.dir, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev)
538 if err != nil {
539 return true
540 }
541
542
543 var highest string
544 for _, line := range strings.Split(string(out), "\n") {
545 line = strings.TrimSpace(line)
546
547
548 if !strings.HasPrefix(line, "refs/tags/") {
549 continue
550 }
551 line = line[len("refs/tags/"):]
552
553 if !strings.HasPrefix(line, prefix) {
554 continue
555 }
556
557 semtag := line[len(prefix):]
558
559
560
561
562 if c := semver.Canonical(semtag); c == "" || !strings.HasPrefix(semtag, c) || !allowed(semtag) {
563 continue
564 }
565 if semver.Compare(semtag, highest) > 0 {
566 highest = semtag
567 }
568 }
569
570 if highest != "" {
571 tag = prefix + highest
572 }
573
574 return tag != "" && !AllHex(tag)
575 }
576
577 if describe() {
578 return tag, err
579 }
580
581
582
583 tags, err := r.Tags(prefix + "v")
584 if err != nil {
585 return "", err
586 }
587 if len(tags) == 0 {
588 return "", nil
589 }
590
591
592
593
594 unlock, err := r.mu.Lock()
595 if err != nil {
596 return "", err
597 }
598 defer unlock()
599
600 if err := r.fetchRefsLocked(); err != nil {
601 return "", err
602 }
603
604
605
606
607
608
609
610
611
612
613
614 describe()
615 return tag, err
616 }
617
618 func (r *gitRepo) DescendsFrom(rev, tag string) (bool, error) {
619
620
621
622
623
624
625 _, err := Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
626
627
628
629
630
631
632 if err == nil {
633 return true, nil
634 }
635
636
637 tags, err := r.Tags(tag)
638 if err != nil {
639 return false, err
640 }
641 if len(tags) == 0 {
642 return false, nil
643 }
644
645
646
647
648 if _, err = r.stat(rev); err != nil {
649 return false, err
650 }
651
652
653 unlock, err := r.mu.Lock()
654 if err != nil {
655 return false, err
656 }
657 defer unlock()
658
659 if r.fetchLevel < fetchAll {
660
661
662
663
664 if err := r.fetchRefsLocked(); err != nil {
665 return false, err
666 }
667 }
668
669 _, err = Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
670 if err == nil {
671 return true, nil
672 }
673 if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
674 return false, nil
675 }
676 return false, err
677 }
678
679 func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
680
681 args := []string{}
682 if subdir != "" {
683 args = append(args, "--", subdir)
684 }
685 info, err := r.Stat(rev)
686 if err != nil {
687 return nil, err
688 }
689
690 unlock, err := r.mu.Lock()
691 if err != nil {
692 return nil, err
693 }
694 defer unlock()
695
696 if err := ensureGitAttributes(r.dir); err != nil {
697 return nil, err
698 }
699
700
701
702
703
704
705 archive, err := Run(r.dir, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
706 if err != nil {
707 if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) {
708 return nil, fs.ErrNotExist
709 }
710 return nil, err
711 }
712
713 return io.NopCloser(bytes.NewReader(archive)), nil
714 }
715
716
717
718
719
720
721
722
723 func ensureGitAttributes(repoDir string) (err error) {
724 const attr = "\n* -export-subst -export-ignore\n"
725
726 d := repoDir + "/info"
727 p := d + "/attributes"
728
729 if err := os.MkdirAll(d, 0755); err != nil {
730 return err
731 }
732
733 f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
734 if err != nil {
735 return err
736 }
737 defer func() {
738 closeErr := f.Close()
739 if closeErr != nil {
740 err = closeErr
741 }
742 }()
743
744 b, err := io.ReadAll(f)
745 if err != nil {
746 return err
747 }
748 if !bytes.HasSuffix(b, []byte(attr)) {
749 _, err := f.WriteString(attr)
750 return err
751 }
752
753 return nil
754 }
755
View as plain text