Skip to content

Commit 14a133f

Browse files
author
Bryan C. Mills
committed
cmd/go/internal/modfetch: prune +incompatible versions more aggressively
codeRepo.Versions previously checked every possible +incompatible version for a 'go.mod' file. That is wasteful and counterproductive. It is wasteful because typically, a project will adopt modules at some major version, after which they will (be required to) use semantic import paths for future major versions. It is counterproductive because it causes an accidental '+incompatible' tag to exist, and no compatible tag can have higher semantic precedence. This change prunes out some of the +incompatible versions in codeRepo.Versions, eliminating the “wasteful” part but not all of the “counterproductive” part: the extraneous versions can still be fetched explicitly, and proxies may include them in the @v/list endpoint. Updates #34165 Updates #34189 Updates #34533 Change-Id: Ifc52c725aa396f7fde2afc727d0d5950acd06946 Reviewed-on: https://go-review.googlesource.com/c/go/+/204439 Run-TryBot: Bryan C. Mills <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 81559af commit 14a133f

File tree

3 files changed

+96
-17
lines changed

3 files changed

+96
-17
lines changed

src/cmd/go/internal/modfetch/coderepo.go

+94-15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io/ioutil"
1313
"os"
1414
"path"
15+
"sort"
1516
"strings"
1617
"time"
1718

@@ -147,8 +148,7 @@ func (r *codeRepo) Versions(prefix string) ([]string, error) {
147148
}
148149
}
149150

150-
list := []string{}
151-
var incompatible []string
151+
var list, incompatible []string
152152
for _, tag := range tags {
153153
if !strings.HasPrefix(tag, p) {
154154
continue
@@ -160,35 +160,114 @@ func (r *codeRepo) Versions(prefix string) ([]string, error) {
160160
if v == "" || v != module.CanonicalVersion(v) || IsPseudoVersion(v) {
161161
continue
162162
}
163+
163164
if err := module.CheckPathMajor(v, r.pathMajor); err != nil {
164165
if r.codeDir == "" && r.pathMajor == "" && semver.Major(v) > "v1" {
165166
incompatible = append(incompatible, v)
166167
}
167168
continue
168169
}
170+
169171
list = append(list, v)
170172
}
173+
SortVersions(list)
174+
SortVersions(incompatible)
171175

172-
if len(incompatible) > 0 {
173-
// Check for later versions that were created not following semantic import versioning,
174-
// as indicated by the absence of a go.mod file. Those versions can be addressed
175-
// by referring to them with a +incompatible suffix, as in v17.0.0+incompatible.
176-
files, err := r.code.ReadFileRevs(incompatible, "go.mod", codehost.MaxGoMod)
177-
if err != nil {
178-
return nil, &module.ModuleError{
176+
return r.appendIncompatibleVersions(list, incompatible)
177+
}
178+
179+
// appendIncompatibleVersions appends "+incompatible" versions to list if
180+
// appropriate, returning the final list.
181+
//
182+
// The incompatible list contains candidate versions without the '+incompatible'
183+
// prefix.
184+
//
185+
// Both list and incompatible must be sorted in semantic order.
186+
func (r *codeRepo) appendIncompatibleVersions(list, incompatible []string) ([]string, error) {
187+
if len(incompatible) == 0 || r.pathMajor != "" {
188+
// No +incompatible versions are possible, so no need to check them.
189+
return list, nil
190+
}
191+
192+
// We assume that if the latest release of any major version has a go.mod
193+
// file, all subsequent major versions will also have go.mod files (and thus
194+
// be ineligible for use as +incompatible versions).
195+
// If we're wrong about a major version, users will still be able to 'go get'
196+
// specific higher versions explicitly — they just won't affect 'latest' or
197+
// appear in 'go list'.
198+
//
199+
// Conversely, we assume that if the latest release of any major version lacks
200+
// a go.mod file, all versions also lack go.mod files. If we're wrong, we may
201+
// include a +incompatible version that isn't really valid, but most
202+
// operations won't try to use that version anyway.
203+
//
204+
// These optimizations bring
205+
// 'go list -versions -m github.com/openshift/origin' down from 1m58s to 0m37s.
206+
// That's still not great, but a substantial improvement.
207+
208+
versionHasGoMod := func(v string) (bool, error) {
209+
_, err := r.code.ReadFile(v, "go.mod", codehost.MaxGoMod)
210+
if err == nil {
211+
return true, nil
212+
}
213+
if !os.IsNotExist(err) {
214+
return false, &module.ModuleError{
179215
Path: r.modPath,
180216
Err: err,
181217
}
182218
}
183-
for _, rev := range incompatible {
184-
f := files[rev]
185-
if os.IsNotExist(f.Err) {
186-
list = append(list, rev+"+incompatible")
187-
}
219+
return false, nil
220+
}
221+
222+
if len(list) > 0 {
223+
ok, err := versionHasGoMod(list[len(list)-1])
224+
if err != nil {
225+
return nil, err
226+
}
227+
if ok {
228+
// The latest compatible version has a go.mod file, so assume that all
229+
// subsequent versions do as well, and do not include any +incompatible
230+
// versions. Even if we are wrong, the author clearly intends module
231+
// consumers to be on the v0/v1 line instead of a higher +incompatible
232+
// version. (See https://golang.org/issue/34189.)
233+
//
234+
// We know of at least two examples where this behavior is desired
235+
// (github.com/russross/[email protected] and
236+
// github.com/libp2p/[email protected]), and (as of 2019-10-29) have no
237+
// concrete examples for which it is undesired.
238+
return list, nil
188239
}
189240
}
190241

191-
SortVersions(list)
242+
var lastMajor string
243+
for i, v := range incompatible {
244+
major := semver.Major(v)
245+
if major == lastMajor {
246+
list = append(list, v+"+incompatible")
247+
continue
248+
}
249+
250+
rem := incompatible[i:]
251+
j := sort.Search(len(rem), func(j int) bool {
252+
return semver.Major(rem[j]) != major
253+
})
254+
latestAtMajor := rem[j-1]
255+
256+
ok, err := versionHasGoMod(latestAtMajor)
257+
if err != nil {
258+
return nil, err
259+
}
260+
if ok {
261+
// This major version has a go.mod file, so it is not allowed as
262+
// +incompatible. Subsequent major versions are likely to also have
263+
// go.mod files, so stop here.
264+
break
265+
}
266+
267+
lastMajor = major
268+
list = append(list, v+"+incompatible")
269+
}
270+
192271
return list, nil
193272
}
194273

src/cmd/go/internal/modfetch/coderepo_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ var codeRepoVersionsTests = []struct {
637637
{
638638
vcs: "git",
639639
path: "github.com/rsc/vgotest1",
640-
versions: []string{"v0.0.0", "v0.0.1", "v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.1.0", "v2.0.0+incompatible"},
640+
versions: []string{"v0.0.0", "v0.0.1", "v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.1.0"},
641641
},
642642
{
643643
vcs: "git",

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type Repo interface {
3434
// Pseudo-versions are not included.
3535
// Versions should be returned sorted in semver order
3636
// (implementations can use SortVersions).
37-
Versions(prefix string) (tags []string, err error)
37+
Versions(prefix string) ([]string, error)
3838

3939
// Stat returns information about the revision rev.
4040
// A revision can be any identifier known to the underlying service:

0 commit comments

Comments
 (0)