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 type RepoRoot struct {
756 Repo string
757 Root string
758 SubDir string
759 IsCustom bool
760 VCS *Cmd
761 }
762
763 func httpPrefix(s string) string {
764 for _, prefix := range [...]string{"http:", "https:"} {
765 if strings.HasPrefix(s, prefix) {
766 return prefix
767 }
768 }
769 return ""
770 }
771
772
773 type ModuleMode int
774
775 const (
776 IgnoreMod ModuleMode = iota
777 PreferMod
778 )
779
780
781
782 func RepoRootForImportPath(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
783 rr, err := repoRootFromVCSPaths(importPath, security, vcsPaths)
784 if err == errUnknownSite {
785 rr, err = repoRootForImportDynamic(importPath, mod, security)
786 if err != nil {
787 err = importErrorf(importPath, "unrecognized import path %q: %v", importPath, err)
788 }
789 }
790
791
792 if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") {
793
794 rr = nil
795 err = importErrorf(importPath, "cannot expand ... in %q", importPath)
796 }
797
798
799 if err == nil {
800 if rr.VCS == vcsMod {
801 counter.Inc("go/vcs:mod")
802 } else {
803 counter.Inc("go/vcs:" + rr.VCS.Cmd)
804 }
805 }
806
807 return rr, err
808 }
809
810 var errUnknownSite = errors.New("dynamic lookup required to find mapping")
811
812
813
814 func repoRootFromVCSPaths(importPath string, security web.SecurityMode, vcsPaths []*vcsPath) (*RepoRoot, error) {
815 if str.HasPathPrefix(importPath, "example.net") {
816
817
818
819
820 return nil, fmt.Errorf("no modules on example.net")
821 }
822 if importPath == "rsc.io" {
823
824
825
826
827 return nil, fmt.Errorf("rsc.io is not a module")
828 }
829
830
831 if prefix := httpPrefix(importPath); prefix != "" {
832
833
834 return nil, fmt.Errorf("%q not allowed in import path", prefix+"//")
835 }
836 for _, srv := range vcsPaths {
837 if !str.HasPathPrefix(importPath, srv.pathPrefix) {
838 continue
839 }
840 m := srv.regexp.FindStringSubmatch(importPath)
841 if m == nil {
842 if srv.pathPrefix != "" {
843 return nil, importErrorf(importPath, "invalid %s import path %q", srv.pathPrefix, importPath)
844 }
845 continue
846 }
847
848
849 match := map[string]string{
850 "prefix": srv.pathPrefix + "/",
851 "import": importPath,
852 }
853 for i, name := range srv.regexp.SubexpNames() {
854 if name != "" && match[name] == "" {
855 match[name] = m[i]
856 }
857 }
858 if srv.vcs != "" {
859 match["vcs"] = expand(match, srv.vcs)
860 }
861 if srv.repo != "" {
862 match["repo"] = expand(match, srv.repo)
863 }
864 if srv.check != nil {
865 if err := srv.check(match); err != nil {
866 return nil, err
867 }
868 }
869 vcs := vcsByCmd(match["vcs"])
870 if vcs == nil {
871 return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
872 }
873 if err := checkGOVCS(vcs, match["root"]); err != nil {
874 return nil, err
875 }
876 var repoURL string
877 if !srv.schemelessRepo {
878 repoURL = match["repo"]
879 } else {
880 repo := match["repo"]
881 var ok bool
882 repoURL, ok = interceptVCSTest(repo, vcs, security)
883 if !ok {
884 scheme, err := func() (string, error) {
885 for _, s := range vcs.Scheme {
886 if security == web.SecureOnly && !vcs.isSecureScheme(s) {
887 continue
888 }
889
890
891
892
893
894 if vcs.PingCmd == "" {
895 return s, nil
896 }
897 if err := vcs.Ping(s, repo); err == nil {
898 return s, nil
899 }
900 }
901 securityFrag := ""
902 if security == web.SecureOnly {
903 securityFrag = "secure "
904 }
905 return "", fmt.Errorf("no %sprotocol found for repository", securityFrag)
906 }()
907 if err != nil {
908 return nil, err
909 }
910 repoURL = scheme + "://" + repo
911 }
912 }
913 rr := &RepoRoot{
914 Repo: repoURL,
915 Root: match["root"],
916 VCS: vcs,
917 }
918 return rr, nil
919 }
920 return nil, errUnknownSite
921 }
922
923 func interceptVCSTest(repo string, vcs *Cmd, security web.SecurityMode) (repoURL string, ok bool) {
924 if VCSTestRepoURL == "" {
925 return "", false
926 }
927 if vcs == vcsMod {
928
929
930 return "", false
931 }
932
933 if scheme, path, ok := strings.Cut(repo, "://"); ok {
934 if security == web.SecureOnly && !vcs.isSecureScheme(scheme) {
935 return "", false
936 }
937 repo = path
938 }
939 for _, host := range VCSTestHosts {
940 if !str.HasPathPrefix(repo, host) {
941 continue
942 }
943
944 httpURL := VCSTestRepoURL + strings.TrimPrefix(repo, host)
945
946 if vcs == vcsSvn {
947
948
949 u, err := urlpkg.Parse(httpURL + "?vcwebsvn=1")
950 if err != nil {
951 panic(fmt.Sprintf("invalid vcs-test repo URL: %v", err))
952 }
953 svnURL, err := web.GetBytes(u)
954 svnURL = bytes.TrimSpace(svnURL)
955 if err == nil && len(svnURL) > 0 {
956 return string(svnURL) + strings.TrimPrefix(repo, host), true
957 }
958
959
960
961 }
962
963 return httpURL, true
964 }
965 return "", false
966 }
967
968
969
970
971
972 func urlForImportPath(importPath string) (*urlpkg.URL, error) {
973 slash := strings.Index(importPath, "/")
974 if slash < 0 {
975 slash = len(importPath)
976 }
977 host, path := importPath[:slash], importPath[slash:]
978 if !strings.Contains(host, ".") {
979 return nil, errors.New("import path does not begin with hostname")
980 }
981 if len(path) == 0 {
982 path = "/"
983 }
984 return &urlpkg.URL{Host: host, Path: path, RawQuery: "go-get=1"}, nil
985 }
986
987
988
989
990
991 func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
992 url, err := urlForImportPath(importPath)
993 if err != nil {
994 return nil, err
995 }
996 resp, err := web.Get(security, url)
997 if err != nil {
998 msg := "https fetch: %v"
999 if security == web.Insecure {
1000 msg = "http/" + msg
1001 }
1002 return nil, fmt.Errorf(msg, err)
1003 }
1004 body := resp.Body
1005 defer body.Close()
1006 imports, err := parseMetaGoImports(body, mod)
1007 if len(imports) == 0 {
1008 if respErr := resp.Err(); respErr != nil {
1009
1010
1011 return nil, respErr
1012 }
1013 }
1014 if err != nil {
1015 return nil, fmt.Errorf("parsing %s: %v", importPath, err)
1016 }
1017
1018 mmi, err := matchGoImport(imports, importPath)
1019 if err != nil {
1020 if _, ok := err.(ImportMismatchError); !ok {
1021 return nil, fmt.Errorf("parse %s: %v", url, err)
1022 }
1023 return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", resp.URL, err)
1024 }
1025 if cfg.BuildV {
1026 log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, url)
1027 }
1028
1029
1030
1031
1032
1033
1034 if mmi.Prefix != importPath {
1035 if cfg.BuildV {
1036 log.Printf("get %q: verifying non-authoritative meta tag", importPath)
1037 }
1038 var imports []metaImport
1039 url, imports, err = metaImportsForPrefix(mmi.Prefix, mod, security)
1040 if err != nil {
1041 return nil, err
1042 }
1043 metaImport2, err := matchGoImport(imports, importPath)
1044 if err != nil || mmi != metaImport2 {
1045 return nil, fmt.Errorf("%s and %s disagree about go-import for %s", resp.URL, url, mmi.Prefix)
1046 }
1047 }
1048
1049 if err := validateRepoSubDir(mmi.SubDir); err != nil {
1050 return nil, fmt.Errorf("%s: invalid subdirectory %q: %v", resp.URL, mmi.SubDir, err)
1051 }
1052
1053 if err := validateRepoRoot(mmi.RepoRoot); err != nil {
1054 return nil, fmt.Errorf("%s: invalid repo root %q: %v", resp.URL, mmi.RepoRoot, err)
1055 }
1056 var vcs *Cmd
1057 if mmi.VCS == "mod" {
1058 vcs = vcsMod
1059 } else {
1060 vcs = vcsByCmd(mmi.VCS)
1061 if vcs == nil {
1062 return nil, fmt.Errorf("%s: unknown vcs %q", resp.URL, mmi.VCS)
1063 }
1064 }
1065
1066 if err := checkGOVCS(vcs, mmi.Prefix); err != nil {
1067 return nil, err
1068 }
1069
1070 repoURL, ok := interceptVCSTest(mmi.RepoRoot, vcs, security)
1071 if !ok {
1072 repoURL = mmi.RepoRoot
1073 }
1074 rr := &RepoRoot{
1075 Repo: repoURL,
1076 Root: mmi.Prefix,
1077 SubDir: mmi.SubDir,
1078 IsCustom: true,
1079 VCS: vcs,
1080 }
1081 return rr, nil
1082 }
1083
1084
1085
1086
1087 func validateRepoSubDir(subdir string) error {
1088 if subdir == "" {
1089 return nil
1090 }
1091 if subdir[0] == '/' {
1092 return errors.New("leading slash")
1093 }
1094 if subdir[0] == '-' {
1095 return errors.New("leading hyphen")
1096 }
1097 return nil
1098 }
1099
1100
1101
1102 func validateRepoRoot(repoRoot string) error {
1103 url, err := urlpkg.Parse(repoRoot)
1104 if err != nil {
1105 return err
1106 }
1107 if url.Scheme == "" {
1108 return errors.New("no scheme")
1109 }
1110 if url.Scheme == "file" {
1111 return errors.New("file scheme disallowed")
1112 }
1113 return nil
1114 }
1115
1116 var fetchGroup singleflight.Group
1117 var (
1118 fetchCacheMu sync.Mutex
1119 fetchCache = map[string]fetchResult{}
1120 )
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130 func metaImportsForPrefix(importPrefix string, mod ModuleMode, security web.SecurityMode) (*urlpkg.URL, []metaImport, error) {
1131 setCache := func(res fetchResult) (fetchResult, error) {
1132 fetchCacheMu.Lock()
1133 defer fetchCacheMu.Unlock()
1134 fetchCache[importPrefix] = res
1135 return res, nil
1136 }
1137
1138 resi, _, _ := fetchGroup.Do(importPrefix, func() (resi any, err error) {
1139 fetchCacheMu.Lock()
1140 if res, ok := fetchCache[importPrefix]; ok {
1141 fetchCacheMu.Unlock()
1142 return res, nil
1143 }
1144 fetchCacheMu.Unlock()
1145
1146 url, err := urlForImportPath(importPrefix)
1147 if err != nil {
1148 return setCache(fetchResult{err: err})
1149 }
1150 resp, err := web.Get(security, url)
1151 if err != nil {
1152 return setCache(fetchResult{url: url, err: fmt.Errorf("fetching %s: %v", importPrefix, err)})
1153 }
1154 body := resp.Body
1155 defer body.Close()
1156 imports, err := parseMetaGoImports(body, mod)
1157 if len(imports) == 0 {
1158 if respErr := resp.Err(); respErr != nil {
1159
1160
1161 return setCache(fetchResult{url: url, err: respErr})
1162 }
1163 }
1164 if err != nil {
1165 return setCache(fetchResult{url: url, err: fmt.Errorf("parsing %s: %v", resp.URL, err)})
1166 }
1167 if len(imports) == 0 {
1168 err = fmt.Errorf("fetching %s: no go-import meta tag found in %s", importPrefix, resp.URL)
1169 }
1170 return setCache(fetchResult{url: url, imports: imports, err: err})
1171 })
1172 res := resi.(fetchResult)
1173 return res.url, res.imports, res.err
1174 }
1175
1176 type fetchResult struct {
1177 url *urlpkg.URL
1178 imports []metaImport
1179 err error
1180 }
1181
1182
1183
1184 type metaImport struct {
1185 Prefix, VCS, RepoRoot, SubDir string
1186 }
1187
1188
1189
1190 type ImportMismatchError struct {
1191 importPath string
1192 mismatches []string
1193 }
1194
1195 func (m ImportMismatchError) Error() string {
1196 formattedStrings := make([]string, len(m.mismatches))
1197 for i, pre := range m.mismatches {
1198 formattedStrings[i] = fmt.Sprintf("meta tag %s did not match import path %s", pre, m.importPath)
1199 }
1200 return strings.Join(formattedStrings, ", ")
1201 }
1202
1203
1204
1205
1206 func matchGoImport(imports []metaImport, importPath string) (metaImport, error) {
1207 match := -1
1208
1209 errImportMismatch := ImportMismatchError{importPath: importPath}
1210 for i, im := range imports {
1211 if !str.HasPathPrefix(importPath, im.Prefix) {
1212 errImportMismatch.mismatches = append(errImportMismatch.mismatches, im.Prefix)
1213 continue
1214 }
1215
1216 if match >= 0 {
1217 if imports[match].VCS == "mod" && im.VCS != "mod" {
1218
1219
1220
1221 break
1222 }
1223 return metaImport{}, fmt.Errorf("multiple meta tags match import path %q", importPath)
1224 }
1225 match = i
1226 }
1227
1228 if match == -1 {
1229 return metaImport{}, errImportMismatch
1230 }
1231 return imports[match], nil
1232 }
1233
1234
1235 func expand(match map[string]string, s string) string {
1236
1237
1238
1239 oldNew := make([]string, 0, 2*len(match))
1240 for k, v := range match {
1241 oldNew = append(oldNew, "{"+k+"}", v)
1242 }
1243 return strings.NewReplacer(oldNew...).Replace(s)
1244 }
1245
1246
1247
1248
1249
1250 var vcsPaths = []*vcsPath{
1251
1252 {
1253 pathPrefix: "github.com",
1254 regexp: lazyregexp.New(`^(?P<root>github\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`),
1255 vcs: "git",
1256 repo: "https://{root}",
1257 check: noVCSSuffix,
1258 },
1259
1260
1261 {
1262 pathPrefix: "bitbucket.org",
1263 regexp: lazyregexp.New(`^(?P<root>bitbucket\.org/(?P<bitname>[\w.\-]+/[\w.\-]+))(/[\w.\-]+)*$`),
1264 vcs: "git",
1265 repo: "https://{root}",
1266 check: noVCSSuffix,
1267 },
1268
1269
1270 {
1271 pathPrefix: "hub.jazz.net/git",
1272 regexp: lazyregexp.New(`^(?P<root>hub\.jazz\.net/git/[a-z0-9]+/[\w.\-]+)(/[\w.\-]+)*$`),
1273 vcs: "git",
1274 repo: "https://{root}",
1275 check: noVCSSuffix,
1276 },
1277
1278
1279 {
1280 pathPrefix: "git.apache.org",
1281 regexp: lazyregexp.New(`^(?P<root>git\.apache\.org/[a-z0-9_.\-]+\.git)(/[\w.\-]+)*$`),
1282 vcs: "git",
1283 repo: "https://{root}",
1284 },
1285
1286
1287 {
1288 pathPrefix: "git.openstack.org",
1289 regexp: lazyregexp.New(`^(?P<root>git\.openstack\.org/[\w.\-]+/[\w.\-]+)(\.git)?(/[\w.\-]+)*$`),
1290 vcs: "git",
1291 repo: "https://{root}",
1292 },
1293
1294
1295 {
1296 pathPrefix: "chiselapp.com",
1297 regexp: lazyregexp.New(`^(?P<root>chiselapp\.com/user/[A-Za-z0-9]+/repository/[\w.\-]+)$`),
1298 vcs: "fossil",
1299 repo: "https://{root}",
1300 },
1301
1302
1303
1304 {
1305 regexp: lazyregexp.New(`(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?(/~?[\w.\-]+)+?)\.(?P<vcs>fossil|git|hg|svn))(/~?[\w.\-]+)*$`),
1306 schemelessRepo: true,
1307 },
1308 }
1309
1310
1311
1312
1313 func noVCSSuffix(match map[string]string) error {
1314 repo := match["repo"]
1315 for _, vcs := range vcsList {
1316 if strings.HasSuffix(repo, "."+vcs.Cmd) {
1317 return fmt.Errorf("invalid version control suffix in %s path", match["prefix"])
1318 }
1319 }
1320 return nil
1321 }
1322
1323
1324
1325 type importError struct {
1326 importPath string
1327 err error
1328 }
1329
1330 func importErrorf(path, format string, args ...any) error {
1331 err := &importError{importPath: path, err: fmt.Errorf(format, args...)}
1332 if errStr := err.Error(); !strings.Contains(errStr, path) {
1333 panic(fmt.Sprintf("path %q not in error %q", path, errStr))
1334 }
1335 return err
1336 }
1337
1338 func (e *importError) Error() string {
1339 return e.err.Error()
1340 }
1341
1342 func (e *importError) Unwrap() error {
1343
1344
1345 return errors.Unwrap(e.err)
1346 }
1347
1348 func (e *importError) ImportPath() string {
1349 return e.importPath
1350 }
1351
View as plain text