Skip to content

Commit de70de6

Browse files
author
Bryan C. Mills
committed
cmd/go: avoid upgrading to +incompatible versions if the latest compatible one has a go.mod file
Previously we would always “upgrade” to the semantically-highest version, even if a newer compatible version exists. That made certain classes of mistakes irreversible: in general we expect users to address bad releases by releasing a new (higher) version, but if the bad release was an unintended +incompatible version, then no release that includes a go.mod file can ever have a higher version, and the bad release will be treated as “latest” forever. Instead, when considering a +incompatible version we now consult the latest compatible (v0 or v1) release first. If the compatible release contains a go.mod file, we ignore the +incompatible releases unless they are expicitly requested (by version, commit ID, or branch name). Fixes #34165 Updates #34189 Change-Id: I7301eb963bbb91b21d3b96a577644221ed988ab7 Reviewed-on: https://go-review.googlesource.com/c/go/+/204440 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 649f341 commit de70de6

File tree

4 files changed

+189
-30
lines changed

4 files changed

+189
-30
lines changed

doc/go1.14.html

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ <h2 id="tools">Tools</h2>
8181

8282
<h3 id="go-command">Go command</h3>
8383

84+
<h4 id="vendor">Vendoring</h4>
8485
<!-- golang.org/issue/33848 -->
86+
8587
<p>
8688
When the main module contains a top-level <code>vendor</code> directory and
8789
its <code>go.mod</code> file specifies <code>go</code> <code>1.14</code> or
@@ -106,20 +108,15 @@ <h3 id="go-command">Go command</h3>
106108
<code>-mod=vendor</code> is set.
107109
</p>
108110

111+
<h4 id="go-flags">Flags</h4>
112+
109113
<p><!-- golang.org/issue/32502, golang.org/issue/30345 -->
110114
The <code>go</code> <code>get</code> command no longer accepts
111115
the <code>-mod</code> flag. Previously, the flag's setting either
112116
<a href="https://golang.org/issue/30345">was ignored</a> or
113117
<a href="https://golang.org/issue/32502">caused the build to fail</a>.
114118
</p>
115119

116-
<p><!-- golang.org/issue/30748 -->
117-
The <code>go</code> command now includes snippets of plain-text error messages
118-
from module proxies and other HTTP servers.
119-
An error message will only be shown if it is valid UTF-8 and consists of only
120-
graphic characters and spaces.
121-
</p>
122-
123120
<p><!-- golang.org/issue/31481 -->
124121
<code>-modcacherw</code> is a new flag that instructs the <code>go</code>
125122
command to leave newly-created directories in the module cache at their
@@ -141,10 +138,33 @@ <h3 id="go-command">Go command</h3>
141138
trimming the ".mod" extension and appending ".sum".
142139
</p>
143140

141+
<h4 id="incompatible-versions"><code>+incompatible</code> versions</h4>
142+
<!-- golang.org/issue/34165 -->
143+
144+
<p>
145+
If the latest version of a module contains a <code>go.mod</code> file,
146+
<code>go</code> <code>get</code> will no longer upgrade to an
147+
<a href="/cmd/go/#hdr-Module_compatibility_and_semantic_versioning">incompatible</a>
148+
major version of that module unless such a version is requested explicitly
149+
or is already required.
150+
<code>go</code> <code>list</code> also omits incompatible major versions
151+
for such a module when fetching directly from version control, but may
152+
include them if reported by a proxy.
153+
</p>
154+
155+
<h4 id="module-downloading">Module downloading</h4>
156+
144157
<p><!-- golang.org/issue/26092 -->
145158
The <code>go</code> command now supports Subversion repositories in module mode.
146159
</p>
147160

161+
<p><!-- golang.org/issue/30748 -->
162+
The <code>go</code> command now includes snippets of plain-text error messages
163+
from module proxies and other HTTP servers.
164+
An error message will only be shown if it is valid UTF-8 and consists of only
165+
graphic characters and spaces.
166+
</p>
167+
148168
<h2 id="runtime">Runtime</h2>
149169

150170
<p>

