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 IsCustom bool
1061 VCS *Cmd
1062 }
1063
1064 func httpPrefix(s string) string {
1065 for _, prefix := range [...]string{"http:", "https:"} {
1066 if strings.HasPrefix(s, prefix) {
1067 return prefix
1068 }
1069 }
1070 return ""
1071 }
1072
1073
1074 type ModuleMode int
1075
1076 const (
1077 IgnoreMod ModuleMode = iota
1078 PreferMod
1079 )
1080
1081
1082
1083 func RepoRootForImportPath(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
1084 rr, err := repoRootFromVCSPaths(importPath, security, vcsPaths)
1085 if err == errUnknownSite {
1086 rr, err = repoRootForImportDynamic(importPath, mod, security)
1087 if err != nil {
1088 err = importErrorf(importPath, "unrecognized import path %q: %v", importPath, err)
1089 }
1090 }
1091 if err != nil {
1092 rr1, err1 := repoRootFromVCSPaths(importPath, security, vcsPathsAfterDynamic)
1093 if err1 == nil {
1094 rr = rr1
1095 err = nil
1096 }
1097 }
1098
1099
1100 if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") {
1101
1102 rr = nil
1103 err = importErrorf(importPath, "cannot expand ... in %q", importPath)
1104 }
1105 return rr, err
1106 }
1107
1108 var errUnknownSite = errors.New("dynamic lookup required to find mapping")
1109
1110
1111
1112 func repoRootFromVCSPaths(importPath string, security web.SecurityMode, vcsPaths []*vcsPath) (*RepoRoot, error) {
1113 if str.HasPathPrefix(importPath, "example.net") {
1114
1115
1116
1117
1118 return nil, fmt.Errorf("no modules on example.net")
1119 }
1120 if importPath == "rsc.io" {
1121
1122
1123
1124
1125 return nil, fmt.Errorf("rsc.io is not a module")
1126 }
1127
1128
1129 if prefix := httpPrefix(importPath); prefix != "" {
1130
1131
1132 return nil, fmt.Errorf("%q not allowed in import path", prefix+"//")
1133 }
1134 for _, srv := range vcsPaths {
1135 if !str.HasPathPrefix(importPath, srv.pathPrefix) {
1136 continue
1137 }
1138 m := srv.regexp.FindStringSubmatch(importPath)
1139 if m == nil {
1140 if srv.pathPrefix != "" {
1141 return nil, importErrorf(importPath, "invalid %s import path %q", srv.pathPrefix, importPath)
1142 }
1143 continue
1144 }
1145
1146
1147 match := map[string]string{
1148 "prefix": srv.pathPrefix + "/",
1149 "import": importPath,
1150 }
1151 for i, name := range srv.regexp.SubexpNames() {
1152 if name != "" && match[name] == "" {
1153 match[name] = m[i]
1154 }
1155 }
1156 if srv.vcs != "" {
1157 match["vcs"] = expand(match, srv.vcs)
1158 }
1159 if srv.repo != "" {
1160 match["repo"] = expand(match, srv.repo)
1161 }
1162 if srv.check != nil {
1163 if err := srv.check(match); err != nil {
1164 return nil, err
1165 }
1166 }
1167 vcs := vcsByCmd(match["vcs"])
1168 if vcs == nil {
1169 return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
1170 }
1171 if err := checkGOVCS(vcs, match["root"]); err != nil {
1172 return nil, err
1173 }
1174 var repoURL string
1175 if !srv.schemelessRepo {
1176 repoURL = match["repo"]
1177 } else {
1178 repo := match["repo"]
1179 var ok bool
1180 repoURL, ok = interceptVCSTest(repo, vcs, security)
1181 if !ok {
1182 scheme, err := func() (string, error) {
1183 for _, s := range vcs.Scheme {
1184 if security == web.SecureOnly && !vcs.isSecureScheme(s) {
1185 continue
1186 }
1187
1188
1189
1190
1191
1192 if vcs.PingCmd == "" {
1193 return s, nil
1194 }
1195 if err := vcs.Ping(s, repo); err == nil {
1196 return s, nil
1197 }
1198 }
1199 securityFrag := ""
1200 if security == web.SecureOnly {
1201 securityFrag = "secure "
1202 }
1203 return "", fmt.Errorf("no %sprotocol found for repository", securityFrag)
1204 }()
1205 if err != nil {
1206 return nil, err
1207 }
1208 repoURL = scheme + "://" + repo
1209 }
1210 }
1211 rr := &RepoRoot{
1212 Repo: repoURL,
1213 Root: match["root"],
1214 VCS: vcs,
1215 }
1216 return rr, nil
1217 }
1218 return nil, errUnknownSite
1219 }
1220
1221 func interceptVCSTest(repo string, vcs *Cmd, security web.SecurityMode) (repoURL string, ok bool) {
1222 if VCSTestRepoURL == "" {
1223 return "", false
1224 }
1225 if vcs == vcsMod {
1226
1227
1228 return "", false
1229 }
1230
1231 if scheme, path, ok := strings.Cut(repo, "://"); ok {
1232 if security == web.SecureOnly && !vcs.isSecureScheme(scheme) {
1233 return "", false
1234 }
1235 repo = path
1236 }
1237 for _, host := range VCSTestHosts {
1238 if !str.HasPathPrefix(repo, host) {
1239 continue
1240 }
1241
1242 httpURL := VCSTestRepoURL + strings.TrimPrefix(repo, host)
1243
1244 if vcs == vcsSvn {
1245
1246
1247 u, err := urlpkg.Parse(httpURL + "?vcwebsvn=1")
1248 if err != nil {
1249 panic(fmt.Sprintf("invalid vcs-test repo URL: %v", err))
1250 }
1251 svnURL, err := web.GetBytes(u)
1252 svnURL = bytes.TrimSpace(svnURL)
1253 if err == nil && len(svnURL) > 0 {
1254 return string(svnURL) + strings.TrimPrefix(repo, host), true
1255 }
1256
1257
1258
1259 }
1260
1261 return httpURL, true
1262 }
1263 return "", false
1264 }
1265
1266
1267
1268
1269
1270 func urlForImportPath(importPath string) (*urlpkg.URL, error) {
1271 slash := strings.Index(importPath, "/")
1272 if slash < 0 {
1273 slash = len(importPath)
1274 }
1275 host, path := importPath[:slash], importPath[slash:]
1276 if !strings.Contains(host, ".") {
1277 return nil, errors.New("import path does not begin with hostname")
1278 }
1279 if len(path) == 0 {
1280 path = "/"
1281 }
1282 return &urlpkg.URL{Host: host, Path: path, RawQuery: "go-get=1"}, nil
1283 }
1284
1285
1286
1287
1288
1289 func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
1290 url, err := urlForImportPath(importPath)
1291 if err != nil {
1292 return nil, err
1293 }
1294 resp, err := web.Get(security, url)
1295 if err != nil {
1296 msg := "https fetch: %v"
1297 if security == web.Insecure {
1298 msg = "http/" + msg
1299 }
1300 return nil, fmt.Errorf(msg, err)
1301 }
1302 body := resp.Body
1303 defer body.Close()
1304 imports, err := parseMetaGoImports(body, mod)
1305 if len(imports) == 0 {
1306 if respErr := resp.Err(); respErr != nil {
1307
1308
1309 return nil, respErr
1310 }
1311 }
1312 if err != nil {
1313 return nil, fmt.Errorf("parsing %s: %v", importPath, err)
1314 }
1315
1316 mmi, err := matchGoImport(imports, importPath)
1317 if err != nil {
1318 if _, ok := err.(ImportMismatchError); !ok {
1319 return nil, fmt.Errorf("parse %s: %v", url, err)
1320 }
1321 return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", resp.URL, err)
1322 }
1323 if cfg.BuildV {
1324 log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, url)
1325 }
1326
1327
1328
1329
1330
1331
1332 if mmi.Prefix != importPath {
1333 if cfg.BuildV {
1334 log.Printf("get %q: verifying non-authoritative meta tag", importPath)
1335 }
1336 var imports []metaImport
1337 url, imports, err = metaImportsForPrefix(mmi.Prefix, mod, security)
1338 if err != nil {
1339 return nil, err
1340 }
1341 metaImport2, err := matchGoImport(imports, importPath)
1342 if err != nil || mmi != metaImport2 {
1343 return nil, fmt.Errorf("%s and %s disagree about go-import for %s", resp.URL, url, mmi.Prefix)
1344 }
1345 }
1346
1347 if err := validateRepoRoot(mmi.RepoRoot); err != nil {
1348 return nil, fmt.Errorf("%s: invalid repo root %q: %v", resp.URL, mmi.RepoRoot, err)
1349 }
1350 var vcs *Cmd
1351 if mmi.VCS == "mod" {
1352 vcs = vcsMod
1353 } else {
1354 vcs = vcsByCmd(mmi.VCS)
1355 if vcs == nil {
1356 return nil, fmt.Errorf("%s: unknown vcs %q", resp.URL, mmi.VCS)
1357 }
1358 }
1359
1360 if err := checkGOVCS(vcs, mmi.Prefix); err != nil {
1361 return nil, err
1362 }
1363
1364 repoURL, ok := interceptVCSTest(mmi.RepoRoot, vcs, security)
1365 if !ok {
1366 repoURL = mmi.RepoRoot
1367 }
1368 rr := &RepoRoot{
1369 Repo: repoURL,
1370 Root: mmi.Prefix,
1371 IsCustom: true,
1372 VCS: vcs,
1373 }
1374 return rr, nil
1375 }
1376
1377
1378
1379 func validateRepoRoot(repoRoot string) error {
1380 url, err := urlpkg.Parse(repoRoot)
1381 if err != nil {
1382 return err
1383 }
1384 if url.Scheme == "" {
1385 return errors.New("no scheme")
1386 }
1387 if url.Scheme == "file" {
1388 return errors.New("file scheme disallowed")
1389 }
1390 return nil
1391 }
1392
1393 var fetchGroup singleflight.Group
1394 var (
1395 fetchCacheMu sync.Mutex
1396 fetchCache = map[string]fetchResult{}
1397 )
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407 func metaImportsForPrefix(importPrefix string, mod ModuleMode, security web.SecurityMode) (*urlpkg.URL, []metaImport, error) {
1408 setCache := func(res fetchResult) (fetchResult, error) {
1409 fetchCacheMu.Lock()
1410 defer fetchCacheMu.Unlock()
1411 fetchCache[importPrefix] = res
1412 return res, nil
1413 }
1414
1415 resi, _, _ := fetchGroup.Do(importPrefix, func() (resi any, err error) {
1416 fetchCacheMu.Lock()
1417 if res, ok := fetchCache[importPrefix]; ok {
1418 fetchCacheMu.Unlock()
1419 return res, nil
1420 }
1421 fetchCacheMu.Unlock()
1422
1423 url, err := urlForImportPath(importPrefix)
1424 if err != nil {
1425 return setCache(fetchResult{err: err})
1426 }
1427 resp, err := web.Get(security, url)
1428 if err != nil {
1429 return setCache(fetchResult{url: url, err: fmt.Errorf("fetching %s: %v", importPrefix, err)})
1430 }
1431 body := resp.Body
1432 defer body.Close()
1433 imports, err := parseMetaGoImports(body, mod)
1434 if len(imports) == 0 {
1435 if respErr := resp.Err(); respErr != nil {
1436
1437
1438 return setCache(fetchResult{url: url, err: respErr})
1439 }
1440 }
1441 if err != nil {
1442 return setCache(fetchResult{url: url, err: fmt.Errorf("parsing %s: %v", resp.URL, err)})
1443 }
1444 if len(imports) == 0 {
1445 err = fmt.Errorf("fetching %s: no go-import meta tag found in %s", importPrefix, resp.URL)
1446 }
1447 return setCache(fetchResult{url: url, imports: imports, err: err})
1448 })
1449 res := resi.(fetchResult)
1450 return res.url, res.imports, res.err
1451 }
1452
1453 type fetchResult struct {
1454 url *urlpkg.URL
1455 imports []metaImport
1456 err error
1457 }
1458
1459
1460
1461 type metaImport struct {
1462 Prefix, VCS, RepoRoot string
1463 }
1464
1465
1466
1467 type ImportMismatchError struct {
1468 importPath string
1469 mismatches []string
1470 }
1471
1472 func (m ImportMismatchError) Error() string {
1473 formattedStrings := make([]string, len(m.mismatches))
1474 for i, pre := range m.mismatches {
1475 formattedStrings[i] = fmt.Sprintf("meta tag %s did not match import path %s", pre, m.importPath)
1476 }
1477 return strings.Join(formattedStrings, ", ")
1478 }
1479
1480
1481
1482
1483 func matchGoImport(imports []metaImport, importPath string) (metaImport, error) {
1484 match := -1
1485
1486 errImportMismatch := ImportMismatchError{importPath: importPath}
1487 for i, im := range imports {
1488 if !str.HasPathPrefix(importPath, im.Prefix) {
1489 errImportMismatch.mismatches = append(errImportMismatch.mismatches, im.Prefix)
1490 continue
1491 }
1492
1493 if match >= 0 {
1494 if imports[match].VCS == "mod" && im.VCS != "mod" {
1495
1496
1497
1498 break
1499 }
1500 return metaImport{}, fmt.Errorf("multiple meta tags match import path %q", importPath)
1501 }
1502 match = i
1503 }
1504
1505 if match == -1 {
1506 return metaImport{}, errImportMismatch
1507 }
1508 return imports[match], nil
1509 }
1510
1511
1512 func expand(match map[string]string, s string) string {
1513
1514
1515
1516 oldNew := make([]string, 0, 2*len(match))
1517 for k, v := range match {
1518 oldNew = append(oldNew, "{"+k+"}", v)
1519 }
1520 return strings.NewReplacer(oldNew...).Replace(s)
1521 }
1522
1523
1524
1525
1526
1527 var vcsPaths = []*vcsPath{
1528
1529 {
1530 pathPrefix: "github.com",
1531 regexp: lazyregexp.New(`^(?P<root>github\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`),
1532 vcs: "git",
1533 repo: "https://{root}",
1534 check: noVCSSuffix,
1535 },
1536
1537
1538 {
1539 pathPrefix: "bitbucket.org",
1540 regexp: lazyregexp.New(`^(?P<root>bitbucket\.org/(?P<bitname>[\w.\-]+/[\w.\-]+))(/[\w.\-]+)*$`),
1541 vcs: "git",
1542 repo: "https://{root}",
1543 check: noVCSSuffix,
1544 },
1545
1546
1547 {
1548 pathPrefix: "hub.jazz.net/git",
1549 regexp: lazyregexp.New(`^(?P<root>hub\.jazz\.net/git/[a-z0-9]+/[\w.\-]+)(/[\w.\-]+)*$`),
1550 vcs: "git",
1551 repo: "https://{root}",
1552 check: noVCSSuffix,
1553 },
1554
1555
1556 {
1557 pathPrefix: "git.apache.org",
1558 regexp: lazyregexp.New(`^(?P<root>git\.apache\.org/[a-z0-9_.\-]+\.git)(/[\w.\-]+)*$`),
1559 vcs: "git",
1560 repo: "https://{root}",
1561 },
1562
1563
1564 {
1565 pathPrefix: "git.openstack.org",
1566 regexp: lazyregexp.New(`^(?P<root>git\.openstack\.org/[\w.\-]+/[\w.\-]+)(\.git)?(/[\w.\-]+)*$`),
1567 vcs: "git",
1568 repo: "https://{root}",
1569 },
1570
1571
1572 {
1573 pathPrefix: "chiselapp.com",
1574 regexp: lazyregexp.New(`^(?P<root>chiselapp\.com/user/[A-Za-z0-9]+/repository/[\w.\-]+)$`),
1575 vcs: "fossil",
1576 repo: "https://{root}",
1577 },
1578
1579
1580
1581 {
1582 regexp: lazyregexp.New(`(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?(/~?[\w.\-]+)+?)\.(?P<vcs>bzr|fossil|git|hg|svn))(/~?[\w.\-]+)*$`),
1583 schemelessRepo: true,
1584 },
1585 }
1586
1587
1588
1589
1590
1591 var vcsPathsAfterDynamic = []*vcsPath{
1592
1593 {
1594 pathPrefix: "launchpad.net",
1595 regexp: lazyregexp.New(`^(?P<root>launchpad\.net/((?P<project>[\w.\-]+)(?P<series>/[\w.\-]+)?|~[\w.\-]+/(\+junk|[\w.\-]+)/[\w.\-]+))(/[\w.\-]+)*$`),
1596 vcs: "bzr",
1597 repo: "https://{root}",
1598 check: launchpadVCS,
1599 },
1600 }
1601
1602
1603
1604
1605 func noVCSSuffix(match map[string]string) error {
1606 repo := match["repo"]
1607 for _, vcs := range vcsList {
1608 if strings.HasSuffix(repo, "."+vcs.Cmd) {
1609 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"])
1610 }
1611 }
1612 return nil
1613 }
1614
1615
1616
1617
1618
1619 func launchpadVCS(match map[string]string) error {
1620 if match["project"] == "" || match["series"] == "" {
1621 return nil
1622 }
1623 url := &urlpkg.URL{
1624 Scheme: "https",
1625 Host: "code.launchpad.net",
1626 Path: expand(match, "/{project}{series}/.bzr/branch-format"),
1627 }
1628 _, err := web.GetBytes(url)
1629 if err != nil {
1630 match["root"] = expand(match, "launchpad.net/{project}")
1631 match["repo"] = expand(match, "https://{root}")
1632 }
1633 return nil
1634 }
1635
1636
1637
1638 type importError struct {
1639 importPath string
1640 err error
1641 }
1642
1643 func importErrorf(path, format string, args ...any) error {
1644 err := &importError{importPath: path, err: fmt.Errorf(format, args...)}
1645 if errStr := err.Error(); !strings.Contains(errStr, path) {
1646 panic(fmt.Sprintf("path %q not in error %q", path, errStr))
1647 }
1648 return err
1649 }
1650
1651 func (e *importError) Error() string {
1652 return e.err.Error()
1653 }
1654
1655 func (e *importError) Unwrap() error {
1656
1657
1658 return errors.Unwrap(e.err)
1659 }
1660
1661 func (e *importError) ImportPath() string {
1662 return e.importPath
1663 }
1664
View as plain text