Commit 1803ab1e authored by Bryan C. Mills's avatar Bryan C. Mills

cmd/go: validate pseudo-versions against module paths and revision metadata

Previously, most operations involving pseudo-versions allowed any
arbitrary combination of version string and date, and would resolve to
the underlying revision (typically a Git commit hash) as long as that
revision existed.

There are a number of problems with that approach:

• The pseudo-version participates in minimal version selection. If its
  version prefix is inaccurate, the pseudo-version may appear to have
  higher precedence that the releases that follow it, effectively
  “pinning” the module to that commit. For release tags, module
  authors are the ones who make the decision about release tagging;
  they should also have control over the pseudo-version precedence
  within their module.

• The commit date within the pseudo-version provides a total order
  among pseudo-versions. If it is not accurate, the pseudo-version
  will sort into the wrong place relative to other commits with the
  same version prefix.

To address those problems, this change restricts the pseudo-versions
that the 'go' command accepts, rendering some previously
accepted-but-not-canonical versions invalid. A pseudo-version is now
valid only if all of:

1. The tag from which the pseudo-version derives points to the named
   revision or one of its ancestors as reported by the underlying VCS
   tool, or the pseudo-version is not derived from any tag (that is,
   has a "vX.0.0-" prefix before the date string and uses the lowest
   major version appropriate to the module path).

2. The date string within the pseudo-version matches the UTC timestamp
   of the revision as reported by the underlying VCS tool.

3. The short name of the revision within the pseudo-version (such as a
   Git hash prefix) is the same as the short name reported by the
   underlying cmd/go/internal/modfetch/codehost.Repo. Specifically, if
   the short name is a SHA-1 prefix, it must use the same number of
   hex digits (12) as codehost.ShortenSHA1.

4. The pseudo-version includes a '+incompatible' suffix only if it is
   needed for the corresponding major version, and only if the
   underlying module does not have a go.mod file.

We believe that all releases of the 'go' tool have generated
pseudo-versions that meet these constraints. However, a few
pseudo-versions edited by hand or generated by third-party tools do
not. If we discover invalid-but-benign pseudo-versions in widely-used
existing dependencies, we may choose to add a whitelist for those
specific path/version combinations.

―

To work around invalid dependencies in leaf modules, users may add a
'replace' directive from the invalid version to its valid equivalent.
Note that the go command's go.mod parser automatically resolves commit
hashes found in 'replace' directives to the appropriate
pseudo-versions, so in most cases one can write something like:

	replace github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c => github.com/docker/docker e7b5f7dbe98c

and then run any 'go' command (such as 'go list' or 'go mod tidy') to
resolve it to an appropriate pseudo-version. Note that the invalid
version will still be used in minimal version selection, so this use
of 'replace' directives is an incomplete workaround.

―

One of the common use cases for higher-than-tagged pseudo-versions is
for projects that do parallel development on release branches. For
example, if a project cuts a 'v1.2' release branch at v1.2.0, they may
want future commits on the main branch to show up as pre-releases for
v1.3.0 rather than for v1.2.1 — especially if v1.2.1 is already tagged
on the release branch. (On the other hand, a backport of a patch to
the v1.2 branch should not show up as a pre-release for v1.3.0.)

To address this use-case, module authors can make use of our existing
support for pseudo-versions derived from pre-release tags: if the
author adds an explicit pre-release tag (such as 'v1.3.0-devel') to
the first commit after the branch, then the pseudo-versions for that
commit and its descendents will be derived from that tag and will sort
appropriately in version selection.

―

Updates #27171
Fixes #29262
Fixes #27173
Fixes #32662
Fixes #32695

