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