Source file src/pkg/cmd/go/internal/modfetch/coderepo.go
1
2
3
4
5 package modfetch
6
7 import (
8 "archive/zip"
9 "errors"
10 "fmt"
11 "io"
12 "io/ioutil"
13 "os"
14 "path"
15 "strings"
16 "time"
17
18 "cmd/go/internal/modfetch/codehost"
19 "cmd/go/internal/modfile"
20 "cmd/go/internal/module"
21 "cmd/go/internal/semver"
22 )
23
24
25 type codeRepo struct {
26 modPath string
27
28
29 code codehost.Repo
30
31 codeRoot string
32
33
34
35 codeDir string
36
37
38
39
40
41
42 pathMajor string
43
44
45 pathPrefix string
46
47
48
49
50
51 pseudoMajor string
52 }
53
54
55
56
57 func newCodeRepo(code codehost.Repo, codeRoot, path string) (Repo, error) {
58 if !hasPathPrefix(path, codeRoot) {
59 return nil, fmt.Errorf("mismatched repo: found %s for %s", codeRoot, path)
60 }
61 pathPrefix, pathMajor, ok := module.SplitPathVersion(path)
62 if !ok {
63 return nil, fmt.Errorf("invalid module path %q", path)
64 }
65 if codeRoot == path {
66 pathPrefix = path
67 }
68 pseudoMajor := module.PathMajorPrefix(pathMajor)
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104 codeDir := ""
105 if codeRoot != path {
106 if !hasPathPrefix(pathPrefix, codeRoot) {
107 return nil, fmt.Errorf("repository rooted at %s cannot contain module %s", codeRoot, path)
108 }
109 codeDir = strings.Trim(pathPrefix[len(codeRoot):], "/")
110 }
111
112 r := &codeRepo{
113 modPath: path,
114 code: code,
115 codeRoot: codeRoot,
116 codeDir: codeDir,
117 pathPrefix: pathPrefix,
118 pathMajor: pathMajor,
119 pseudoMajor: pseudoMajor,
120 }
121
122 return r, nil
123 }
124
125 func (r *codeRepo) ModulePath() string {
126 return r.modPath
127 }
128
129 func (r *codeRepo) Versions(prefix string) ([]string, error) {
130
131
132
133 if strings.HasPrefix(r.modPath, "gopkg.in/") && strings.HasSuffix(r.modPath, "-unstable") {
134 return nil, nil
135 }
136
137 p := prefix
138 if r.codeDir != "" {
139 p = r.codeDir + "/" + p
140 }
141 tags, err := r.code.Tags(p)
142 if err != nil {
143 return nil, err
144 }
145
146 list := []string{}
147 var incompatible []string
148 for _, tag := range tags {
149 if !strings.HasPrefix(tag, p) {
150 continue
151 }
152 v := tag
153 if r.codeDir != "" {
154 v = v[len(r.codeDir)+1:]
155 }
156 if v == "" || v != module.CanonicalVersion(v) || IsPseudoVersion(v) {
157 continue
158 }
159 if err := module.MatchPathMajor(v, r.pathMajor); err != nil {
160 if r.codeDir == "" && r.pathMajor == "" && semver.Major(v) > "v1" {
161 incompatible = append(incompatible, v)
162 }
163 continue
164 }
165 list = append(list, v)
166 }
167
168 if len(incompatible) > 0 {
169
170
171
172 files, err := r.code.ReadFileRevs(incompatible, "go.mod", codehost.MaxGoMod)
173 if err != nil {
174 return nil, err
175 }
176 for _, rev := range incompatible {
177 f := files[rev]
178 if os.IsNotExist(f.Err) {
179 list = append(list, rev+"+incompatible")
180 }
181 }
182 }
183
184 SortVersions(list)
185 return list, nil
186 }
187
188 func (r *codeRepo) Stat(rev string) (*RevInfo, error) {
189 if rev == "latest" {
190 return r.Latest()
191 }
192 codeRev := r.revToRev(rev)
193 info, err := r.code.Stat(codeRev)
194 if err != nil {
195 return nil, &module.ModuleError{
196 Path: r.modPath,
197 Err: &module.InvalidVersionError{
198 Version: rev,
199 Err: err,
200 },
201 }
202 }
203 return r.convert(info, rev)
204 }
205
206 func (r *codeRepo) Latest() (*RevInfo, error) {
207 info, err := r.code.Latest()
208 if err != nil {
209 return nil, err
210 }
211 return r.convert(info, "")
212 }
213
214
215
216
217
218
219 func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, error) {
220 info2 := &RevInfo{
221 Name: info.Name,
222 Short: info.Short,
223 Time: info.Time,
224 }
225
226
227
228
229
230 var canUseIncompatible func() bool
231 canUseIncompatible = func() bool {
232 var ok bool
233 if r.codeDir == "" && r.pathMajor == "" {
234 _, errGoMod := r.code.ReadFile(info.Name, "go.mod", codehost.MaxGoMod)
235 if errGoMod != nil {
236 ok = true
237 }
238 }
239 canUseIncompatible = func() bool { return ok }
240 return ok
241 }
242
243 invalidf := func(format string, args ...interface{}) error {
244 return &module.ModuleError{
245 Path: r.modPath,
246 Err: &module.InvalidVersionError{
247 Version: info2.Version,
248 Err: fmt.Errorf(format, args...),
249 },
250 }
251 }
252
253
254
255 checkGoMod := func() (*RevInfo, error) {
256
257
258
259
260
261
262
263
264
265
266 _, _, _, err := r.findDir(info2.Version)
267 if err != nil {
268
269
270 return nil, &module.ModuleError{
271 Path: r.modPath,
272 Err: &module.InvalidVersionError{
273 Version: info2.Version,
274 Err: notExistError(err.Error()),
275 },
276 }
277 }
278
279
280
281 if strings.HasSuffix(info2.Version, "+incompatible") {
282 if !canUseIncompatible() {
283 if r.pathMajor != "" {
284 return nil, invalidf("+incompatible suffix not allowed: module path includes a major version suffix, so major version must match")
285 } else {
286 return nil, invalidf("+incompatible suffix not allowed: module contains a go.mod file, so semantic import versioning is required")
287 }
288 }
289
290 if err := module.MatchPathMajor(strings.TrimSuffix(info2.Version, "+incompatible"), r.pathMajor); err == nil {
291 return nil, invalidf("+incompatible suffix not allowed: major version %s is compatible", semver.Major(info2.Version))
292 }
293 }
294
295 return info2, nil
296 }
297
298
299
300
301
302
303
304 if statVers != "" && statVers == module.CanonicalVersion(statVers) {
305 info2.Version = statVers
306
307 if IsPseudoVersion(info2.Version) {
308 if err := r.validatePseudoVersion(info, info2.Version); err != nil {
309 return nil, err
310 }
311 return checkGoMod()
312 }
313
314 if err := module.MatchPathMajor(info2.Version, r.pathMajor); err != nil {
315 if canUseIncompatible() {
316 info2.Version += "+incompatible"
317 return checkGoMod()
318 } else {
319 if vErr, ok := err.(*module.InvalidVersionError); ok {
320
321
322 err = vErr.Err
323 }
324 return nil, invalidf("module contains a go.mod file, so major version must be compatible: %v", err)
325 }
326 }
327
328 return checkGoMod()
329 }
330
331
332
333
334
335
336 tagPrefix := ""
337 if r.codeDir != "" {
338 tagPrefix = r.codeDir + "/"
339 }
340
341
342
343
344 tagToVersion := func(tag string) (v string, tagIsCanonical bool) {
345 if !strings.HasPrefix(tag, tagPrefix) {
346 return "", false
347 }
348 trimmed := tag[len(tagPrefix):]
349
350 if IsPseudoVersion(tag) {
351 return "", false
352 }
353
354 v = semver.Canonical(trimmed)
355 if v == "" || !strings.HasPrefix(trimmed, v) {
356 return "", false
357 }
358 if v == trimmed {
359 tagIsCanonical = true
360 }
361
362 if err := module.MatchPathMajor(v, r.pathMajor); err != nil {
363 if canUseIncompatible() {
364 return v + "+incompatible", tagIsCanonical
365 }
366 return "", false
367 }
368
369 return v, tagIsCanonical
370 }
371
372
373 if v, tagIsCanonical := tagToVersion(info.Version); tagIsCanonical {
374 info2.Version = v
375 return checkGoMod()
376 }
377
378
379
380 var pseudoBase string
381 for _, pathTag := range info.Tags {
382 v, tagIsCanonical := tagToVersion(pathTag)
383 if tagIsCanonical {
384 if statVers != "" && semver.Compare(v, statVers) == 0 {
385
386
387 info2.Version = v
388 return checkGoMod()
389 } else {
390
391
392
393
394
395
396
397 if semver.Compare(info2.Version, v) < 0 {
398 info2.Version = v
399 }
400 }
401 } else if v != "" && semver.Compare(v, statVers) == 0 {
402
403
404
405
406
407
408
409
410
411
412 pseudoBase = v
413 }
414 }
415
416
417
418 if info2.Version != "" {
419 return checkGoMod()
420 }
421
422 if pseudoBase == "" {
423 var tag string
424 if r.pseudoMajor != "" || canUseIncompatible() {
425 tag, _ = r.code.RecentTag(info.Name, tagPrefix, r.pseudoMajor)
426 } else {
427
428 tag, _ = r.code.RecentTag(info.Name, tagPrefix, "v1")
429 if tag == "" {
430 tag, _ = r.code.RecentTag(info.Name, tagPrefix, "v0")
431 }
432 }
433 pseudoBase, _ = tagToVersion(tag)
434 }
435
436 info2.Version = PseudoVersion(r.pseudoMajor, pseudoBase, info.Time, info.Short)
437 return checkGoMod()
438 }
439
440
441
442
443
444
445
446
447
448
449 func (r *codeRepo) validatePseudoVersion(info *codehost.RevInfo, version string) (err error) {
450 defer func() {
451 if err != nil {
452 if _, ok := err.(*module.ModuleError); !ok {
453 if _, ok := err.(*module.InvalidVersionError); !ok {
454 err = &module.InvalidVersionError{Version: version, Pseudo: true, Err: err}
455 }
456 err = &module.ModuleError{Path: r.modPath, Err: err}
457 }
458 }
459 }()
460
461 if err := module.MatchPathMajor(version, r.pathMajor); err != nil {
462 return err
463 }
464
465 rev, err := PseudoVersionRev(version)
466 if err != nil {
467 return err
468 }
469 if rev != info.Short {
470 switch {
471 case strings.HasPrefix(rev, info.Short):
472 return fmt.Errorf("revision is longer than canonical (%s)", info.Short)
473 case strings.HasPrefix(info.Short, rev):
474 return fmt.Errorf("revision is shorter than canonical (%s)", info.Short)
475 default:
476 return fmt.Errorf("does not match short name of revision (%s)", info.Short)
477 }
478 }
479
480 t, err := PseudoVersionTime(version)
481 if err != nil {
482 return err
483 }
484 if !t.Equal(info.Time.Truncate(time.Second)) {
485 return fmt.Errorf("does not match version-control timestamp (%s)", info.Time.UTC().Format(time.RFC3339))
486 }
487
488 tagPrefix := ""
489 if r.codeDir != "" {
490 tagPrefix = r.codeDir + "/"
491 }
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509 base, err := PseudoVersionBase(strings.TrimSuffix(version, "+incompatible"))
510 if err != nil {
511 return err
512 }
513 if base == "" {
514 if r.pseudoMajor == "" && semver.Major(version) == "v1" {
515 return fmt.Errorf("major version without preceding tag must be v0, not v1")
516 }
517 return nil
518 } else {
519 for _, tag := range info.Tags {
520 versionOnly := strings.TrimPrefix(tag, tagPrefix)
521 if versionOnly == base {
522
523
524
525
526
527
528
529
530
531
532
533
534
535 return fmt.Errorf("tag (%s) found on revision %s is already canonical, so should not be replaced with a pseudo-version derived from that tag", tag, rev)
536 }
537 }
538 }
539
540 tags, err := r.code.Tags(tagPrefix + base)
541 if err != nil {
542 return err
543 }
544
545 var lastTag string
546 ancestorFound := false
547 for _, tag := range tags {
548 versionOnly := strings.TrimPrefix(tag, tagPrefix)
549 if semver.Compare(versionOnly, base) == 0 {
550 lastTag = tag
551 ancestorFound, err = r.code.DescendsFrom(info.Name, tag)
552 if ancestorFound {
553 break
554 }
555 }
556 }
557
558 if lastTag == "" {
559 return fmt.Errorf("preceding tag (%s) not found", base)
560 }
561
562 if !ancestorFound {
563 if err != nil {
564 return err
565 }
566 rev, err := PseudoVersionRev(version)
567 if err != nil {
568 return fmt.Errorf("not a descendent of preceding tag (%s)", lastTag)
569 }
570 return fmt.Errorf("revision %s is not a descendent of preceding tag (%s)", rev, lastTag)
571 }
572 return nil
573 }
574
575 func (r *codeRepo) revToRev(rev string) string {
576 if semver.IsValid(rev) {
577 if IsPseudoVersion(rev) {
578 r, _ := PseudoVersionRev(rev)
579 return r
580 }
581 if semver.Build(rev) == "+incompatible" {
582 rev = rev[:len(rev)-len("+incompatible")]
583 }
584 if r.codeDir == "" {
585 return rev
586 }
587 return r.codeDir + "/" + rev
588 }
589 return rev
590 }
591
592 func (r *codeRepo) versionToRev(version string) (rev string, err error) {
593 if !semver.IsValid(version) {
594 return "", &module.ModuleError{
595 Path: r.modPath,
596 Err: &module.InvalidVersionError{
597 Version: version,
598 Err: errors.New("syntax error"),
599 },
600 }
601 }
602 return r.revToRev(version), nil
603 }
604
605
606
607
608
609 func (r *codeRepo) findDir(version string) (rev, dir string, gomod []byte, err error) {
610 rev, err = r.versionToRev(version)
611 if err != nil {
612 return "", "", nil, err
613 }
614
615
616
617 file1 := path.Join(r.codeDir, "go.mod")
618 gomod1, err1 := r.code.ReadFile(rev, file1, codehost.MaxGoMod)
619 if err1 != nil && !os.IsNotExist(err1) {
620 return "", "", nil, fmt.Errorf("reading %s/%s at revision %s: %v", r.pathPrefix, file1, rev, err1)
621 }
622 mpath1 := modfile.ModulePath(gomod1)
623 found1 := err1 == nil && isMajor(mpath1, r.pathMajor)
624
625 var file2 string
626 if r.pathMajor != "" && r.codeRoot != r.modPath && !strings.HasPrefix(r.pathMajor, ".") {
627
628
629
630
631
632
633
634
635
636
637 dir2 := path.Join(r.codeDir, r.pathMajor[1:])
638 file2 = path.Join(dir2, "go.mod")
639 gomod2, err2 := r.code.ReadFile(rev, file2, codehost.MaxGoMod)
640 if err2 != nil && !os.IsNotExist(err2) {
641 return "", "", nil, fmt.Errorf("reading %s/%s at revision %s: %v", r.pathPrefix, file2, rev, err2)
642 }
643 mpath2 := modfile.ModulePath(gomod2)
644 found2 := err2 == nil && isMajor(mpath2, r.pathMajor)
645
646 if found1 && found2 {
647 return "", "", nil, fmt.Errorf("%s/%s and ...%s/go.mod both have ...%s module paths at revision %s", r.pathPrefix, file1, r.pathMajor, r.pathMajor, rev)
648 }
649 if found2 {
650 return rev, dir2, gomod2, nil
651 }
652 if err2 == nil {
653 if mpath2 == "" {
654 return "", "", nil, fmt.Errorf("%s/%s is missing module path at revision %s", r.pathPrefix, file2, rev)
655 }
656 return "", "", nil, fmt.Errorf("%s/%s has non-...%s module path %q at revision %s", r.pathPrefix, file2, r.pathMajor, mpath2, rev)
657 }
658 }
659
660
661 if found1 {
662
663 return rev, r.codeDir, gomod1, nil
664 }
665 if err1 == nil {
666
667 suffix := ""
668 if file2 != "" {
669 suffix = fmt.Sprintf(" (and ...%s/go.mod does not exist)", r.pathMajor)
670 }
671 if mpath1 == "" {
672 return "", "", nil, fmt.Errorf("%s is missing module path%s at revision %s", file1, suffix, rev)
673 }
674 if r.pathMajor != "" {
675 return "", "", nil, fmt.Errorf("%s has non-...%s module path %q%s at revision %s", file1, r.pathMajor, mpath1, suffix, rev)
676 }
677 return "", "", nil, fmt.Errorf("%s has post-%s module path %q%s at revision %s", file1, semver.Major(version), mpath1, suffix, rev)
678 }
679
680 if r.codeDir == "" && (r.pathMajor == "" || strings.HasPrefix(r.pathMajor, ".")) {
681
682 return rev, "", nil, nil
683 }
684
685
686
687 if file2 != "" {
688 return "", "", nil, fmt.Errorf("missing %s/go.mod and ...%s/go.mod at revision %s", r.pathPrefix, r.pathMajor, rev)
689 }
690 return "", "", nil, fmt.Errorf("missing %s/go.mod at revision %s", r.pathPrefix, rev)
691 }
692
693 func isMajor(mpath, pathMajor string) bool {
694 if mpath == "" {
695 return false
696 }
697 if pathMajor == "" {
698
699 i := len(mpath)
700 for i > 0 && '0' <= mpath[i-1] && mpath[i-1] <= '9' {
701 i--
702 }
703 if i < len(mpath) && i >= 2 && mpath[i-1] == 'v' && mpath[i-2] == '/' {
704
705 return false
706 }
707 return true
708 }
709
710 return strings.HasSuffix(mpath, pathMajor)
711 }
712
713 func (r *codeRepo) GoMod(version string) (data []byte, err error) {
714 if version != module.CanonicalVersion(version) {
715 return nil, fmt.Errorf("version %s is not canonical", version)
716 }
717
718 if IsPseudoVersion(version) {
719
720
721
722
723 _, err := r.Stat(version)
724 if err != nil {
725 return nil, err
726 }
727 }
728
729 rev, dir, gomod, err := r.findDir(version)
730 if err != nil {
731 return nil, err
732 }
733 if gomod != nil {
734 return gomod, nil
735 }
736 data, err = r.code.ReadFile(rev, path.Join(dir, "go.mod"), codehost.MaxGoMod)
737 if err != nil {
738 if os.IsNotExist(err) {
739 return r.legacyGoMod(rev, dir), nil
740 }
741 return nil, err
742 }
743 return data, nil
744 }
745
746 func (r *codeRepo) legacyGoMod(rev, dir string) []byte {
747
748
749
750
751
752
753
754 return []byte(fmt.Sprintf("module %s\n", modfile.AutoQuote(r.modPath)))
755 }
756
757 func (r *codeRepo) modPrefix(rev string) string {
758 return r.modPath + "@" + rev
759 }
760
761 func (r *codeRepo) Zip(dst io.Writer, version string) error {
762 if version != module.CanonicalVersion(version) {
763 return fmt.Errorf("version %s is not canonical", version)
764 }
765
766 if IsPseudoVersion(version) {
767
768
769
770
771 _, err := r.Stat(version)
772 if err != nil {
773 return err
774 }
775 }
776
777 rev, dir, _, err := r.findDir(version)
778 if err != nil {
779 return err
780 }
781 dl, actualDir, err := r.code.ReadZip(rev, dir, codehost.MaxZipFile)
782 if err != nil {
783 return err
784 }
785 defer dl.Close()
786 if actualDir != "" && !hasPathPrefix(dir, actualDir) {
787 return fmt.Errorf("internal error: downloading %v %v: dir=%q but actualDir=%q", r.modPath, rev, dir, actualDir)
788 }
789 subdir := strings.Trim(strings.TrimPrefix(dir, actualDir), "/")
790
791
792 f, err := ioutil.TempFile("", "go-codehost-")
793 if err != nil {
794 dl.Close()
795 return err
796 }
797 defer os.Remove(f.Name())
798 defer f.Close()
799 maxSize := int64(codehost.MaxZipFile)
800 lr := &io.LimitedReader{R: dl, N: maxSize + 1}
801 if _, err := io.Copy(f, lr); err != nil {
802 dl.Close()
803 return err
804 }
805 dl.Close()
806 if lr.N <= 0 {
807 return fmt.Errorf("downloaded zip file too large")
808 }
809 size := (maxSize + 1) - lr.N
810 if _, err := f.Seek(0, 0); err != nil {
811 return err
812 }
813
814
815 zr, err := zip.NewReader(f, size)
816 if err != nil {
817 return err
818 }
819
820 zw := zip.NewWriter(dst)
821 if subdir != "" {
822 subdir += "/"
823 }
824 haveLICENSE := false
825 topPrefix := ""
826 haveGoMod := make(map[string]bool)
827 for _, zf := range zr.File {
828 if topPrefix == "" {
829 i := strings.Index(zf.Name, "/")
830 if i < 0 {
831 return fmt.Errorf("missing top-level directory prefix")
832 }
833 topPrefix = zf.Name[:i+1]
834 }
835 if !strings.HasPrefix(zf.Name, topPrefix) {
836 return fmt.Errorf("zip file contains more than one top-level directory")
837 }
838 dir, file := path.Split(zf.Name)
839 if file == "go.mod" {
840 haveGoMod[dir] = true
841 }
842 }
843 root := topPrefix + subdir
844 inSubmodule := func(name string) bool {
845 for {
846 dir, _ := path.Split(name)
847 if len(dir) <= len(root) {
848 return false
849 }
850 if haveGoMod[dir] {
851 return true
852 }
853 name = dir[:len(dir)-1]
854 }
855 }
856
857 for _, zf := range zr.File {
858 if !zf.FileInfo().Mode().IsRegular() {
859
860 continue
861 }
862
863 if topPrefix == "" {
864 i := strings.Index(zf.Name, "/")
865 if i < 0 {
866 return fmt.Errorf("missing top-level directory prefix")
867 }
868 topPrefix = zf.Name[:i+1]
869 }
870 if strings.HasSuffix(zf.Name, "/") {
871 continue
872 }
873 if !strings.HasPrefix(zf.Name, topPrefix) {
874 return fmt.Errorf("zip file contains more than one top-level directory")
875 }
876 name := strings.TrimPrefix(zf.Name, topPrefix)
877 if !strings.HasPrefix(name, subdir) {
878 continue
879 }
880 if name == ".hg_archival.txt" {
881
882
883 continue
884 }
885 name = strings.TrimPrefix(name, subdir)
886 if isVendoredPackage(name) {
887 continue
888 }
889 if inSubmodule(zf.Name) {
890 continue
891 }
892 base := path.Base(name)
893 if strings.ToLower(base) == "go.mod" && base != "go.mod" {
894 return fmt.Errorf("zip file contains %s, want all lower-case go.mod", zf.Name)
895 }
896 if name == "LICENSE" {
897 haveLICENSE = true
898 }
899 size := int64(zf.UncompressedSize64)
900 if size < 0 || maxSize < size {
901 return fmt.Errorf("module source tree too big")
902 }
903 maxSize -= size
904
905 rc, err := zf.Open()
906 if err != nil {
907 return err
908 }
909 w, err := zw.Create(r.modPrefix(version) + "/" + name)
910 if err != nil {
911 return err
912 }
913 lr := &io.LimitedReader{R: rc, N: size + 1}
914 if _, err := io.Copy(w, lr); err != nil {
915 return err
916 }
917 if lr.N <= 0 {
918 return fmt.Errorf("individual file too large")
919 }
920 }
921
922 if !haveLICENSE && subdir != "" {
923 data, err := r.code.ReadFile(rev, "LICENSE", codehost.MaxLICENSE)
924 if err == nil {
925 w, err := zw.Create(r.modPrefix(version) + "/LICENSE")
926 if err != nil {
927 return err
928 }
929 if _, err := w.Write(data); err != nil {
930 return err
931 }
932 }
933 }
934
935 return zw.Close()
936 }
937
938
939
940 func hasPathPrefix(s, prefix string) bool {
941 switch {
942 default:
943 return false
944 case len(s) == len(prefix):
945 return s == prefix
946 case len(s) > len(prefix):
947 if prefix != "" && prefix[len(prefix)-1] == '/' {
948 return strings.HasPrefix(s, prefix)
949 }
950 return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
951 }
952 }
953
954 func isVendoredPackage(name string) bool {
955 var i int
956 if strings.HasPrefix(name, "vendor/") {
957 i += len("vendor/")
958 } else if j := strings.Index(name, "/vendor/"); j >= 0 {
959
960
961
962
963
964
965
966
967
968
969
970
971
972 i += len("/vendor/")
973 } else {
974 return false
975 }
976 return strings.Contains(name[i:], "/")
977 }
978
View as plain text