1 // Copyright 2018 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
16 "cmd/go/internal/modfetch/codehost"
17 "cmd/go/internal/modfile"
18 "cmd/go/internal/module"
19 "cmd/go/internal/semver"
22 // A codeRepo implements modfetch.Repo using an underlying codehost.Repo.
23 type codeRepo
struct {
36 func newCodeRepo(code codehost
.Repo
, root
, path
string) (Repo
, error
) {
37 if !hasPathPrefix(path
, root
) {
38 return nil, fmt
.Errorf("mismatched repo: found %s for %s", root
, path
)
40 pathPrefix
, pathMajor
, ok
:= module
.SplitPathVersion(path
)
42 return nil, fmt
.Errorf("invalid module path %q", path
)
46 pseudoMajor
= pathMajor
[1:]
49 // At this point we might have:
50 // codeRoot = github.com/rsc/foo
51 // path = github.com/rsc/foo/bar/v2
52 // pathPrefix = github.com/rsc/foo/bar
56 // Compute codeDir = bar, the subdirectory within the repo
57 // corresponding to the module root.
58 codeDir
:= strings
.Trim(strings
.TrimPrefix(pathPrefix
, root
), "/")
59 if strings
.HasPrefix(path
, "gopkg.in/") {
60 // But gopkg.in is a special legacy case, in which pathPrefix does not start with codeRoot.
61 // For example we might have:
62 // codeRoot = gopkg.in/yaml.v2
63 // pathPrefix = gopkg.in/yaml
66 // codeDir = pathPrefix (because codeRoot is not a prefix of pathPrefix)
67 // Clear codeDir - the module root is the repo root for gopkg.in repos.
76 pathPrefix
: pathPrefix
,
78 pseudoMajor
: pseudoMajor
,
84 func (r
*codeRepo
) ModulePath() string {
88 func (r
*codeRepo
) Versions(prefix
string) ([]string, error
) {
89 // Special case: gopkg.in/macaroon-bakery.v2-unstable
90 // does not use the v2 tags (those are for macaroon-bakery.v2).
91 // It has no possible tags at all.
92 if strings
.HasPrefix(r
.modPath
, "gopkg.in/") && strings
.HasSuffix(r
.modPath
, "-unstable") {
98 p
= r
.codeDir
+ "/" + p
100 tags
, err
:= r
.code
.Tags(p
)
106 var incompatible
[]string
107 for _
, tag
:= range tags
{
108 if !strings
.HasPrefix(tag
, p
) {
113 v
= v
[len(r
.codeDir
)+1:]
115 if v
== "" || v
!= module
.CanonicalVersion(v
) ||
IsPseudoVersion(v
) {
118 if !module
.MatchPathMajor(v
, r
.pathMajor
) {
119 if r
.codeDir
== "" && r
.pathMajor
== "" && semver
.Major(v
) > "v1" {
120 incompatible
= append(incompatible
, v
)
124 list
= append(list
, v
)
127 if len(incompatible
) > 0 {
128 // Check for later versions that were created not following semantic import versioning,
129 // as indicated by the absence of a go.mod file. Those versions can be addressed
130 // by referring to them with a +incompatible suffix, as in v17.0.0+incompatible.
131 files
, err
:= r
.code
.ReadFileRevs(incompatible
, "go.mod", codehost
.MaxGoMod
)
135 for _
, rev
:= range incompatible
{
137 if os
.IsNotExist(f
.Err
) {
138 list
= append(list
, rev
+"+incompatible")
147 func (r
*codeRepo
) Stat(rev
string) (*RevInfo
, error
) {
151 codeRev
:= r
.revToRev(rev
)
152 if semver
.IsValid(codeRev
) && r
.codeDir
!= "" {
153 codeRev
= r
.codeDir
+ "/" + codeRev
155 info
, err
:= r
.code
.Stat(codeRev
)
159 return r
.convert(info
, rev
)
162 func (r
*codeRepo
) Latest() (*RevInfo
, error
) {
163 info
, err
:= r
.code
.Latest()
167 return r
.convert(info
, "")
170 func (r
*codeRepo
) convert(info
*codehost
.RevInfo
, statVers
string) (*RevInfo
, error
) {
177 // Determine version.
178 if module
.CanonicalVersion(statVers
) == statVers
&& module
.MatchPathMajor(statVers
, r
.pathMajor
) {
179 // The original call was repo.Stat(statVers), and requestedVersion is OK, so use it.
180 info2
.Version
= statVers
182 // Otherwise derive a version from a code repo tag.
183 // Tag must have a prefix matching codeDir.
189 // If this is a plain tag (no dir/ prefix)
190 // and the module path is unversioned,
191 // and if the underlying file tree has no go.mod,
192 // then allow using the tag with a +incompatible suffix.
193 canUseIncompatible
:= false
194 if r
.codeDir
== "" && r
.pathMajor
== "" {
195 _
, errGoMod
:= r
.code
.ReadFile(info
.Name
, "go.mod", codehost
.MaxGoMod
)
197 canUseIncompatible
= true
201 tagToVersion
:= func(v
string) string {
202 if !strings
.HasPrefix(v
, p
) {
206 if module
.CanonicalVersion(v
) != v ||
IsPseudoVersion(v
) {
209 if module
.MatchPathMajor(v
, r
.pathMajor
) {
212 if canUseIncompatible
{
213 return v
+ "+incompatible"
218 // If info.Version is OK, use it.
219 if v
:= tagToVersion(info
.Version
); v
!= "" {
222 // Otherwise look through all known tags for latest in semver ordering.
223 for _
, tag
:= range info
.Tags
{
224 if v
:= tagToVersion(tag
); v
!= "" && semver
.Compare(info2
.Version
, v
) < 0 {
228 // Otherwise make a pseudo-version.
229 if info2
.Version
== "" {
230 tag
, _
:= r
.code
.RecentTag(statVers
, p
)
231 v
= tagToVersion(tag
)
232 // TODO: Check that v is OK for r.pseudoMajor or else is OK for incompatible.
233 info2
.Version
= PseudoVersion(r
.pseudoMajor
, v
, info
.Time
, info
.Short
)
238 // Do not allow a successful stat of a pseudo-version for a subdirectory
239 // unless the subdirectory actually does have a go.mod.
240 if IsPseudoVersion(info2
.Version
) && r
.codeDir
!= "" {
241 _
, _
, _
, err
:= r
.findDir(info2
.Version
)
243 // TODO: It would be nice to return an error like "not a module".
244 // Right now we return "missing go.mod", which is a little confusing.
252 func (r
*codeRepo
) revToRev(rev
string) string {
253 if semver
.IsValid(rev
) {
254 if IsPseudoVersion(rev
) {
255 r
, _
:= PseudoVersionRev(rev
)
258 if semver
.Build(rev
) == "+incompatible" {
259 rev
= rev
[:len(rev
)-len("+incompatible")]
264 return r
.codeDir
+ "/" + rev
269 func (r
*codeRepo
) versionToRev(version
string) (rev
string, err error
) {
270 if !semver
.IsValid(version
) {
271 return "", fmt
.Errorf("malformed semantic version %q", version
)
273 return r
.revToRev(version
), nil
276 func (r
*codeRepo
) findDir(version
string) (rev
, dir
string, gomod
[]byte, err error
) {
277 rev
, err
= r
.versionToRev(version
)
279 return "", "", nil, err
282 // Load info about go.mod but delay consideration
283 // (except I/O error) until we rule out v2/go.mod.
284 file1
:= path
.Join(r
.codeDir
, "go.mod")
285 gomod1
, err1
:= r
.code
.ReadFile(rev
, file1
, codehost
.MaxGoMod
)
286 if err1
!= nil && !os
.IsNotExist(err1
) {
287 return "", "", nil, fmt
.Errorf("reading %s/%s at revision %s: %v", r
.pathPrefix
, file1
, rev
, err1
)
289 mpath1
:= modfile
.ModulePath(gomod1
)
290 found1
:= err1
== nil && isMajor(mpath1
, r
.pathMajor
)
293 if r
.pathMajor
!= "" && !strings
.HasPrefix(r
.pathMajor
, ".") {
294 // Suppose pathMajor is "/v2".
295 // Either go.mod should claim v2 and v2/go.mod should not exist,
296 // or v2/go.mod should exist and claim v2. Not both.
297 // Note that we don't check the full path, just the major suffix,
298 // because of replacement modules. This might be a fork of
299 // the real module, found at a different path, usable only in
300 // a replace directive.
301 dir2
:= path
.Join(r
.codeDir
, r
.pathMajor
[1:])
302 file2
= path
.Join(dir2
, "go.mod")
303 gomod2
, err2
:= r
.code
.ReadFile(rev
, file2
, codehost
.MaxGoMod
)
304 if err2
!= nil && !os
.IsNotExist(err2
) {
305 return "", "", nil, fmt
.Errorf("reading %s/%s at revision %s: %v", r
.pathPrefix
, file2
, rev
, err2
)
307 mpath2
:= modfile
.ModulePath(gomod2
)
308 found2
:= err2
== nil && isMajor(mpath2
, r
.pathMajor
)
310 if found1
&& found2
{
311 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
)
314 return rev
, dir2
, gomod2
, nil
318 return "", "", nil, fmt
.Errorf("%s/%s is missing module path at revision %s", r
.pathPrefix
, file2
, rev
)
320 return "", "", nil, fmt
.Errorf("%s/%s has non-...%s module path %q at revision %s", r
.pathPrefix
, file2
, r
.pathMajor
, mpath2
, rev
)
324 // Not v2/go.mod, so it's either go.mod or nothing. Which is it?
326 // Explicit go.mod with matching module path OK.
327 return rev
, r
.codeDir
, gomod1
, nil
330 // Explicit go.mod with non-matching module path disallowed.
333 suffix
= fmt
.Sprintf(" (and ...%s/go.mod does not exist)", r
.pathMajor
)
336 return "", "", nil, fmt
.Errorf("%s is missing module path%s at revision %s", file1
, suffix
, rev
)
338 if r
.pathMajor
!= "" { // ".v1", ".v2" for gopkg.in
339 return "", "", nil, fmt
.Errorf("%s has non-...%s module path %q%s at revision %s", file1
, r
.pathMajor
, mpath1
, suffix
, rev
)
341 return "", "", nil, fmt
.Errorf("%s has post-%s module path %q%s at revision %s", file1
, semver
.Major(version
), mpath1
, suffix
, rev
)
344 if r
.codeDir
== "" && (r
.pathMajor
== "" || strings
.HasPrefix(r
.pathMajor
, ".")) {
345 // Implicit go.mod at root of repo OK for v0/v1 and for gopkg.in.
346 return rev
, "", nil, nil
349 // Implicit go.mod below root of repo or at v2+ disallowed.
350 // Be clear about possibility of using either location for v2+.
352 return "", "", nil, fmt
.Errorf("missing %s/go.mod and ...%s/go.mod at revision %s", r
.pathPrefix
, r
.pathMajor
, rev
)
354 return "", "", nil, fmt
.Errorf("missing %s/go.mod at revision %s", r
.pathPrefix
, rev
)
357 func isMajor(mpath
, pathMajor
string) bool {
362 // mpath must NOT have version suffix.
364 for i
> 0 && '0' <= mpath
[i
-1] && mpath
[i
-1] <= '9' {
367 if i
< len(mpath
) && i
>= 2 && mpath
[i
-1] == 'v' && mpath
[i
-2] == '/' {
368 // Found valid suffix.
373 // Otherwise pathMajor is ".v1", ".v2" (gopkg.in), or "/v2", "/v3" etc.
374 return strings
.HasSuffix(mpath
, pathMajor
)
377 func (r
*codeRepo
) GoMod(version
string) (data
[]byte, err error
) {
378 rev
, dir
, gomod
, err
:= r
.findDir(version
)
385 data
, err
= r
.code
.ReadFile(rev
, path
.Join(dir
, "go.mod"), codehost
.MaxGoMod
)
387 if os
.IsNotExist(err
) {
388 return r
.legacyGoMod(rev
, dir
), nil
395 func (r
*codeRepo
) legacyGoMod(rev
, dir
string) []byte {
396 // We used to try to build a go.mod reflecting pre-existing
397 // package management metadata files, but the conversion
398 // was inherently imperfect (because those files don't have
399 // exactly the same semantics as go.mod) and, when done
400 // for dependencies in the middle of a build, impossible to
401 // correct. So we stopped.
402 // Return a fake go.mod that simply declares the module path.
403 return []byte(fmt
.Sprintf("module %s\n", modfile
.AutoQuote(r
.modPath
)))
406 func (r
*codeRepo
) modPrefix(rev
string) string {
407 return r
.modPath
+ "@" + rev
410 func (r
*codeRepo
) Zip(version
string, tmpdir
string) (tmpfile
string, err error
) {
411 rev
, dir
, _
, err
:= r
.findDir(version
)
415 dl
, actualDir
, err
:= r
.code
.ReadZip(rev
, dir
, codehost
.MaxZipFile
)
419 if actualDir
!= "" && !hasPathPrefix(dir
, actualDir
) {
420 return "", fmt
.Errorf("internal error: downloading %v %v: dir=%q but actualDir=%q", r
.path
, rev
, dir
, actualDir
)
422 subdir
:= strings
.Trim(strings
.TrimPrefix(dir
, actualDir
), "/")
424 // Spool to local file.
425 f
, err
:= ioutil
.TempFile(tmpdir
, "go-codehost-")
430 defer os
.Remove(f
.Name())
432 maxSize
:= int64(codehost
.MaxZipFile
)
433 lr
:= &io
.LimitedReader
{R
: dl
, N
: maxSize
+ 1}
434 if _
, err
:= io
.Copy(f
, lr
); err
!= nil {
440 return "", fmt
.Errorf("downloaded zip file too large")
442 size
:= (maxSize
+ 1) - lr
.N
443 if _
, err
:= f
.Seek(0, 0); err
!= nil {
447 // Translate from zip file we have to zip file we want.
448 zr
, err
:= zip
.NewReader(f
, size
)
452 f2
, err
:= ioutil
.TempFile(tmpdir
, "go-codezip-")
457 zw
:= zip
.NewWriter(f2
)
470 haveGoMod
:= make(map[string]bool)
471 for _
, zf
:= range zr
.File
{
473 i
:= strings
.Index(zf
.Name
, "/")
475 return "", fmt
.Errorf("missing top-level directory prefix")
477 topPrefix
= zf
.Name
[:i
+1]
479 if !strings
.HasPrefix(zf
.Name
, topPrefix
) {
480 return "", fmt
.Errorf("zip file contains more than one top-level directory")
482 dir
, file
:= path
.Split(zf
.Name
)
483 if file
== "go.mod" {
484 haveGoMod
[dir
] = true
487 root
:= topPrefix
+ subdir
488 inSubmodule
:= func(name
string) bool {
490 dir
, _
:= path
.Split(name
)
491 if len(dir
) <= len(root
) {
497 name
= dir
[:len(dir
)-1]
500 for _
, zf
:= range zr
.File
{
502 i
:= strings
.Index(zf
.Name
, "/")
504 return "", fmt
.Errorf("missing top-level directory prefix")
506 topPrefix
= zf
.Name
[:i
+1]
508 if strings
.HasSuffix(zf
.Name
, "/") { // drop directory dummy entries
511 if !strings
.HasPrefix(zf
.Name
, topPrefix
) {
512 return "", fmt
.Errorf("zip file contains more than one top-level directory")
514 name
:= strings
.TrimPrefix(zf
.Name
, topPrefix
)
515 if !strings
.HasPrefix(name
, subdir
) {
518 if name
== ".hg_archival.txt" {
519 // Inserted by hg archive.
520 // Not correct to drop from other version control systems, but too bad.
523 name
= strings
.TrimPrefix(name
, subdir
)
524 if isVendoredPackage(name
) {
527 if inSubmodule(zf
.Name
) {
530 base
:= path
.Base(name
)
531 if strings
.ToLower(base
) == "go.mod" && base
!= "go.mod" {
532 return "", fmt
.Errorf("zip file contains %s, want all lower-case go.mod", zf
.Name
)
534 if name
== "LICENSE" {
537 size
:= int64(zf
.UncompressedSize
)
538 if size
< 0 || maxSize
< size
{
539 return "", fmt
.Errorf("module source tree too big")
547 w
, err
:= zw
.Create(r
.modPrefix(version
) + "/" + name
)
548 lr
:= &io
.LimitedReader
{R
: rc
, N
: size
+ 1}
549 if _
, err
:= io
.Copy(w
, lr
); err
!= nil {
553 return "", fmt
.Errorf("individual file too large")
557 if !haveLICENSE
&& subdir
!= "" {
558 data
, err
:= r
.code
.ReadFile(rev
, "LICENSE", codehost
.MaxLICENSE
)
560 w
, err
:= zw
.Create(r
.modPrefix(version
) + "/LICENSE")
564 if _
, err
:= w
.Write(data
); err
!= nil {
569 if err
:= zw
.Close(); err
!= nil {
572 if err
:= f2
.Close(); err
!= nil {
576 return f2
.Name(), nil
579 // hasPathPrefix reports whether the path s begins with the
580 // elements in prefix.
581 func hasPathPrefix(s
, prefix
string) bool {
585 case len(s
) == len(prefix
):
587 case len(s
) > len(prefix
):
588 if prefix
!= "" && prefix
[len(prefix
)-1] == '/' {
589 return strings
.HasPrefix(s
, prefix
)
591 return s
[len(prefix
)] == '/' && s
[:len(prefix
)] == prefix
595 func isVendoredPackage(name
string) bool {
597 if strings
.HasPrefix(name
, "vendor/") {
599 } else if j
:= strings
.Index(name
, "/vendor/"); j
>= 0 {
604 return strings
.Contains(name
[i
:], "/")