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