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