Change-Id: I0d50a538b6fdb0d3080aca9c9c3df1040da1b329
Reviewed-on: https://go-review.googlesource.com/c/go/+/181881
Run-TryBot: Bryan C. Mills <bcmills@google.com>
Reviewed-by: default avatarJay Conrod <jayconrod@google.com>
parent 851616d2
......@@ -161,6 +161,64 @@ TODO
TODO
</p>
<h3 id="modules">Modules</h3>
<h4 id="version-validation">Version validation</h4><!-- CL 181881 -->
<p>
When extracting a module from a version control system, the <code>go</code>
command now performs additional validation on the requested version string.
</p>
<p>
The <code>+incompatible</code> version annotation bypasses the requirement
of <a href="/cmd/go/#hdr-Module_compatibility_and_semantic_versioning">semantic
import versioning</a> for repositories that predate the introduction of
modules. The <code>go</code> command now verifies that such a version does not
include an explicit <code>go.mod</code> file.
</p>
<p>
The <code>go</code> command now verifies the mapping
between <a href="/cmd/go#hdr-Pseudo_versions">pseudo-versions</a> and
version-control metadata. Specifically:
<ul>
<li>The version prefix must be derived from a tag on the named revision or
one of its ancestors, or be of the form <code>vX.0.0</code>.</li>
<li>The date string must match the UTC timestamp of the revision.</li>
<li>The short name of the revision must use the same number of characters as
what the <code>go</code> command would generate. (For SHA-1 hashes as used
by <code>git</code>, a 12-digit prefix.)</li>
</ul>
</p>
<p>
If the main module directly requires a version that fails the above
validation, a corrected version can be obtained by redacting the version to
just the commit hash and re-running a <code>go</code> command such as <code>go
list -m all</code> or <code>go mod tidy</code>. For example,
<pre>require github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c</pre>
can be redacted to
<pre>require github.com/docker/docker e7b5f7dbe98c</pre>
which resolves to
<pre>require github.com/docker/docker v0.7.3-0.20190319215453-e7b5f7dbe98c</pre>
</p>
<p>
If the main module has a transitive requirement on a version that fails
validation, the invalid version can still be replaced with a valid one through
the use of a <a href="/cmd/go/#hdr-The_go_mod_file"><code>replace</code>
directive</a> in the <code>go.mod</code> file of
the <a href="/cmd/go/#hdr-The_main_module_and_the_build_list">main module</a>.
If the replacement is a commit hash, it will be resolved to the appropriate
pseudo-version. For example,
<pre>replace github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c => github.com/docker/docker e7b5f7dbe98c</pre>
resolves to
<pre>replace github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c => github.com/docker/docker v0.7.3-0.20190319215453-e7b5f7dbe98c</pre>
</p>
<h3 id="compiler">Compiler toolchain</h3>
<p><!-- CL 170448 -->
......
......@@ -128,7 +128,7 @@ func TestConvertLegacyConfig(t *testing.T) {
{
// golang.org/issue/24585 - confusion about v2.0.0 tag in legacy non-v2 module
"github.com/fishy/gcsbucket", "v0.0.0-20150410205453-618d60fe84e0",
"github.com/fishy/gcsbucket", "v0.0.0-20180217031846-618d60fe84e0",
`module github.com/fishy/gcsbucket
require (
......
......@@ -216,29 +216,21 @@ func (r *cachingRepo) Latest() (*RevInfo, error) {
return &info, nil
}
func (r *cachingRepo) GoMod(rev string) ([]byte, error) {
func (r *cachingRepo) GoMod(version string) ([]byte, error) {
type cached struct {
text []byte
err error
}
c := r.cache.Do("gomod:"+rev, func() interface{} {
file, text, err := readDiskGoMod(r.path, rev)
c := r.cache.Do("gomod:"+version, func() interface{} {
file, text, err := readDiskGoMod(r.path, version)
if err == nil {
// Note: readDiskGoMod already called checkGoMod.
return cached{text, nil}
}
// Convert rev to canonical version
// so that we use the right identifier in the go.sum check.
info, err := r.Stat(rev)
if err != nil {
return cached{nil, err}
}
rev = info.Version
text, err = r.r.GoMod(rev)
text, err = r.r.GoMod(version)
if err == nil {
checkGoMod(r.path, rev, text)
checkGoMod(r.path, version, text)
if err := writeDiskGoMod(file, text); err != nil {
fmt.Fprintf(os.Stderr, "go: writing go.mod cache: %v\n", err)
}
......
......@@ -79,14 +79,16 @@ type Repo interface {
// nested in a single top-level directory, whose name is not specified.
ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error)
// RecentTag returns the most recent tag at or before the given rev
// with the given prefix. It should make a best-effort attempt to
// find a tag that is a valid semantic version (following the prefix),
// or else the result is not useful to the caller, but it need not
// incur great expense in doing so. For example, the git implementation
// of RecentTag limits git's search to tags matching the glob expression
// "v[0-9]*.[0-9]*.[0-9]*" (after the prefix).
RecentTag(rev, prefix string) (tag string, err error)
// RecentTag returns the most recent tag on rev or one of its predecessors
// with the given prefix and major version.
// An empty major string matches any major version.
RecentTag(rev, prefix, major string) (tag string, err error)
// DescendsFrom reports whether rev or any of its ancestors has the given tag.
//
// DescendsFrom must return true for any tag returned by RecentTag for the
// same revision.
DescendsFrom(rev, tag string) (bool, error)
}
// A Rev describes a single revision in a source code repository.
......@@ -105,6 +107,20 @@ type FileRev struct {
Err error // error if any; os.IsNotExist(Err)==true if rev exists but file does not exist in that rev
}
// UnknownRevisionError is an error equivalent to os.ErrNotExist, but for a
// revision rather than a file.
type UnknownRevisionError struct {
Rev string
}
func (e *UnknownRevisionError) Error() string {
return "unknown revision " + e.Rev
}
func (e *UnknownRevisionError) Is(err error) bool {
return err == os.ErrNotExist
}
// AllHex reports whether the revision rev is entirely lower-case hexadecimal digits.
func AllHex(rev string) bool {
for i := 0; i < len(rev); i++ {
......
......@@ -10,6 +10,7 @@ import (
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
......@@ -318,7 +319,7 @@ func (r *gitRepo) stat(rev string) (*RevInfo, error) {
hash = rev
}
} else {
return nil, fmt.Errorf("unknown revision %s", rev)
return nil, &UnknownRevisionError{Rev: rev}
}
// Protect r.fetchLevel and the "fetch more and more" sequence.
......@@ -378,17 +379,30 @@ func (r *gitRepo) stat(rev string) (*RevInfo, error) {
// Last resort.
// Fetch all heads and tags and hope the hash we want is in the history.
if err := r.fetchRefsLocked(); err != nil {
return nil, err
}
return r.statLocal(rev, rev)
}
// fetchRefsLocked fetches all heads and tags from the origin, along with the
// ancestors of those commits.
//
// We only fetch heads and tags, not arbitrary other commits: we don't want to
// pull in off-branch commits (such as rejected GitHub pull requests) that the
// server may be willing to provide. (See the comments within the stat method
// for more detail.)
//
// fetchRefsLocked requires that r.mu remain locked for the duration of the call.
func (r *gitRepo) fetchRefsLocked() error {
if r.fetchLevel < fetchAll {
// TODO(bcmills): should we wait to upgrade fetchLevel until after we check
// err? If there is a temporary server error, we want subsequent fetches to
// try again instead of proceeding with an incomplete repo.
r.fetchLevel = fetchAll
if err := r.fetchUnshallow("refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
return nil, err
return err
}
r.fetchLevel = fetchAll
}
return r.statLocal(rev, rev)
return nil
}
func (r *gitRepo) fetchUnshallow(refSpecs ...string) error {
......@@ -411,7 +425,7 @@ func (r *gitRepo) fetchUnshallow(refSpecs ...string) error {
func (r *gitRepo) statLocal(version, rev string) (*RevInfo, error) {
out, err := Run(r.dir, "git", "-c", "log.showsignature=false", "log", "-n1", "--format=format:%H %ct %D", rev, "--")
if err != nil {
return nil, fmt.Errorf("unknown revision %s", rev)
return nil, &UnknownRevisionError{Rev: rev}
}
f := strings.Fields(string(out))
if len(f) < 2 {
......@@ -648,7 +662,7 @@ func (r *gitRepo) readFileRevs(tags []string, file string, fileMap map[string]*F
return missing, nil
}
func (r *gitRepo) RecentTag(rev, prefix string) (tag string, err error) {
func (r *gitRepo) RecentTag(rev, prefix, major string) (tag string, err error) {
info, err := r.Stat(rev)
if err != nil {
return "", err
......@@ -681,7 +695,7 @@ func (r *gitRepo) RecentTag(rev, prefix string) (tag string, err error) {
semtag := line[len(prefix):]
// Consider only tags that are valid and complete (not just major.minor prefixes).
if c := semver.Canonical(semtag); c != "" && strings.HasPrefix(semtag, c) {
if c := semver.Canonical(semtag); c != "" && strings.HasPrefix(semtag, c) && (major == "" || semver.Major(c) == major) {
highest = semver.Max(highest, semtag)
}
}
......@@ -716,12 +730,8 @@ func (r *gitRepo) RecentTag(rev, prefix string) (tag string, err error) {
}
defer unlock()
if r.fetchLevel < fetchAll {
// Fetch all heads and tags and see if that gives us enough history.
if err := r.fetchUnshallow("refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
return "", err
}
r.fetchLevel = fetchAll
if err := r.fetchRefsLocked(); err != nil {
return "", err
}
// If we've reached this point, we have all of the commits that are reachable
......@@ -738,6 +748,67 @@ func (r *gitRepo) RecentTag(rev, prefix string) (tag string, err error) {
return tag, err
}
func (r *gitRepo) DescendsFrom(rev, tag string) (bool, error) {
// The "--is-ancestor" flag was added to "git merge-base" in version 1.8.0, so
// this won't work with Git 1.7.1. According to golang.org/issue/28550, cmd/go
// already doesn't work with Git 1.7.1, so at least it's not a regression.
//
// git merge-base --is-ancestor exits with status 0 if rev is an ancestor, or
// 1 if not.
_, err := Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
// Git reports "is an ancestor" with exit code 0 and "not an ancestor" with
// exit code 1.
// Unfortunately, if we've already fetched rev with a shallow history, git
// merge-base has been observed to report a false-negative, so don't stop yet
// even if the exit code is 1!
if err == nil {
return true, nil
}
// See whether the tag and rev even exist.
tags, err := r.Tags(tag)
if err != nil {
return false, err
}
if len(tags) == 0 {
return false, nil
}
// NOTE: r.stat is very careful not to fetch commits that we shouldn't know
// about, like rejected GitHub pull requests, so don't try to short-circuit
// that here.
if _, err = r.stat(rev); err != nil {
return false, err
}
// Now fetch history so that git can search for a path.
unlock, err := r.mu.Lock()
if err != nil {
return false, err
}
defer unlock()
if r.fetchLevel < fetchAll {
// Fetch the complete history for all refs and heads. It would be more
// efficient to only fetch the history from rev to tag, but that's much more
// complicated, and any kind of shallow fetch is fairly likely to trigger
// bugs in JGit servers and/or the go command anyway.
if err := r.fetchRefsLocked(); err != nil {
return false, err
}
}
_, err = Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
if err == nil {
return true, nil
}
if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
return false, nil
}
return false, err
}
func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error) {
// TODO: Use maxSize or drop it.
args := []string{}
......
......@@ -347,7 +347,7 @@ func (r *vcsRepo) fetch() {
func (r *vcsRepo) statLocal(rev string) (*RevInfo, error) {
out, err := Run(r.dir, r.cmd.statLocal(rev, r.remote))
if err != nil {
return nil, vcsErrorf("unknown revision %s", rev)
return nil, &UnknownRevisionError{Rev: rev}
}
return r.cmd.parseStat(rev, string(out))
}
......@@ -392,7 +392,7 @@ func (r *vcsRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[s
return nil, vcsErrorf("ReadFileRevs not implemented")
}
func (r *vcsRepo) RecentTag(rev, prefix string) (tag string, err error) {
func (r *vcsRepo) RecentTag(rev, prefix, major string) (tag string, err error) {
// We don't technically need to lock here since we're returning an error
// uncondititonally, but doing so anyway will help to avoid baking in
// lock-inversion bugs.
......@@ -405,6 +405,16 @@ func (r *vcsRepo) RecentTag(rev, prefix string) (tag string, err error) {
return "", vcsErrorf("RecentTag not implemented")
}
func (r *vcsRepo) DescendsFrom(rev, tag string) (bool, error) {
unlock, err := r.mu.Lock()
if err != nil {
return false, err
}
defer unlock()
return false, vcsErrorf("DescendsFrom not implemented")
}
func (r *vcsRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error) {
if r.cmd.readZip == nil {
return nil, "", vcsErrorf("ReadZip not implemented for %s", r.cmd.vcs)
......
This diff is collapsed.
......@@ -7,7 +7,6 @@ package modfetch
import (
"archive/zip"
"internal/testenv"
"io"
"io/ioutil"
"log"
"os"
......@@ -695,21 +694,10 @@ func TestLatest(t *testing.T) {
// fixedTagsRepo is a fake codehost.Repo that returns a fixed list of tags
type fixedTagsRepo struct {
tags []string
codehost.Repo
}
func (ch *fixedTagsRepo) Tags(string) ([]string, error) { return ch.tags, nil }
func (ch *fixedTagsRepo) Latest() (*codehost.RevInfo, error) { panic("not impl") }
func (ch *fixedTagsRepo) ReadFile(string, string, int64) ([]byte, error) { panic("not impl") }
func (ch *fixedTagsRepo) ReadFileRevs([]string, string, int64) (map[string]*codehost.FileRev, error) {
panic("not impl")
}
func (ch *fixedTagsRepo) ReadZip(string, string, int64) (io.ReadCloser, string, error) {
panic("not impl")
}
func (ch *fixedTagsRepo) RecentTag(string, string) (string, error) {
panic("not impl")
}
func (ch *fixedTagsRepo) Stat(string) (*codehost.RevInfo, error) { panic("not impl") }
func (ch *fixedTagsRepo) Tags(string) ([]string, error) { return ch.tags, nil }
func TestNonCanonicalSemver(t *testing.T) {
root := "golang.org/x/issue24476"
......
......@@ -281,6 +281,12 @@ func (p *proxyRepo) Stat(rev string) (*RevInfo, error) {
if err := json.Unmarshal(data, info); err != nil {
return nil, err
}
if info.Version != rev && rev == module.CanonicalVersion(rev) && module.Check(p.path, rev) == nil {
// If we request a correct, appropriate version for the module path, the
// proxy must return either exactly that version or an error — not some
// arbitrary other version.
return nil, fmt.Errorf("requested canonical version %s, but proxy returned info for version %s", rev, info.Version)
}
return info, nil
}
......@@ -298,6 +304,10 @@ func (p *proxyRepo) Latest() (*RevInfo, error) {
}
func (p *proxyRepo) GoMod(version string) ([]byte, error) {
if version != module.CanonicalVersion(version) {
return nil, fmt.Errorf("version %s is not canonical", version)
}
encVer, err := module.EncodeVersion(version)
if err != nil {
return nil, err
......@@ -310,6 +320,10 @@ func (p *proxyRepo) GoMod(version string) ([]byte, error) {
}
func (p *proxyRepo) Zip(dst io.Writer, version string) error {
if version != module.CanonicalVersion(version) {
return fmt.Errorf("version %s is not canonical", version)
}
encVer, err := module.EncodeVersion(version)
if err != nil {
return err
......
......@@ -35,13 +35,18 @@
package modfetch
import (
"cmd/go/internal/semver"
"errors"
"fmt"
"internal/lazyregexp"
"strings"
"time"
"cmd/go/internal/module"
"cmd/go/internal/semver"
"internal/lazyregexp"
)
var pseudoVersionRE = lazyregexp.New(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`)
// PseudoVersion returns a pseudo-version for the given major version ("v1")
// preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time,
// and revision identifier (usually a 12-byte commit hash prefix).
......@@ -49,7 +54,6 @@ func PseudoVersion(major, older string, t time.Time, rev string) string {
if major == "" {
major = "v0"
}
major = strings.TrimSuffix(major, "-unstable") // make gopkg.in/macaroon-bakery.v2-unstable use "v2"
segment := fmt.Sprintf("%s-%s", t.UTC().Format("20060102150405"), rev)
build := semver.Build(older)
older = semver.Canonical(older)
......@@ -65,11 +69,16 @@ func PseudoVersion(major, older string, t time.Time, rev string) string {
i := strings.LastIndex(older, ".") + 1
v, patch := older[:i], older[i:]
// Increment PATCH by adding 1 to decimal:
// scan right to left turning 9s to 0s until you find a digit to increment.
// (Number might exceed int64, but math/big is overkill.)
digits := []byte(patch)
for i = len(digits) - 1; i >= 0 && digits[i] == '9'; i-- {
// Reassemble.
return v + incDecimal(patch) + "-0." + segment + build
}
// incDecimal returns the decimal string incremented by 1.
func incDecimal(decimal string) string {
// Scan right to left turning 9s to 0s until you find a digit to increment.
digits := []byte(decimal)
i := len(digits) - 1
for ; i >= 0 && digits[i] == '9'; i-- {
digits[i] = '0'
}
if i >= 0 {
......@@ -79,13 +88,29 @@ func PseudoVersion(major, older string, t time.Time, rev string) string {
digits[0] = '1'
digits = append(digits, '0')
}
patch = string(digits)
// Reassemble.
return v + patch + "-0." + segment + build
return string(digits)
}
var pseudoVersionRE = lazyregexp.New(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+incompatible)?$`)
// decDecimal returns the decimal string decremented by 1, or the empty string
// if the decimal is all zeroes.
func decDecimal(decimal string) string {
// Scan right to left turning 0s to 9s until you find a digit to decrement.
digits := []byte(decimal)
i := len(digits) - 1
for ; i >= 0 && digits[i] == '0'; i-- {
digits[i] = '9'
}
if i < 0 {
// decimal is all zeros
return ""
}
if i == 0 && digits[i] == '1' && len(digits) > 1 {
digits = digits[1:]
} else {
digits[i]--
}
return string(digits)
}
// IsPseudoVersion reports whether v is a pseudo-version.
func IsPseudoVersion(v string) bool {
......@@ -96,13 +121,17 @@ func IsPseudoVersion(v string) bool {
// It returns an error if v is not a pseudo-version or if the time stamp
// embedded in the pseudo-version is not a valid time.
func PseudoVersionTime(v string) (time.Time, error) {
timestamp, _, err := parsePseudoVersion(v)
_, timestamp, _, _, err := parsePseudoVersion(v)
if err != nil {
return time.Time{}, err
}
t, err := time.Parse("20060102150405", timestamp)
if err != nil {
return time.Time{}, fmt.Errorf("pseudo-version with malformed time %s: %q", timestamp, v)
return time.Time{}, &module.InvalidVersionError{
Version: v,
Pseudo: true,
Err: fmt.Errorf("malformed time %q", timestamp),
}
}
return t, nil
}
......@@ -110,22 +139,99 @@ func PseudoVersionTime(v string) (time.Time, error) {
// PseudoVersionRev returns the revision identifier of the pseudo-version v.
// It returns an error if v is not a pseudo-version.
func PseudoVersionRev(v string) (rev string, err error) {
_, rev, err = parsePseudoVersion(v)
_, _, rev, _, err = parsePseudoVersion(v)
return
}
func parsePseudoVersion(v string) (timestamp, rev string, err error) {
// PseudoVersionBase returns the canonical parent version, if any, upon which
// the pseudo-version v is based.
//
// If v has no parent version (that is, if it is "vX.0.0-[…]"),
// PseudoVersionBase returns the empty string and a nil error.
func PseudoVersionBase(v string) (string, error) {
base, _, _, build, err := parsePseudoVersion(v)
if err != nil {
return "", err
}
switch pre := semver.Prerelease(base); pre {
case "":
// vX.0.0-yyyymmddhhmmss-abcdef123456 → ""
if build != "" {
// Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible
// are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag,
// but the "+incompatible" suffix implies that the major version of
// the parent tag is not compatible with the module's import path.
//
// There are a few such entries in the index generated by proxy.golang.org,
// but we believe those entries were generated by the proxy itself.
return "", &module.InvalidVersionError{
Version: v,
Pseudo: true,
Err: fmt.Errorf("lacks base version, but has build metadata %q", build),
}
}
return "", nil
case "-0":
// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z
// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible
base = strings.TrimSuffix(base, pre)
i := strings.LastIndexByte(base, '.')
if i < 0 {
panic("base from parsePseudoVersion missing patch number: " + base)
}
patch := decDecimal(base[i+1:])
if patch == "" {
// vX.0.0-0 is invalid, but has been observed in the wild in the index
// generated by requests to proxy.golang.org.
//
// NOTE(bcmills): I cannot find a historical bug that accounts for
// pseudo-versions of this form, nor have I seen such versions in any
// actual go.mod files. If we find actual examples of this form and a
// reasonable theory of how they came into existence, it seems fine to
// treat them as equivalent to vX.0.0 (especially since the invalid
// pseudo-versions have lower precedence than the real ones). For now, we
// reject them.
return "", &module.InvalidVersionError{
Version: v,
Pseudo: true,
Err: fmt.Errorf("version before %s would have negative patch number", base),
}
}
return base[:i+1] + patch + build, nil
default:
// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre
// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible
if !strings.HasSuffix(base, ".0") {
panic(`base from parsePseudoVersion missing ".0" before date: ` + base)
}
return strings.TrimSuffix(base, ".0") + build, nil
}
}
var errPseudoSyntax = errors.New("syntax error")
func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) {
if !IsPseudoVersion(v) {
return "", "", fmt.Errorf("malformed pseudo-version %q", v)
return "", "", "", "", &module.InvalidVersionError{
Version: v,
Pseudo: true,
Err: errPseudoSyntax,
}
}
v = strings.TrimSuffix(v, "+incompatible")
build = semver.Build(v)
v = strings.TrimSuffix(v, build)
j := strings.LastIndex(v, "-")
v, rev = v[:j], v[j+1:]
i := strings.LastIndex(v, "-")
if j := strings.LastIndex(v, "."); j > i {
base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0"
timestamp = v[j+1:]
} else {
base = v[:i] // "vX.0.0"
timestamp = v[i+1:]
}
return timestamp, rev, nil
return base, timestamp, rev, build, nil
}
......@@ -23,6 +23,10 @@ var pseudoTests = []struct {
{"unused", "v1.2.99999999999999999", "v1.2.100000000000000000-0.20060102150405-hash"},
{"unused", "v1.2.3-pre", "v1.2.3-pre.0.20060102150405-hash"},
{"unused", "v1.3.0-pre", "v1.3.0-pre.0.20060102150405-hash"},
{"unused", "v0.0.0--", "v0.0.0--.0.20060102150405-hash"},
{"unused", "v1.0.0+metadata", "v1.0.1-0.20060102150405-hash+metadata"},
{"unused", "v2.0.0+incompatible", "v2.0.1-0.20060102150405-hash+incompatible"},
{"unused", "v2.3.0-pre+incompatible", "v2.3.0-pre.0.20060102150405-hash+incompatible"},
}
var pseudoTime = time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
......@@ -79,3 +83,72 @@ func TestPseudoVersionRev(t *testing.T) {
}
}
}
func TestPseudoVersionBase(t *testing.T) {
for _, tt := range pseudoTests {
base, err := PseudoVersionBase(tt.version)
if err != nil {
t.Errorf("PseudoVersionBase(%q): %v", tt.version, err)
} else if base != tt.older {
t.Errorf("PseudoVersionBase(%q) = %q; want %q", tt.version, base, tt.older)
}
}
}
func TestInvalidPseudoVersionBase(t *testing.T) {
for _, in := range []string{
"v0.0.0",
"v0.0.0-", // malformed: empty prerelease
"v0.0.0-0.20060102150405-hash", // Z+1 == 0
"v0.1.0-0.20060102150405-hash", // Z+1 == 0
"v1.0.0-0.20060102150405-hash", // Z+1 == 0
"v0.0.0-20060102150405-hash+incompatible", // "+incompatible without base version
"v0.0.0-20060102150405-hash+metadata", // other metadata without base version
} {
base, err := PseudoVersionBase(in)
if err == nil || base != "" {
t.Errorf(`PseudoVersionBase(%q) = %q, %v; want "", error`, in, base, err)
}
}
}
func TestIncDecimal(t *testing.T) {
cases := []struct {
in, want string
}{
{"0", "1"},
{"1", "2"},
{"99", "100"},
{"100", "101"},
{"101", "102"},
}
for _, tc := range cases {
got := incDecimal(tc.in)
if got != tc.want {
t.Fatalf("incDecimal(%q) = %q; want %q", tc.in, tc.want, got)
}
}
}
func TestDecDecimal(t *testing.T) {
cases := []struct {
in, want string
}{
{"", ""},
{"0", ""},
{"00", ""},
{"1", "0"},
{"2", "1"},
{"99", "98"},
{"100", "99"},
{"101", "100"},
}
for _, tc := range cases {
got := decDecimal(tc.in)
if got != tc.want {
t.Fatalf("decDecimal(%q) = %q; want %q", tc.in, tc.want, got)
}
}
}
......@@ -16,7 +16,6 @@ import (
"unicode"
"cmd/go/internal/module"
"cmd/go/internal/semver"
)
// A File is the parsed, interpreted form of a go.mod file.
......@@ -214,10 +213,9 @@ func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, f
fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
old := args[1]
v, err := parseVersion(s, &args[1], fix)
v, err := parseVersion(verb, s, &args[1], fix)
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid module version %q: %v\n", f.Syntax.Name, line.Start.Line, old, err)
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
pathMajor, err := modulePathMajor(s)
......@@ -225,11 +223,8 @@ func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, f
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
if !module.MatchPathMajor(v, pathMajor) {
if pathMajor == "" {
pathMajor = "v0 or v1"
}
fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v)
if err := module.MatchPathMajor(v, pathMajor); err != nil {
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, &Error{Verb: verb, ModPath: s, Err: err})
return
}
if verb == "require" {
......@@ -265,17 +260,13 @@ func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, f
}
var v string
if arrow == 2 {
old := args[1]
v, err = parseVersion(s, &args[1], fix)
v, err = parseVersion(verb, s, &args[1], fix)
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
if !module.MatchPathMajor(v, pathMajor) {
if pathMajor == "" {
pathMajor = "v0 or v1"
}
fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v)
if err := module.MatchPathMajor(v, pathMajor); err != nil {
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, &Error{Verb: verb, ModPath: s, Err: err})
return
}
}
......@@ -296,10 +287,9 @@ func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, f
}
}
if len(args) == arrow+3 {
old := args[arrow+1]
nv, err = parseVersion(ns, &args[arrow+2], fix)
nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
if err != nil {
fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
return
}
if IsDirectoryPath(ns) {
......@@ -411,15 +401,41 @@ func parseString(s *string) (string, error) {
return t, nil
}
func parseVersion(path string, s *string, fix VersionFixer) (string, error) {
type Error struct {
Verb string
ModPath string
Err error
}
func (e *Error) Error() string {
return fmt.Sprintf("%s %s: %v", e.Verb, e.ModPath, e.Err)
}
func (e *Error) Unwrap() error { return e.Err }
func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
t, err := parseString(s)
if err != nil {
return "", err
return "", &Error{
Verb: verb,
ModPath: path,
Err: &module.InvalidVersionError{
Version: *s,
Err: err,
},
}
}
if fix != nil {
var err error
t, err = fix(path, t)
if err != nil {
if err, ok := err.(*module.ModuleError); ok {
return "", &Error{
Verb: verb,
ModPath: path,
Err: err.Err,
}
}
return "", err
}
}
......@@ -427,7 +443,14 @@ func parseVersion(path string, s *string, fix VersionFixer) (string, error) {
*s = v
return *s, nil
}
return "", fmt.Errorf("version must be of the form v1.2.3")
return "", &Error{
Verb: verb,
ModPath: path,
Err: &module.InvalidVersionError{
Version: t,
Err: errors.New("must be of the form v1.2.3"),
},
}
}
func modulePathMajor(path string) (string, error) {
......
......@@ -733,10 +733,18 @@ func fixVersion(path, vers string) (string, error) {
// Avoid the query if it looks OK.
_, pathMajor, ok := module.SplitPathVersion(path)
if !ok {
return "", fmt.Errorf("malformed module path: %s", path)
return "", &module.ModuleError{
Path: path,
Err: &module.InvalidVersionError{
Version: vers,
Err: fmt.Errorf("malformed module path %q", path),
},
}
}
if vers != "" && module.CanonicalVersion(vers) == vers && module.MatchPathMajor(vers, pathMajor) {
return vers, nil
if vers != "" && module.CanonicalVersion(vers) == vers {
if err := module.MatchPathMajor(vers, pathMajor); err == nil {
return vers, nil
}
}
info, err := Query(path, vers, "", nil)
......
......@@ -1093,18 +1093,18 @@ func (r *mvsReqs) required(mod module.Version) ([]module.Version, error) {
data, err := modfetch.GoMod(mod.Path, mod.Version)
if err != nil {
return nil, fmt.Errorf("%s@%s: %v", mod.Path, mod.Version, err)
return nil, err
}
f, err := modfile.ParseLax("go.mod", data, nil)
if err != nil {
return nil, fmt.Errorf("%s@%s: parsing go.mod: %v", mod.Path, mod.Version, err)
return nil, module.VersionError(mod, fmt.Errorf("parsing go.mod: %v", err))
}
if f.Module == nil {
return nil, fmt.Errorf("%s@%s: parsing go.mod: missing module line", mod.Path, mod.Version)
return nil, module.VersionError(mod, errors.New("parsing go.mod: missing module line"))
}
if mpath := f.Module.Mod.Path; mpath != origPath && mpath != mod.Path {
return nil, fmt.Errorf("%s@%s: parsing go.mod: unexpected module path %q", mod.Path, mod.Version, mpath)
return nil, module.VersionError(mod, fmt.Errorf("parsing go.mod: unexpected module path %q", mpath))
}
if f.Go != nil {
r.versions.LoadOrStore(mod, f.Go.Version)
......
......@@ -158,6 +158,9 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
// semantic versioning defines them to be equivalent.
if vers := module.CanonicalVersion(query); vers != "" && vers != query {
info, err = modfetch.Stat(proxy, path, vers)
if !errors.Is(err, os.ErrNotExist) {
return info, err
}
}
if err != nil {
return nil, queryErr
......
......@@ -106,12 +106,18 @@ var queryTests = []struct {
{path: queryRepo, query: "v0.2", err: `no matching versions for query "v0.2"`},
{path: queryRepo, query: "v0.0", vers: "v0.0.3"},
{path: queryRepo, query: "v1.9.10-pre2+metadata", vers: "v1.9.10-pre2.0.20190513201126-42abcb6df8ee"},
// golang.org/issue/29262: The major version for for a module without a suffix
// should be based on the most recent tag (v1 as appropriate, not v0
// unconditionally).
{path: queryRepo, query: "42abcb6df8ee", vers: "v1.9.10-pre2.0.20190513201126-42abcb6df8ee"},
{path: queryRepo, query: "v1.9.10-pre2+wrongmetadata", err: `unknown revision v1.9.10-pre2+wrongmetadata`},
{path: queryRepo, query: "v1.9.10-pre2", err: `unknown revision v1.9.10-pre2`},
{path: queryRepo, query: "latest", vers: "v1.9.9"},
{path: queryRepo, query: "latest", current: "v1.9.10-pre1", vers: "v1.9.10-pre1"},
{path: queryRepo, query: "latest", current: "v1.9.10-pre2+metadata", vers: "v1.9.10-pre2.0.20190513201126-42abcb6df8ee"},
{path: queryRepo, query: "latest", current: "v0.0.0-20990101120000-5ba9a4ea6213", vers: "v0.0.0-20990101120000-5ba9a4ea6213"},
{path: queryRepo, query: "latest", current: "v0.0.0-20190513201126-42abcb6df8ee", vers: "v0.0.0-20190513201126-42abcb6df8ee"},
{path: queryRepo, query: "latest", allow: "NOMATCH", err: `no matching versions for query "latest"`},
{path: queryRepo, query: "latest", current: "v1.9.9", allow: "NOMATCH", err: `no matching versions for query "latest" (current version is v1.9.9)`},
{path: queryRepo, query: "latest", current: "v1.99.99", err: `unknown revision v1.99.99`},
......@@ -125,20 +131,35 @@ var queryTests = []struct {
{path: queryRepo, query: ">v1.10.0", err: `no matching versions for query ">v1.10.0"`},
{path: queryRepo, query: ">=v1.10.0", err: `no matching versions for query ">=v1.10.0"`},
{path: queryRepo, query: "6cf84eb", vers: "v0.0.2-0.20180704023347-6cf84ebaea54"},
// golang.org/issue/27173: A pseudo-version may be based on the highest tag on
// any parent commit, or any existing semantically-lower tag: a given commit
// could have been a pre-release for a backport tag at any point.
{path: queryRepo, query: "3ef0cec634e0", vers: "v0.1.2-0.20180704023347-3ef0cec634e0"},
{path: queryRepo, query: "v0.1.2-0.20180704023347-3ef0cec634e0", vers: "v0.1.2-0.20180704023347-3ef0cec634e0"},
{path: queryRepo, query: "v0.1.1-0.20180704023347-3ef0cec634e0", vers: "v0.1.1-0.20180704023347-3ef0cec634e0"},
{path: queryRepo, query: "v0.0.4-0.20180704023347-3ef0cec634e0", vers: "v0.0.4-0.20180704023347-3ef0cec634e0"},
// Invalid tags are tested in cmd/go/testdata/script/mod_pseudo_invalid.txt.
{path: queryRepo, query: "start", vers: "v0.0.0-20180704023101-5e9e31667ddf"},
{path: queryRepo, query: "5e9e31667ddf", vers: "v0.0.0-20180704023101-5e9e31667ddf"},
{path: queryRepo, query: "v0.0.0-20180704023101-5e9e31667ddf", vers: "v0.0.0-20180704023101-5e9e31667ddf"},
{path: queryRepo, query: "7a1b6bf", vers: "v0.1.0"},
{path: queryRepoV2, query: "<v0.0.0", err: `no matching versions for query "<v0.0.0"`},
{path: queryRepoV2, query: "<=v0.0.0", err: `no matching versions for query "<=v0.0.0"`},
{path: queryRepoV2, query: ">v0.0.0", vers: "v2.0.0"},
{path: queryRepoV2, query: ">=v0.0.0", vers: "v2.0.0"},
{path: queryRepoV2, query: "v0.0.1+foo", vers: "v2.0.0-20180704023347-179bc86b1be3"},
{path: queryRepoV2, query: "v2", vers: "v2.5.5"},
{path: queryRepoV2, query: "v2.5", vers: "v2.5.5"},
{path: queryRepoV2, query: "v2.6", err: `no matching versions for query "v2.6"`},
{path: queryRepoV2, query: "v2.6.0-pre1", vers: "v2.6.0-pre1"},
{path: queryRepoV2, query: "latest", vers: "v2.5.5"},
{path: queryRepoV3, query: "e0cf3de987e6", vers: "v3.0.0-20180704024501-e0cf3de987e6"},
{path: queryRepoV3, query: "latest", vers: "v3.0.0-20180704024501-e0cf3de987e6"},
{path: emptyRepo, query: "latest", vers: "v0.0.0-20180704023549-7bb914627242"},
......@@ -162,10 +183,12 @@ func TestQuery(t *testing.T) {
t.Run(strings.ReplaceAll(tt.path, "/", "_")+"/"+tt.query+"/"+tt.current+"/"+allow, func(t *testing.T) {
info, err := Query(tt.path, tt.query, tt.current, allowed)
if tt.err != "" {
if err != nil && err.Error() == tt.err {
return
if err == nil {
t.Errorf("Query(%q, %q, %v) = %v, want error %q", tt.path, tt.query, allow, info.Version, tt.err)
} else if err.Error() != tt.err {
t.Errorf("Query(%q, %q, %v): %v, want error %q", tt.path, tt.query, allow, err, tt.err)
}
t.Fatalf("Query(%q, %q, %v): %v, want error %q", tt.path, tt.query, allow, err, tt.err)
return
}
if err != nil {
t.Fatalf("Query(%q, %q, %v): %v", tt.path, tt.query, allow, err)
......
......@@ -18,6 +18,7 @@ package module
// Changes to the semantics in this file require approval from rsc.
import (
"errors"
"fmt"
"sort"
"strings"
......@@ -40,6 +41,60 @@ type Version struct {
Version string `json:",omitempty"`
}
// A ModuleError indicates an error specific to a module.
type ModuleError struct {
Path string
Version string
Err error
}
// VersionError returns a ModuleError derived from a Version and error.
func VersionError(v Version, err error) error {
return &ModuleError{
Path: v.Path,
Version: v.Version,
Err: err,
}
}
func (e *ModuleError) Error() string {
if v, ok := e.Err.(*InvalidVersionError); ok {
return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err)
}
if e.Version != "" {
return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err)
}
return fmt.Sprintf("module %s: %v", e.Path, e.Err)
}
func (e *ModuleError) Unwrap() error { return e.Err }
// An InvalidVersionError indicates an error specific to a version, with the
// module path unknown or specified externally.
//
// A ModuleError may wrap an InvalidVersionError, but an InvalidVersionError
// must not wrap a ModuleError.
type InvalidVersionError struct {
Version string
Pseudo bool
Err error
}
// noun returns either "version" or "pseudo-version", depending on whether
// e.Version is a pseudo-version.
func (e *InvalidVersionError) noun() string {
if e.Pseudo {
return "pseudo-version"
}
return "version"
}
func (e *InvalidVersionError) Error() string {
return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
}
func (e *InvalidVersionError) Unwrap() error { return e.Err }
// Check checks that a given module path, version pair is valid.
// In addition to the path being a valid module path
// and the version being a valid semantic version,
......@@ -51,17 +106,14 @@ func Check(path, version string) error {
return err
}
if !semver.IsValid(version) {
return fmt.Errorf("malformed semantic version %v", version)
return &ModuleError{
Path: path,
Err: &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")},
}
}
_, pathMajor, _ := SplitPathVersion(path)
if !MatchPathMajor(version, pathMajor) {
if pathMajor == "" {
pathMajor = "v0 or v1"
}
if pathMajor[0] == '.' { // .v1
pathMajor = pathMajor[1:]
}
return fmt.Errorf("mismatched module path %v and version %v (want %v)", path, version, pathMajor)
if err := MatchPathMajor(version, pathMajor); err != nil {
return &ModuleError{Path: path, Err: err}
}
return nil
}
......@@ -320,22 +372,56 @@ func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
return prefix, pathMajor, true
}
// MatchPathMajor reports whether the semantic version v
// matches the path major version pathMajor.
func MatchPathMajor(v, pathMajor string) bool {
// MatchPathMajor returns a non-nil error if the semantic version v
// does not match the path major version pathMajor.
func MatchPathMajor(v, pathMajor string) error {
if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
}
if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
// Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
// For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
return true
return nil
}
m := semver.Major(v)
if pathMajor == "" {
return m == "v0" || m == "v1" || semver.Build(v) == "+incompatible"
if m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" {
return nil
}
pathMajor = "v0 or v1"
} else if pathMajor[0] == '/' || pathMajor[0] == '.' {
if m == pathMajor[1:] {
return nil
}
pathMajor = pathMajor[1:]
}
return &InvalidVersionError{
Version: v,
Err: fmt.Errorf("should be %s, not %s", pathMajor, semver.Major(v)),
}
}
// PathMajorPrefix returns the major-version tag prefix implied by pathMajor.
// An empty PathMajorPrefix allows either v0 or v1.
//
// Note that MatchPathMajor may accept some versions that do not actually begin
// with this prefix: namely, it accepts a 'v0.0.0-' prefix for a '.v1'
// pathMajor, even though that pathMajor implies 'v1' tagging.
func PathMajorPrefix(pathMajor string) string {
if pathMajor == "" {
return ""
}
if pathMajor[0] != '/' && pathMajor[0] != '.' {
panic("pathMajor suffix " + pathMajor + " passed to PathMajorPrefix lacks separator")
}
if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
}
m := pathMajor[1:]
if m != semver.Major(m) {
panic("pathMajor suffix " + pathMajor + "passed to PathMajorPrefix is not a valid major version")
}
return (pathMajor[0] == '/' || pathMajor[0] == '.') && m == pathMajor[1:]
return m
}
// CanonicalVersion returns the canonical form of the version string v.
......
......@@ -87,7 +87,6 @@ func (e *BuildListError) Module() module.Version {
func (e *BuildListError) Error() string {
b := &strings.Builder{}
errMsg := e.Err.Error()
stack := e.stack
// Don't print modules at the beginning of the chain without a
......@@ -97,16 +96,19 @@ func (e *BuildListError) Error() string {
stack = stack[:len(stack)-1]
}
// Don't print the last module if the error message already
// starts with module path and version.
errMentionsLast := len(stack) > 0 && strings.HasPrefix(errMsg, fmt.Sprintf("%s@%s: ", stack[0].m.Path, stack[0].m.Version))
for i := len(stack) - 1; i >= 1; i-- {
fmt.Fprintf(b, "%s@%s %s\n\t", stack[i].m.Path, stack[i].m.Version, stack[i].nextReason)
}
if errMentionsLast || len(stack) == 0 {
b.WriteString(errMsg)
if len(stack) == 0 {
b.WriteString(e.Err.Error())
} else {
fmt.Fprintf(b, "%s@%s: %s", stack[0].m.Path, stack[0].m.Version, errMsg)
// Ensure that the final module path and version are included as part of the
// error message.
if _, ok := e.Err.(*module.ModuleError); ok {
fmt.Fprintf(b, "%v", e.Err)
} else {
fmt.Fprintf(b, "%v", module.VersionError(stack[0].m, e.Err))
}
}
return b.String()
}
......
......@@ -7,18 +7,18 @@ env GOPROXY=direct
env GOSUMDB=off
go mod download rsc.io/quote@a91498bed0a73d4bb9c1fb2597925f7883bc40a7
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v0.0.0-20180709162918-a91498bed0a7.info
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v0.0.0-20180709162918-a91498bed0a7.mod
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v0.0.0-20180709162918-a91498bed0a7.zip
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-0.20180709162918-a91498bed0a7.info
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-0.20180709162918-a91498bed0a7.mod
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-0.20180709162918-a91498bed0a7.zip
go mod download rsc.io/quote@master
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v0.0.0-20180710144737-5d9f230bcfba.info
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v0.0.0-20180710144737-5d9f230bcfba.mod
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v0.0.0-20180710144737-5d9f230bcfba.zip
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-0.20180709162918-a91498bed0a7.info
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-0.20180709162918-a91498bed0a7.mod
exists $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.3-0.20180709162918-a91498bed0a7.zip
-- go.mod --
module m
-- m.go --
package m
\ No newline at end of file
package m
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment