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