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