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