src/cmd/go/internal/modfetch/repo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ type Repo interface {
5555

5656
// A Rev describes a single revision in a module repository.
5757
type RevInfo struct {
58-
Version string // version string
58+
Version string // suggested version string for this revision
5959
Time time.Time // commit time
6060

6161
// These fields are used for Stat of arbitrary rev,

src/cmd/go/internal/modload/query.go

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"os"
1111
pathpkg "path"
12+
"path/filepath"
1213
"strings"
1314
"sync"
1415

@@ -90,10 +91,20 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
9091
badVersion := func(v string) (*modfetch.RevInfo, error) {
9192
return nil, fmt.Errorf("invalid semantic version %q in range %q", v, query)
9293
}
93-
var ok func(module.Version) bool
94-
var prefix string
95-
var preferOlder bool
96-
var mayUseLatest bool
94+
matchesMajor := func(v string) bool {
95+
_, pathMajor, ok := module.SplitPathVersion(path)
96+
if !ok {
97+
return false
98+
}
99+
return module.CheckPathMajor(v, pathMajor) == nil
100+
}
101+
var (
102+
ok func(module.Version) bool
103+
prefix string
104+
preferOlder bool
105+
mayUseLatest bool
106+
preferIncompatible bool = strings.HasSuffix(current, "+incompatible")
107+
)
97108
switch {
98109
case query == "latest":
99110
ok = allowed
@@ -126,6 +137,9 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
126137
ok = func(m module.Version) bool {
127138
return semver.Compare(m.Version, v) <= 0 && allowed(m)
128139
}
140+
if !matchesMajor(v) {
141+
preferIncompatible = true
142+
}
129143

130144
case strings.HasPrefix(query, "<"):
131145
v := query[len("<"):]
@@ -135,6 +149,9 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
135149
ok = func(m module.Version) bool {
136150
return semver.Compare(m.Version, v) < 0 && allowed(m)
137151
}
152+
if !matchesMajor(v) {
153+
preferIncompatible = true
154+
}
138155

139156
case strings.HasPrefix(query, ">="):
140157
v := query[len(">="):]
@@ -145,6 +162,9 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
145162
return semver.Compare(m.Version, v) >= 0 && allowed(m)
146163
}
147164
preferOlder = true
165+
if !matchesMajor(v) {
166+
preferIncompatible = true
167+
}
148168

149169
case strings.HasPrefix(query, ">"):
150170
v := query[len(">"):]
@@ -159,12 +179,18 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
159179
return semver.Compare(m.Version, v) > 0 && allowed(m)
160180
}
161181
preferOlder = true
182+
if !matchesMajor(v) {
183+
preferIncompatible = true
184+
}
162185

163186
case semver.IsValid(query) && isSemverPrefix(query):
164187
ok = func(m module.Version) bool {
165188
return matchSemverPrefix(query, m.Version) && allowed(m)
166189
}
167190
prefix = query + "."
191+
if !matchesMajor(query) {
192+
preferIncompatible = true
193+
}
168194

169195
default:
170196
// Direct lookup of semantic version or commit identifier.
@@ -217,6 +243,10 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
217243
if err != nil {
218244
return nil, err
219245
}
246+
releases, prereleases, err := filterVersions(path, versions, ok, preferIncompatible)
247+
if err != nil {
248+
return nil, err
249+
}
220250

221251
lookup := func(v string) (*modfetch.RevInfo, error) {
222252
rev, err := repo.Stat(v)
@@ -237,28 +267,18 @@ func queryProxy(proxy, path, query, current string, allowed func(module.Version)
237267
}
238268

239269
if preferOlder {
240-
for _, v := range versions {
241-
if semver.Prerelease(v) == "" && ok(module.Version{Path: path, Version: v}) {
242-
return lookup(v)
243-
}
270+
if len(releases) > 0 {
271+
return lookup(releases[0])
244272
}
245-
for _, v := range versions {
246-
if semver.Prerelease(v) != "" && ok(module.Version{Path: path, Version: v}) {
247-
return lookup(v)
248-
}
273+
if len(prereleases) > 0 {
274+
return lookup(prereleases[0])
249275
}
250276
} else {
251-
for i := len(versions) - 1; i >= 0; i-- {
252-
v := versions[i]
253-
if semver.Prerelease(v) == "" && ok(module.Version{Path: path, Version: v}) {
254-
return lookup(v)
255-
}
277+
if len(releases) > 0 {
278+
return lookup(releases[len(releases)-1])
256279
}
257-
for i := len(versions) - 1; i >= 0; i-- {
258-
v := versions[i]
259-
if semver.Prerelease(v) != "" && ok(module.Version{Path: path, Version: v}) {
260-
return lookup(v)
261-
}
280+
if len(prereleases) > 0 {
281+
return lookup(prereleases[len(prereleases)-1])
262282
}
263283
}
264284

@@ -302,6 +322,52 @@ func matchSemverPrefix(p, v string) bool {
302322
return len(v) > len(p) && v[len(p)] == '.' && v[:len(p)] == p && semver.Prerelease(v) == ""
303323
}
304324

325+
// filterVersions classifies versions into releases and pre-releases, filtering
326+
// out:
327+
// 1. versions that do not satisfy the 'ok' predicate, and
328+
// 2. "+incompatible" versions, if a compatible one satisfies the predicate
329+
// and the incompatible version is not preferred.
330+
func filterVersions(path string, versions []string, ok func(module.Version) bool, preferIncompatible bool) (releases, prereleases []string, err error) {
331+
var lastCompatible string
332+
for _, v := range versions {
333+
if !ok(module.Version{Path: path, Version: v}) {
334+
continue
335+
}
336+
337+
if !preferIncompatible {
338+
if !strings.HasSuffix(v, "+incompatible") {
339+
lastCompatible = v
340+
} else if lastCompatible != "" {
341+
// If the latest compatible version is allowed and has a go.mod file,
342+
// ignore any version with a higher (+incompatible) major version. (See
343+
// https://golang.org/issue/34165.) Note that we even prefer a
344+
// compatible pre-release over an incompatible release.
345+
346+
ok, err := versionHasGoMod(module.Version{Path: path, Version: lastCompatible})
347+
if err != nil {
348+
return nil, nil, err
349+
}
350+
if ok {
351+
break
352+
}
353+
354+
// No acceptable compatible release has a go.mod file, so the versioning
355+
// for the module might not be module-aware, and we should respect
356+
// legacy major-version tags.
357+
preferIncompatible = true
358+
}
359+
}
360+
361+
if semver.Prerelease(v) != "" {
362+
prereleases = append(prereleases, v)
363+
} else {
364+
releases = append(releases, v)
365+
}
366+
}
367+
368+
return releases, prereleases, nil
369+
}
370+
305371
type QueryResult struct {
306372
Mod module.Version
307373
Rev *modfetch.RevInfo
@@ -590,3 +656,12 @@ func ModuleHasRootPackage(m module.Version) (bool, error) {
590656
_, ok := dirInModule(m.Path, m.Path, root, isLocal)
591657
return ok, nil
592658
}
659+
660+
func versionHasGoMod(m module.Version) (bool, error) {
661+
root, _, err := fetch(m)
662+
if err != nil {
663+
return false, err
664+
}
665+
fi, err := os.Stat(filepath.Join(root, "go.mod"))
666+
return err == nil && !fi.IsDir(), nil
667+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Regression test for golang.org/issue/34189 and golang.org/issue/34165:
2+
# @latest, @upgrade, and @patch should prefer compatible versions over
3+
# +incompatible ones, even if offered by a proxy.
4+
5+
[!net] skip
6+
7+
env GO111MODULE=on
8+
env GOPROXY=
9+
env GOSUMDB=
10+
11+
# github.com/russross/blackfriday v2.0.0+incompatible exists,
12+
# and should be resolved if we ask for v2.0 explicitly.
13+
14+
go list -m github.com/russross/[email protected]
15+
stdout '^github.com/russross/blackfriday v2\.0\.0\+incompatible$'
16+
17+
# blackfriday v1.5.2 has a go.mod file, so v1.5.2 should be preferred over
18+
# v2.0.0+incompatible when resolving latest, upgrade, and patch.
19+
20+
go list -m github.com/russross/blackfriday@latest
21+
stdout '^github.com/russross/blackfriday v1\.'
22+
23+
go list -m github.com/russross/blackfriday@upgrade
24+
stdout '^github.com/russross/blackfriday v1\.'
25+
26+
go list -m github.com/russross/blackfriday@patch
27+
stdout '^github.com/russross/blackfriday v1\.'
28+
29+
# If we're fetching directly from version control, ignored +incompatible
30+
# versions should also be omitted by 'go list'.
31+
32+
# (Note that they may still be included in results from a proxy: in proxy mode,
33+
# we would need to fetch the whole zipfile for the latest compatible version in
34+
# order to determine whether it contains a go.mod file, and part of the point of
35+
# the proxy is to avoid fetching unnecessary data.)
36+
37+
env GOPROXY=direct
38+
39+
go list -versions -m github.com/russross/blackfriday github.com/russross/blackfriday
40+
stdout '^github.com/russross/blackfriday v1\.5\.1 v1\.5\.2' # and possibly others
41+
! stdout ' v2\.'
42+
43+
# However, if the latest compatible version does not include a go.mod file,
44+
# +incompatible versions should still be listed, as they may still reflect the
45+
# intent of the module author.
46+
47+
go list -versions -m github.com/rsc/legacytest
48+
stdout '^github.com/rsc/legacytest v1\.0\.0 v1\.1\.0-pre v1\.2\.0 v2\.0\.0\+incompatible'
49+
50+
# If we're fetching directly from version control, asking for a commit hash
51+
# corresponding to a +incompatible version should continue to produce the
52+
# +incompatible version tagged for that commit, even if it is no longer listed.
53+
54+
go list -m github.com/russross/blackfriday@cadec560ec52
55+
stdout '^github.com/russross/blackfriday v2\.0\.0\+incompatible$'
56+
57+
# Similarly, requesting an untagged commit should continue to produce a +incompatible
58+
# pseudo-version.
59+
60+
go list -m github.com/rsc/legacytest@7303f7796364
61+
stdout '^github.com/rsc/legacytest v2\.0\.1-0\.20180717164253-7303f7796364\+incompatible$'
62+
63+
-- go.mod --
64+
module github.com/golang.org/issue/34165

0 commit comments

Comments
 (0)