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