1
2
3
4
5 package vcs
6
7 import (
8 "bytes"
9 "errors"
10 "fmt"
11 "internal/godebug"
12 "internal/lazyregexp"
13 "internal/singleflight"
14 "io/fs"
15 "log"
16 urlpkg "net/url"
17 "os"
18 "os/exec"
19 "path/filepath"
20 "regexp"
21 "strconv"
22 "strings"
23 "sync"
24 "time"
25
26 "cmd/go/internal/base"
27 "cmd/go/internal/cfg"
28 "cmd/go/internal/search"
29 "cmd/go/internal/str"
30 "cmd/go/internal/web"
31 "cmd/internal/pathcache"
32
33 "golang.org/x/mod/module"
34 )
35
36
37
38 type Cmd struct {
39 Name string
40 Cmd string
41 Env []string
42 RootNames []rootName
43
44 CreateCmd []string
45 DownloadCmd []string
46
47 TagCmd []tagCmd
48 TagLookupCmd []tagCmd
49 TagSyncCmd []string
50 TagSyncDefault []string
51
52 Scheme []string
53 PingCmd string
54
55 RemoteRepo func(v *Cmd, rootDir string) (remoteRepo string, err error)
56 ResolveRepo func(v *Cmd, rootDir, remoteRepo string) (realRepo string, err error)
57 Status func(v *Cmd, rootDir string) (Status, error)
58 }
59
60
61 type Status struct {
62 Revision string
63 CommitTime time.Time
64 Uncommitted bool
65 }
66
67 var (
68
69
70
71
72
73 VCSTestRepoURL string
74
75
76 VCSTestHosts []string
77
78
79
80 VCSTestIsLocalHost func(*urlpkg.URL) bool
81 )
82
83 var defaultSecureScheme = map[string]bool{
84 "https": true,
85 "git+ssh": true,
86 "bzr+ssh": true,
87 "svn+ssh": true,
88 "ssh": true,
89 }
90
91 func (v *Cmd) IsSecure(repo string) bool {
92 u, err := urlpkg.Parse(repo)
93 if err != nil {
94
95 return false
96 }
97 if VCSTestRepoURL != "" && web.IsLocalHost(u) {
98
99
100
101 return true
102 }
103 return v.isSecureScheme(u.Scheme)
104 }
105
106 func (v *Cmd) isSecureScheme(scheme string) bool {
107 switch v.Cmd {
108 case "git":
109
110
111
112 if allow := os.Getenv("GIT_ALLOW_PROTOCOL"); allow != "" {
113 for _, s := range strings.Split(allow, ":") {
114 if s == scheme {
115 return true
116 }
117 }
118 return false
119 }
120 }
121 return defaultSecureScheme[scheme]
122 }
123
124
125
126 type tagCmd struct {
127 cmd string
128 pattern string
129 }
130
131
132 var vcsList = []*Cmd{
133 vcsHg,
134 vcsGit,
135 vcsSvn,
136 vcsBzr,
137 vcsFossil,
138 }
139
140
141
142 var vcsMod = &Cmd{Name: "mod"}
143
144
145
146 func vcsByCmd(cmd string) *Cmd {
147 for _, vcs := range vcsList {
148 if vcs.Cmd == cmd {
149 return vcs
150 }
151 }
152 return nil
153 }
154
155
156 var vcsHg = &Cmd{
157 Name: "Mercurial",
158 Cmd: "hg",
159
160
161
162 Env: []string{"HGPLAIN=1"},
163 RootNames: []rootName{
164 {filename: ".hg", isDir: true},
165 },
166
167 CreateCmd: []string{"clone -U -- {repo} {dir}"},
168 DownloadCmd: []string{"pull"},
169
170
171
172
173
174
175 TagCmd: []tagCmd{
176 {"tags", `^(\S+)`},
177 {"branches", `^(\S+)`},
178 },
179 TagSyncCmd: []string{"update -r {tag}"},
180 TagSyncDefault: []string{"update default"},
181
182 Scheme: []string{"https", "http", "ssh"},
183 PingCmd: "identify -- {scheme}://{repo}",
184 RemoteRepo: hgRemoteRepo,
185 Status: hgStatus,
186 }
187
188 func hgRemoteRepo(vcsHg *Cmd, rootDir string) (remoteRepo string, err error) {
189 out, err := vcsHg.runOutput(rootDir, "paths default")
190 if err != nil {
191 return "", err
192 }
193 return strings.TrimSpace(string(out)), nil
194 }
195
196 func hgStatus(vcsHg *Cmd, rootDir string) (Status, error) {
197
198 out, err := vcsHg.runOutputVerboseOnly(rootDir, `log -r. -T {node}:{date|hgdate}`)
199 if err != nil {
200 return Status{}, err
201 }
202
203 var rev string
204 var commitTime time.Time
205 if len(out) > 0 {
206
207 if i := bytes.IndexByte(out, ' '); i > 0 {
208 out = out[:i]
209 }
210 rev, commitTime, err = parseRevTime(out)
211 if err != nil {
212 return Status{}, err
213 }
214 }
215
216
217 out, err = vcsHg.runOutputVerboseOnly(rootDir, "status -S")
218 if err != nil {
219 return Status{}, err
220 }
221 uncommitted := len(out) > 0
222
223 return Status{
224 Revision: rev,
225 CommitTime: commitTime,
226 Uncommitted: uncommitted,
227 }, nil
228 }
229
230
231 func parseRevTime(out []byte) (string, time.Time, error) {
232 buf := string(bytes.TrimSpace(out))
233
234 i := strings.IndexByte(buf, ':')
235 if i < 1 {
236 return "", time.Time{}, errors.New("unrecognized VCS tool output")
237 }
238 rev := buf[:i]
239
240 secs, err := strconv.ParseInt(string(buf[i+1:]), 10, 64)
241 if err != nil {
242 return "", time.Time{}, fmt.Errorf("unrecognized VCS tool output: %v", err)
243 }
244
245 return rev, time.Unix(secs, 0), nil
246 }
247
248
249 var vcsGit = &Cmd{
250 Name: "Git",
251 Cmd: "git",
252 RootNames: []rootName{
253 {filename: ".git", isDir: true},
254 },
255
256 CreateCmd: []string{"clone -- {repo} {dir}", "-go-internal-cd {dir} submodule update --init --recursive"},
257 DownloadCmd: []string{"pull --ff-only", "submodule update --init --recursive"},
258
259 TagCmd: []tagCmd{
260
261
262 {"show-ref", `(?:tags|origin)/(\S+)$`},
263 },
264 TagLookupCmd: []tagCmd{
265 {"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
266 },
267 TagSyncCmd: []string{"checkout {tag}", "submodule update --init --recursive"},
268
269
270
271
272
273 TagSyncDefault: []string{"submodule update --init --recursive"},
274
275 Scheme: []string{"git", "https", "http", "git+ssh", "ssh"},
276
277
278
279
280
281 PingCmd: "ls-remote {scheme}://{repo}",
282
283 RemoteRepo: gitRemoteRepo,
284 Status: gitStatus,
285 }
286
287
288
289 var scpSyntaxRe = lazyregexp.New(`^(\w+)@([\w.-]+):(.*)$`)
290
291 func gitRemoteRepo(vcsGit *Cmd, rootDir string) (remoteRepo string, err error) {
292 const cmd = "config remote.origin.url"
293 outb, err := vcsGit.run1(rootDir, cmd, nil, false)
294 if err != nil {
295
296
297 if outb != nil && len(outb) == 0 {
298 return "", errors.New("remote origin not found")
299 }
300 return "", err
301 }
302 out := strings.TrimSpace(string(outb))
303
304 var repoURL *urlpkg.URL
305 if m := scpSyntaxRe.FindStringSubmatch(out); m != nil {
306
307
308
309 repoURL = &urlpkg.URL{
310 Scheme: "ssh",
311 User: urlpkg.User(m[1]),
312 Host: m[2],
313 Path: m[3],
314 }
315 } else {
316 repoURL, err = urlpkg.Parse(out)
317 if err != nil {
318 return "", err
319 }
320 }
321
322
323
324
325 for _, s := range vcsGit.Scheme {
326 if repoURL.Scheme == s {
327 return repoURL.String(), nil
328 }
329 }
330 return "", errors.New("unable to parse output of git " + cmd)
331 }
332
333 func gitStatus(vcsGit *Cmd, rootDir string) (Status, error) {
334 out, err := vcsGit.runOutputVerboseOnly(rootDir, "status --porcelain")
335 if err != nil {
336 return Status{}, err
337 }
338 uncommitted := len(out) > 0
339
340
341
342
343 var rev string
344 var commitTime time.Time
345 out, err = vcsGit.runOutputVerboseOnly(rootDir, "-c log.showsignature=false log -1 --format=%H:%ct")
346 if err != nil && !uncommitted {
347 return Status{}, err
348 } else if err == nil {
349 rev, commitTime, err = parseRevTime(out)
350 if err != nil {
351 return Status{}, err
352 }
353 }
354
355 return Status{
356 Revision: rev,
357 CommitTime: commitTime,
358 Uncommitted: uncommitted,
359 }, nil
360 }
361
362
363 var vcsBzr = &Cmd{
364 Name: "Bazaar",
365 Cmd: "bzr",
366 RootNames: []rootName{
367 {filename: ".bzr", isDir: true},
368 },
369
370 CreateCmd: []string{"branch -- {repo} {dir}"},
371
372
373
374 DownloadCmd: []string{"pull --overwrite"},
375
376 TagCmd: []tagCmd{{"tags", `^(\S+)`}},
377 TagSyncCmd: []string{"update -r {tag}"},
378 TagSyncDefault: []string{"update -r revno:-1"},
379
380 Scheme: []string{"https", "http", "bzr", "bzr+ssh"},
381 PingCmd: "info -- {scheme}://{repo}",
382 RemoteRepo: bzrRemoteRepo,
383 ResolveRepo: bzrResolveRepo,
384 Status: bzrStatus,
385 }
386
387 func bzrRemoteRepo(vcsBzr *Cmd, rootDir string) (remoteRepo string, err error) {
388 outb, err := vcsBzr.runOutput(rootDir, "config parent_location")
389 if err != nil {
390 return "", err
391 }
392 return strings.TrimSpace(string(outb)), nil
393 }
394
395 func bzrResolveRepo(vcsBzr *Cmd, rootDir, remoteRepo string) (realRepo string, err error) {
396 outb, err := vcsBzr.runOutput(rootDir, "info "+remoteRepo)
397 if err != nil {
398 return "", err
399 }
400 out := string(outb)
401
402
403
404
405
406
407 found := false
408 for _, prefix := range []string{"\n branch root: ", "\n repository branch: "} {
409 i := strings.Index(out, prefix)
410 if i >= 0 {
411 out = out[i+len(prefix):]
412 found = true
413 break
414 }
415 }
416 if !found {
417 return "", fmt.Errorf("unable to parse output of bzr info")
418 }
419
420 i := strings.Index(out, "\n")
421 if i < 0 {
422 return "", fmt.Errorf("unable to parse output of bzr info")
423 }
424 out = out[:i]
425 return strings.TrimSpace(out), nil
426 }
427
428 func bzrStatus(vcsBzr *Cmd, rootDir string) (Status, error) {
429 outb, err := vcsBzr.runOutputVerboseOnly(rootDir, "version-info")
430 if err != nil {
431 return Status{}, err
432 }
433 out := string(outb)
434
435
436
437
438
439
440 var rev string
441 var commitTime time.Time
442
443 for _, line := range strings.Split(out, "\n") {
444 i := strings.IndexByte(line, ':')
445 if i < 0 {
446 continue
447 }
448 key := line[:i]
449 value := strings.TrimSpace(line[i+1:])
450
451 switch key {
452 case "revision-id":
453 rev = value
454 case "date":
455 var err error
456 commitTime, err = time.Parse("2006-01-02 15:04:05 -0700", value)
457 if err != nil {
458 return Status{}, errors.New("unable to parse output of bzr version-info")
459 }
460 }
461 }
462
463 outb, err = vcsBzr.runOutputVerboseOnly(rootDir, "status")
464 if err != nil {
465 return Status{}, err
466 }
467
468
469 if bytes.HasPrefix(outb, []byte("working tree is out of date")) {
470 i := bytes.IndexByte(outb, '\n')
471 if i < 0 {
472 i = len(outb)
473 }
474 outb = outb[:i]
475 }
476 uncommitted := len(outb) > 0
477
478 return Status{
479 Revision: rev,
480 CommitTime: commitTime,
481 Uncommitted: uncommitted,
482 }, nil
483 }
484
485
486 var vcsSvn = &Cmd{
487 Name: "Subversion",
488 Cmd: "svn",
489 RootNames: []rootName{
490 {filename: ".svn", isDir: true},
491 },
492
493 CreateCmd: []string{"checkout -- {repo} {dir}"},
494 DownloadCmd: []string{"update"},
495
496
497
498
499 Scheme: []string{"https", "http", "svn", "svn+ssh"},
500 PingCmd: "info -- {scheme}://{repo}",
501 RemoteRepo: svnRemoteRepo,
502 }
503
504 func svnRemoteRepo(vcsSvn *Cmd, rootDir string) (remoteRepo string, err error) {
505 outb, err := vcsSvn.runOutput(rootDir, "info")
506 if err != nil {
507 return "", err
508 }
509 out := string(outb)
510
511
512
513
514
515
516
517
518
519
520
521 i := strings.Index(out, "\nURL: ")
522 if i < 0 {
523 return "", fmt.Errorf("unable to parse output of svn info")
524 }
525 out = out[i+len("\nURL: "):]
526 i = strings.Index(out, "\n")
527 if i < 0 {
528 return "", fmt.Errorf("unable to parse output of svn info")
529 }
530 out = out[:i]
531 return strings.TrimSpace(out), nil
532 }
533
534
535
536 const fossilRepoName = ".fossil"
537
538
539 var vcsFossil = &Cmd{
540 Name: "Fossil",
541 Cmd: "fossil",
542 RootNames: []rootName{
543 {filename: ".fslckout", isDir: false},
544 {filename: "_FOSSIL_", isDir: false},
545 },
546
547 CreateCmd: []string{"-go-internal-mkdir {dir} clone -- {repo} " + filepath.Join("{dir}", fossilRepoName), "-go-internal-cd {dir} open .fossil"},
548 DownloadCmd: []string{"up"},
549
550 TagCmd: []tagCmd{{"tag ls", `(.*)`}},
551 TagSyncCmd: []string{"up tag:{tag}"},
552 TagSyncDefault: []string{"up trunk"},
553
554 Scheme: []string{"https", "http"},
555 RemoteRepo: fossilRemoteRepo,
556 Status: fossilStatus,
557 }
558
559 func fossilRemoteRepo(vcsFossil *Cmd, rootDir string) (remoteRepo string, err error) {
560 out, err := vcsFossil.runOutput(rootDir, "remote-url")
561 if err != nil {
562 return "", err
563 }
564 return strings.TrimSpace(string(out)), nil
565 }
566
567 var errFossilInfo = errors.New("unable to parse output of fossil info")
568
569 func fossilStatus(vcsFossil *Cmd, rootDir string) (Status, error) {
570 outb, err := vcsFossil.runOutputVerboseOnly(rootDir, "info")
571 if err != nil {
572 return Status{}, err
573 }
574 out := string(outb)
575
576
577
578
579
580
581
582
583 const prefix = "\ncheckout:"
584 const suffix = " UTC"
585 i := strings.Index(out, prefix)
586 if i < 0 {
587 return Status{}, errFossilInfo
588 }
589 checkout := out[i+len(prefix):]
590 i = strings.Index(checkout, suffix)
591 if i < 0 {
592 return Status{}, errFossilInfo
593 }
594 checkout = strings.TrimSpace(checkout[:i])
595
596 i = strings.IndexByte(checkout, ' ')
597 if i < 0 {
598 return Status{}, errFossilInfo
599 }
600 rev := checkout[:i]
601
602 commitTime, err := time.ParseInLocation(time.DateTime, checkout[i+1:], time.UTC)
603 if err != nil {
604 return Status{}, fmt.Errorf("%v: %v", errFossilInfo, err)
605 }
606
607
608 outb, err = vcsFossil.runOutputVerboseOnly(rootDir, "changes --differ")
609 if err != nil {
610 return Status{}, err
611 }
612 uncommitted := len(outb) > 0
613
614 return Status{
615 Revision: rev,
616 CommitTime: commitTime,
617 Uncommitted: uncommitted,
618 }, nil
619 }
620
621 func (v *Cmd) String() string {
622 return v.Name
623 }
624
625
626
627
628
629
630
631
632 func (v *Cmd) run(dir string, cmd string, keyval ...string) error {
633 _, err := v.run1(dir, cmd, keyval, true)
634 return err
635 }
636
637
638 func (v *Cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error {
639 _, err := v.run1(dir, cmd, keyval, false)
640 return err
641 }
642
643
644 func (v *Cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) {
645 return v.run1(dir, cmd, keyval, true)
646 }
647
648
649
650 func (v *Cmd) runOutputVerboseOnly(dir string, cmd string, keyval ...string) ([]byte, error) {
651 return v.run1(dir, cmd, keyval, false)
652 }
653
654
655 func (v *Cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) {
656 m := make(map[string]string)
657 for i := 0; i < len(keyval); i += 2 {
658 m[keyval[i]] = keyval[i+1]
659 }
660 args := strings.Fields(cmdline)
661 for i, arg := range args {
662 args[i] = expand(m, arg)
663 }
664
665 if len(args) >= 2 && args[0] == "-go-internal-mkdir" {
666 var err error
667 if filepath.IsAbs(args[1]) {
668 err = os.Mkdir(args[1], fs.ModePerm)
669 } else {
670 err = os.Mkdir(filepath.Join(dir, args[1]), fs.ModePerm)
671 }
672 if err != nil {
673 return nil, err
674 }
675 args = args[2:]
676 }
677
678 if len(args) >= 2 && args[0] == "-go-internal-cd" {
679 if filepath.IsAbs(args[1]) {
680 dir = args[1]
681 } else {
682 dir = filepath.Join(dir, args[1])
683 }
684 args = args[2:]
685 }
686
687 _, err := pathcache.LookPath(v.Cmd)
688 if err != nil {
689 fmt.Fprintf(os.Stderr,
690 "go: missing %s command. See https://golang.org/s/gogetcmd\n",
691 v.Name)
692 return nil, err
693 }
694
695 cmd := exec.Command(v.Cmd, args...)
696 cmd.Dir = dir
697 if v.Env != nil {
698 cmd.Env = append(cmd.Environ(), v.Env...)
699 }
700 if cfg.BuildX {
701 fmt.Fprintf(os.Stderr, "cd %s\n", dir)
702 fmt.Fprintf(os.Stderr, "%s %s\n", v.Cmd, strings.Join(args, " "))
703 }
704 out, err := cmd.Output()
705 if err != nil {
706 if verbose || cfg.BuildV {
707 fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.Cmd, strings.Join(args, " "))
708 if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
709 os.Stderr.Write(ee.Stderr)
710 } else {
711 fmt.Fprintln(os.Stderr, err.Error())
712 }
713 }
714 }
715 return out, err
716 }
717
718
719 func (v *Cmd) Ping(scheme, repo string) error {
720
721
722
723
724 dir := cfg.GOMODCACHE
725 if !cfg.ModulesEnabled {
726 dir = filepath.Join(cfg.BuildContext.GOPATH, "src")
727 }
728 os.MkdirAll(dir, 0777)
729
730 release, err := base.AcquireNet()
731 if err != nil {
732 return err
733 }
734 defer release()
735
736 return v.runVerboseOnly(dir, v.PingCmd, "scheme", scheme, "repo", repo)
737 }
738
739
740
741 func (v *Cmd) Create(dir, repo string) error {
742 release, err := base.AcquireNet()
743 if err != nil {
744 return err
745 }
746 defer release()
747
748 for _, cmd := range v.CreateCmd {
749 if err := v.run(filepath.Dir(dir), cmd, "dir", dir, "repo", repo); err != nil {
750 return err
751 }
752 }
753 return nil
754 }
755
756
757 func (v *Cmd) Download(dir string) error {
758 release, err := base.AcquireNet()
759 if err != nil {
760 return err
761 }
762 defer release()
763
764 for _, cmd := range v.DownloadCmd {
765 if err := v.run(dir, cmd); err != nil {
766 return err
767 }
768 }
769 return nil
770 }
771
772
773 func (v *Cmd) Tags(dir string) ([]string, error) {
774 var tags []string
775 for _, tc := range v.TagCmd {
776 out, err := v.runOutput(dir, tc.cmd)
777 if err != nil {
778 return nil, err
779 }
780 re := regexp.MustCompile(`(?m-s)` + tc.pattern)
781 for _, m := range re.FindAllStringSubmatch(string(out), -1) {
782 tags = append(tags, m[1])
783 }
784 }
785 return tags, nil
786 }
787
788
789
790 func (v *Cmd) TagSync(dir, tag string) error {
791 if v.TagSyncCmd == nil {
792 return nil
793 }
794 if tag != "" {
795 for _, tc := range v.TagLookupCmd {
796 out, err := v.runOutput(dir, tc.cmd, "tag", tag)
797 if err != nil {
798 return err
799 }
800 re := regexp.MustCompile(`(?m-s)` + tc.pattern)
801 m := re.FindStringSubmatch(string(out))
802 if len(m) > 1 {
803 tag = m[1]
804 break
805 }
806 }
807 }
808
809 release, err := base.AcquireNet()
810 if err != nil {
811 return err
812 }
813 defer release()
814
815 if tag == "" && v.TagSyncDefault != nil {
816 for _, cmd := range v.TagSyncDefault {
817 if err := v.run(dir, cmd); err != nil {
818 return err
819 }
820 }
821 return nil
822 }
823
824 for _, cmd := range v.TagSyncCmd {
825 if err := v.run(dir, cmd, "tag", tag); err != nil {
826 return err
827 }
828 }
829 return nil
830 }
831
832
833
834 type vcsPath struct {
835 pathPrefix string
836 regexp *lazyregexp.Regexp
837 repo string
838 vcs string
839 check func(match map[string]string) error
840 schemelessRepo bool
841 }
842
843 var allowmultiplevcs = godebug.New("allowmultiplevcs")
844
845
846
847
848
849 func FromDir(dir, srcRoot string) (repoDir string, vcsCmd *Cmd, err error) {
850
851 dir = filepath.Clean(dir)
852 if srcRoot != "" {
853 srcRoot = filepath.Clean(srcRoot)
854 if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {
855 return "", nil, fmt.Errorf("directory %q is outside source root %q", dir, srcRoot)
856 }
857 }
858
859 origDir := dir
860 for len(dir) > len(srcRoot) {
861 for _, vcs := range vcsList {
862 if isVCSRoot(dir, vcs.RootNames) {
863 if vcsCmd == nil {
864
865 vcsCmd = vcs
866 repoDir = dir
867 if allowmultiplevcs.Value() == "1" {
868 allowmultiplevcs.IncNonDefault()
869 return repoDir, vcsCmd, nil
870 }
871
872
873
874
875 continue
876 }
877 if vcsCmd == vcsGit && vcs == vcsGit {
878
879
880
881 continue
882 }
883 return "", nil, fmt.Errorf("multiple VCS detected: %s in %q, and %s in %q",
884 vcsCmd.Cmd, repoDir, vcs.Cmd, dir)
885 }
886 }
887
888
889 ndir := filepath.Dir(dir)
890 if len(ndir) >= len(dir) {
891 break
892 }
893 dir = ndir
894 }
895 if vcsCmd == nil {
896 return "", nil, &vcsNotFoundError{dir: origDir}
897 }
898 return repoDir, vcsCmd, nil
899 }
900
901
902
903 func isVCSRoot(dir string, rootNames []rootName) bool {
904 for _, root := range rootNames {
905 fi, err := os.Stat(filepath.Join(dir, root.filename))
906 if err == nil && fi.IsDir() == root.isDir {
907 return true
908 }
909 }
910
911 return false
912 }
913
914 type rootName struct {
915 filename string
916 isDir bool
917 }
918
919 type vcsNotFoundError struct {
920 dir string
921 }
922
923 func (e *vcsNotFoundError) Error() string {
924 return fmt.Sprintf("directory %q is not using a known version control system", e.dir)
925 }
926
927 func (e *vcsNotFoundError) Is(err error) bool {
928 return err == os.ErrNotExist
929 }
930
931
932 type govcsRule struct {
933 pattern string
934 allowed []string
935 }
936
937
938 type govcsConfig []govcsRule
939
940 func parseGOVCS(s string) (govcsConfig, error) {
941 s = strings.TrimSpace(s)
942 if s == "" {
943 return nil, nil
944 }
945 var cfg govcsConfig
946 have := make(map[string]string)
947 for _, item := range strings.Split(s, ",") {
948 item = strings.TrimSpace(item)
949 if item == "" {
950 return nil, fmt.Errorf("empty entry in GOVCS")
951 }
952 pattern, list, found := strings.Cut(item, ":")
953 if !found {
954 return nil, fmt.Errorf("malformed entry in GOVCS (missing colon): %q", item)
955 }
956 pattern, list = strings.TrimSpace(pattern), strings.TrimSpace(list)
957 if pattern == "" {
958 return nil, fmt.Errorf("empty pattern in GOVCS: %q", item)
959 }
960 if list == "" {
961 return nil, fmt.Errorf("empty VCS list in GOVCS: %q", item)
962 }
963 if search.IsRelativePath(pattern) {
964 return nil, fmt.Errorf("relative pattern not allowed in GOVCS: %q", pattern)
965 }
966 if old := have[pattern]; old != "" {
967 return nil, fmt.Errorf("unreachable pattern in GOVCS: %q after %q", item, old)
968 }
969 have[pattern] = item
970 allowed := strings.Split(list, "|")
971 for i, a := range allowed {
972 a = strings.TrimSpace(a)
973 if a == "" {
974 return nil, fmt.Errorf("empty VCS name in GOVCS: %q", item)
975 }
976 allowed[i] = a
977 }
978 cfg = append(cfg, govcsRule{pattern, allowed})
979 }
980 return cfg, nil
981 }
982
983 func (c *govcsConfig) allow(path string, private bool, vcs string) bool {
984 for _, rule := range *c {
985 match := false
986 switch rule.pattern {
987 case "private":
988 match = private
989 case "public":
990 match = !private
991 default:
992
993
994 match = module.MatchPrefixPatterns(rule.pattern, path)
995 }
996 if !match {
997 continue
998 }
999 for _, allow := range rule.allowed {
1000 if allow == vcs || allow == "all" {
1001 return true
1002 }
1003 }
1004 return false
1005 }
1006
1007
1008 return false
1009 }
1010
1011 var (
1012 govcs govcsConfig
1013 govcsErr error
1014 govcsOnce sync.Once
1015 )
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029 var defaultGOVCS = govcsConfig{
1030 {"private", []string{"all"}},
1031 {"public", []string{"git", "hg"}},
1032 }
1033
1034
1035
1036
1037
1038 func checkGOVCS(vcs *Cmd, root string) error {
1039 if vcs == vcsMod {
1040
1041
1042
1043 return nil
1044 }
1045
1046 govcsOnce.Do(func() {
1047 govcs, govcsErr = parseGOVCS(os.Getenv("GOVCS"))
1048 govcs = append(govcs, defaultGOVCS...)
1049 })
1050 if govcsErr != nil {
1051 return govcsErr
1052 }
1053
1054 private := module.MatchPrefixPatterns(cfg.GOPRIVATE, root)
1055 if !govcs.allow(root, private, vcs.Cmd) {
1056 what := "public"
1057 if private {
1058 what = "private"
1059 }
1060 return fmt.Errorf("GOVCS disallows using %s for %s %s; see 'go help vcs'", vcs.Cmd, what, root)
1061 }
1062
1063 return nil
1064 }
1065
1066
1067 type RepoRoot struct {
1068 Repo string
1069 Root string
1070 IsCustom bool
1071 VCS *Cmd
1072 }
1073
1074 func httpPrefix(s string) string {
1075 for _, prefix := range [...]string{"http:", "https:"} {
1076 if strings.HasPrefix(s, prefix) {
1077 return prefix
1078 }
1079 }
1080 return ""
1081 }
1082
1083
1084 type ModuleMode int
1085
1086 const (
1087 IgnoreMod ModuleMode = iota
1088 PreferMod
1089 )
1090
1091
1092
1093 func RepoRootForImportPath(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
1094 rr, err := repoRootFromVCSPaths(importPath, security, vcsPaths)
1095 if err == errUnknownSite {
1096 rr, err = repoRootForImportDynamic(importPath, mod, security)
1097 if err != nil {
1098 err = importErrorf(importPath, "unrecognized import path %q: %v", importPath, err)
1099 }
1100 }
1101 if err != nil {
1102 rr1, err1 := repoRootFromVCSPaths(importPath, security, vcsPathsAfterDynamic)
1103 if err1 == nil {
1104 rr = rr1
1105 err = nil
1106 }
1107 }
1108
1109
1110 if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") {
1111
1112 rr = nil
1113 err = importErrorf(importPath, "cannot expand ... in %q", importPath)
1114 }
1115 return rr, err
1116 }
1117
1118 var errUnknownSite = errors.New("dynamic lookup required to find mapping")
1119
1120
1121
1122 func repoRootFromVCSPaths(importPath string, security web.SecurityMode, vcsPaths []*vcsPath) (*RepoRoot, error) {
1123 if str.HasPathPrefix(importPath, "example.net") {
1124
1125
1126
1127
1128 return nil, fmt.Errorf("no modules on example.net")
1129 }
1130 if importPath == "rsc.io" {
1131
1132
1133
1134
1135 return nil, fmt.Errorf("rsc.io is not a module")
1136 }
1137
1138
1139 if prefix := httpPrefix(importPath); prefix != "" {
1140
1141
1142 return nil, fmt.Errorf("%q not allowed in import path", prefix+"//")
1143 }
1144 for _, srv := range vcsPaths {
1145 if !str.HasPathPrefix(importPath, srv.pathPrefix) {
1146 continue
1147 }
1148 m := srv.regexp.FindStringSubmatch(importPath)
1149 if m == nil {
1150 if srv.pathPrefix != "" {
1151 return nil, importErrorf(importPath, "invalid %s import path %q", srv.pathPrefix, importPath)
1152 }
1153 continue
1154 }
1155
1156
1157 match := map[string]string{
1158 "prefix": srv.pathPrefix + "/",
1159 "import": importPath,
1160 }
1161 for i, name := range srv.regexp.SubexpNames() {
1162 if name != "" && match[name] == "" {
1163 match[name] = m[i]
1164 }
1165 }
1166 if srv.vcs != "" {
1167 match["vcs"] = expand(match, srv.vcs)
1168 }
1169 if srv.repo != "" {
1170 match["repo"] = expand(match, srv.repo)
1171 }
1172 if srv.check != nil {
1173 if err := srv.check(match); err != nil {
1174 return nil, err
1175 }
1176 }
1177 vcs := vcsByCmd(match["vcs"])
1178 if vcs == nil {
1179 return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
1180 }
1181 if err := checkGOVCS(vcs, match["root"]); err != nil {
1182 return nil, err
1183 }
1184 var repoURL string
1185 if !srv.schemelessRepo {
1186 repoURL = match["repo"]
1187 } else {
1188 repo := match["repo"]
1189 var ok bool
1190 repoURL, ok = interceptVCSTest(repo, vcs, security)
1191 if !ok {
1192 scheme, err := func() (string, error) {
1193 for _, s := range vcs.Scheme {
1194 if security == web.SecureOnly && !vcs.isSecureScheme(s) {
1195 continue
1196 }
1197
1198
1199
1200
1201
1202 if vcs.PingCmd == "" {
1203 return s, nil
1204 }
1205 if err := vcs.Ping(s, repo); err == nil {
1206 return s, nil
1207 }
1208 }
1209 securityFrag := ""
1210 if security == web.SecureOnly {
1211 securityFrag = "secure "
1212 }
1213 return "", fmt.Errorf("no %sprotocol found for repository", securityFrag)
1214 }()
1215 if err != nil {
1216 return nil, err
1217 }
1218 repoURL = scheme + "://" + repo
1219 }
1220 }
1221 rr := &RepoRoot{
1222 Repo: repoURL,
1223 Root: match["root"],
1224 VCS: vcs,
1225 }
1226 return rr, nil
1227 }
1228 return nil, errUnknownSite
1229 }
1230
1231 func interceptVCSTest(repo string, vcs *Cmd, security web.SecurityMode) (repoURL string, ok bool) {
1232 if VCSTestRepoURL == "" {
1233 return "", false
1234 }
1235 if vcs == vcsMod {
1236
1237
1238 return "", false
1239 }
1240
1241 if scheme, path, ok := strings.Cut(repo, "://"); ok {
1242 if security == web.SecureOnly && !vcs.isSecureScheme(scheme) {
1243 return "", false
1244 }
1245 repo = path
1246 }
1247 for _, host := range VCSTestHosts {
1248 if !str.HasPathPrefix(repo, host) {
1249 continue
1250 }
1251
1252 httpURL := VCSTestRepoURL + strings.TrimPrefix(repo, host)
1253
1254 if vcs == vcsSvn {
1255
1256
1257 u, err := urlpkg.Parse(httpURL + "?vcwebsvn=1")
1258 if err != nil {
1259 panic(fmt.Sprintf("invalid vcs-test repo URL: %v", err))
1260 }
1261 svnURL, err := web.GetBytes(u)
1262 svnURL = bytes.TrimSpace(svnURL)
1263 if err == nil && len(svnURL) > 0 {
1264 return string(svnURL) + strings.TrimPrefix(repo, host), true
1265 }
1266
1267
1268
1269 }
1270
1271 return httpURL, true
1272 }
1273 return "", false
1274 }
1275
1276
1277
1278
1279
1280 func urlForImportPath(importPath string) (*urlpkg.URL, error) {
1281 slash := strings.Index(importPath, "/")
1282 if slash < 0 {
1283 slash = len(importPath)
1284 }
1285 host, path := importPath[:slash], importPath[slash:]
1286 if !strings.Contains(host, ".") {
1287 return nil, errors.New("import path does not begin with hostname")
1288 }
1289 if len(path) == 0 {
1290 path = "/"
1291 }
1292 return &urlpkg.URL{Host: host, Path: path, RawQuery: "go-get=1"}, nil
1293 }
1294
1295
1296
1297
1298
1299 func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
1300 url, err := urlForImportPath(importPath)
1301 if err != nil {
1302 return nil, err
1303 }
1304 resp, err := web.Get(security, url)
1305 if err != nil {
1306 msg := "https fetch: %v"
1307 if security == web.Insecure {
1308 msg = "http/" + msg
1309 }
1310 return nil, fmt.Errorf(msg, err)
1311 }
1312 body := resp.Body
1313 defer body.Close()
1314 imports, err := parseMetaGoImports(body, mod)
1315 if len(imports) == 0 {
1316 if respErr := resp.Err(); respErr != nil {
1317
1318
1319 return nil, respErr
1320 }
1321 }
1322 if err != nil {
1323 return nil, fmt.Errorf("parsing %s: %v", importPath, err)
1324 }
1325
1326 mmi, err := matchGoImport(imports, importPath)
1327 if err != nil {
1328 if _, ok := err.(ImportMismatchError); !ok {
1329 return nil, fmt.Errorf("parse %s: %v", url, err)
1330 }
1331 return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", resp.URL, err)
1332 }
1333 if cfg.BuildV {
1334 log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, url)
1335 }
1336
1337
1338
1339
1340
1341
1342 if mmi.Prefix != importPath {
1343 if cfg.BuildV {
1344 log.Printf("get %q: verifying non-authoritative meta tag", importPath)
1345 }
1346 var imports []metaImport
1347 url, imports, err = metaImportsForPrefix(mmi.Prefix, mod, security)
1348 if err != nil {
1349 return nil, err
1350 }
1351 metaImport2, err := matchGoImport(imports, importPath)
1352 if err != nil || mmi != metaImport2 {
1353 return nil, fmt.Errorf("%s and %s disagree about go-import for %s", resp.URL, url, mmi.Prefix)
1354 }
1355 }
1356
1357 if err := validateRepoRoot(mmi.RepoRoot); err != nil {
1358 return nil, fmt.Errorf("%s: invalid repo root %q: %v", resp.URL, mmi.RepoRoot, err)
1359 }
1360 var vcs *Cmd
1361 if mmi.VCS == "mod" {
1362 vcs = vcsMod
1363 } else {
1364 vcs = vcsByCmd(mmi.VCS)
1365 if vcs == nil {
1366 return nil, fmt.Errorf("%s: unknown vcs %q", resp.URL, mmi.VCS)
1367 }
1368 }
1369
1370 if err := checkGOVCS(vcs, mmi.Prefix); err != nil {
1371 return nil, err
1372 }
1373
1374 repoURL, ok := interceptVCSTest(mmi.RepoRoot, vcs, security)
1375 if !ok {
1376 repoURL = mmi.RepoRoot
1377 }
1378 rr := &RepoRoot{
1379 Repo: repoURL,
1380 Root: mmi.Prefix,
1381 IsCustom: true,
1382 VCS: vcs,
1383 }
1384 return rr, nil
1385 }
1386
1387
1388
1389 func validateRepoRoot(repoRoot string) error {
1390 url, err := urlpkg.Parse(repoRoot)
1391 if err != nil {
1392 return err
1393 }
1394 if url.Scheme == "" {
1395 return errors.New("no scheme")
1396 }
1397 if url.Scheme == "file" {
1398 return errors.New("file scheme disallowed")
1399 }
1400 return nil
1401 }
1402
1403 var fetchGroup singleflight.Group
1404 var (
1405 fetchCacheMu sync.Mutex
1406 fetchCache = map[string]fetchResult{}
1407 )
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417 func metaImportsForPrefix(importPrefix string, mod ModuleMode, security web.SecurityMode) (*urlpkg.URL, []metaImport, error) {
1418 setCache := func(res fetchResult) (fetchResult, error) {
1419 fetchCacheMu.Lock()
1420 defer fetchCacheMu.Unlock()
1421 fetchCache[importPrefix] = res
1422 return res, nil
1423 }
1424
1425 resi, _, _ := fetchGroup.Do(importPrefix, func() (resi any, err error) {
1426 fetchCacheMu.Lock()
1427 if res, ok := fetchCache[importPrefix]; ok {
1428 fetchCacheMu.Unlock()
1429 return res, nil
1430 }
1431 fetchCacheMu.Unlock()
1432
1433 url, err := urlForImportPath(importPrefix)
1434 if err != nil {
1435 return setCache(fetchResult{err: err})
1436 }
1437 resp, err := web.Get(security, url)
1438 if err != nil {
1439 return setCache(fetchResult{url: url, err: fmt.Errorf("fetching %s: %v", importPrefix, err)})
1440 }
1441 body := resp.Body
1442 defer body.Close()
1443 imports, err := parseMetaGoImports(body, mod)
1444 if len(imports) == 0 {
1445 if respErr := resp.Err(); respErr != nil {
1446
1447
1448 return setCache(fetchResult{url: url, err: respErr})
1449 }
1450 }
1451 if err != nil {
1452 return setCache(fetchResult{url: url, err: fmt.Errorf("parsing %s: %v", resp.URL, err)})
1453 }
1454 if len(imports) == 0 {
1455 err = fmt.Errorf("fetching %s: no go-import meta tag found in %s", importPrefix, resp.URL)
1456 }
1457 return setCache(fetchResult{url: url, imports: imports, err: err})
1458 })
1459 res := resi.(fetchResult)
1460 return res.url, res.imports, res.err
1461 }
1462
1463 type fetchResult struct {
1464 url *urlpkg.URL
1465 imports []metaImport
1466 err error
1467 }
1468
1469
1470
1471 type metaImport struct {
1472 Prefix, VCS, RepoRoot string
1473 }
1474
1475
1476
1477 type ImportMismatchError struct {
1478 importPath string
1479 mismatches []string
1480 }
1481
1482 func (m ImportMismatchError) Error() string {
1483 formattedStrings := make([]string, len(m.mismatches))
1484 for i, pre := range m.mismatches {
1485 formattedStrings[i] = fmt.Sprintf("meta tag %s did not match import path %s", pre, m.importPath)
1486 }
1487 return strings.Join(formattedStrings, ", ")
1488 }
1489
1490
1491
1492
1493 func matchGoImport(imports []metaImport, importPath string) (metaImport, error) {
1494 match := -1
1495
1496 errImportMismatch := ImportMismatchError{importPath: importPath}
1497 for i, im := range imports {
1498 if !str.HasPathPrefix(importPath, im.Prefix) {
1499 errImportMismatch.mismatches = append(errImportMismatch.mismatches, im.Prefix)
1500 continue
1501 }
1502
1503 if match >= 0 {
1504 if imports[match].VCS == "mod" && im.VCS != "mod" {
1505
1506
1507
1508 break
1509 }
1510 return metaImport{}, fmt.Errorf("multiple meta tags match import path %q", importPath)
1511 }
1512 match = i
1513 }
1514
1515 if match == -1 {
1516 return metaImport{}, errImportMismatch
1517 }
1518 return imports[match], nil
1519 }
1520
1521
1522 func expand(match map[string]string, s string) string {
1523
1524
1525
1526 oldNew := make([]string, 0, 2*len(match))
1527 for k, v := range match {
1528 oldNew = append(oldNew, "{"+k+"}", v)
1529 }
1530 return strings.NewReplacer(oldNew...).Replace(s)
1531 }
1532
1533
1534
1535
1536
1537 var vcsPaths = []*vcsPath{
1538
1539 {
1540 pathPrefix: "github.com",
1541 regexp: lazyregexp.New(`^(?P<root>github\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`),
1542 vcs: "git",
1543 repo: "https://{root}",
1544 check: noVCSSuffix,
1545 },
1546
1547
1548 {
1549 pathPrefix: "bitbucket.org",
1550 regexp: lazyregexp.New(`^(?P<root>bitbucket\.org/(?P<bitname>[\w.\-]+/[\w.\-]+))(/[\w.\-]+)*$`),
1551 vcs: "git",
1552 repo: "https://{root}",
1553 check: noVCSSuffix,
1554 },
1555
1556
1557 {
1558 pathPrefix: "hub.jazz.net/git",
1559 regexp: lazyregexp.New(`^(?P<root>hub\.jazz\.net/git/[a-z0-9]+/[\w.\-]+)(/[\w.\-]+)*$`),
1560 vcs: "git",
1561 repo: "https://{root}",
1562 check: noVCSSuffix,
1563 },
1564
1565
1566 {
1567 pathPrefix: "git.apache.org",
1568 regexp: lazyregexp.New(`^(?P<root>git\.apache\.org/[a-z0-9_.\-]+\.git)(/[\w.\-]+)*$`),
1569 vcs: "git",
1570 repo: "https://{root}",
1571 },
1572
1573
1574 {
1575 pathPrefix: "git.openstack.org",
1576 regexp: lazyregexp.New(`^(?P<root>git\.openstack\.org/[\w.\-]+/[\w.\-]+)(\.git)?(/[\w.\-]+)*$`),
1577 vcs: "git",
1578 repo: "https://{root}",
1579 },
1580
1581
1582 {
1583 pathPrefix: "chiselapp.com",
1584 regexp: lazyregexp.New(`^(?P<root>chiselapp\.com/user/[A-Za-z0-9]+/repository/[\w.\-]+)$`),
1585 vcs: "fossil",
1586 repo: "https://{root}",
1587 },
1588
1589
1590
1591 {
1592 regexp: lazyregexp.New(`(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?(/~?[\w.\-]+)+?)\.(?P<vcs>bzr|fossil|git|hg|svn))(/~?[\w.\-]+)*$`),
1593 schemelessRepo: true,
1594 },
1595 }
1596
1597
1598
1599
1600
1601 var vcsPathsAfterDynamic = []*vcsPath{
1602
1603 {
1604 pathPrefix: "launchpad.net",
1605 regexp: lazyregexp.New(`^(?P<root>launchpad\.net/((?P<project>[\w.\-]+)(?P<series>/[\w.\-]+)?|~[\w.\-]+/(\+junk|[\w.\-]+)/[\w.\-]+))(/[\w.\-]+)*$`),
1606 vcs: "bzr",
1607 repo: "https://{root}",
1608 check: launchpadVCS,
1609 },
1610 }
1611
1612
1613
1614
1615 func noVCSSuffix(match map[string]string) error {
1616 repo := match["repo"]
1617 for _, vcs := range vcsList {
1618 if strings.HasSuffix(repo, "."+vcs.Cmd) {
1619 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"])
1620 }
1621 }
1622 return nil
1623 }
1624
1625
1626
1627
1628
1629 func launchpadVCS(match map[string]string) error {
1630 if match["project"] == "" || match["series"] == "" {
1631 return nil
1632 }
1633 url := &urlpkg.URL{
1634 Scheme: "https",
1635 Host: "code.launchpad.net",
1636 Path: expand(match, "/{project}{series}/.bzr/branch-format"),
1637 }
1638 _, err := web.GetBytes(url)
1639 if err != nil {
1640 match["root"] = expand(match, "launchpad.net/{project}")
1641 match["repo"] = expand(match, "https://{root}")
1642 }
1643 return nil
1644 }
1645
1646
1647
1648 type importError struct {
1649 importPath string
1650 err error
1651 }
1652
1653 func importErrorf(path, format string, args ...any) error {
1654 err := &importError{importPath: path, err: fmt.Errorf(format, args...)}
1655 if errStr := err.Error(); !strings.Contains(errStr, path) {
1656 panic(fmt.Sprintf("path %q not in error %q", path, errStr))
1657 }
1658 return err
1659 }
1660
1661 func (e *importError) Error() string {
1662 return e.err.Error()
1663 }
1664
1665 func (e *importError) Unwrap() error {
1666
1667
1668 return errors.Unwrap(e.err)
1669 }
1670
1671 func (e *importError) ImportPath() string {
1672 return e.importPath
1673 }
1674
View as plain text