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