Skip to content

Commit 9430c1a

Browse files
committed
cmd/go/internal/modfetch: move to new pseudo-version design
The original pseudo-version design used versions of the form v0.0.0-yyyymmddhhmmss-abcdef123456 These were intentionally chosen to be valid semantic versions that sort below any explicitly-chosen semantic version (even v0.0.0), so that they could be used before anything was tagged but after that would essentially only be useful in replace statements (because the max operation during MVS would always prefer a tagged version). Then we changed the go command to accept hashes on the command line, so that you can say go get github.com/my/proj@abcdef and it will download and use v0.0.0-yyyymmddhhmmss-abcdef123456. If you were using v1.10.1 before and this commit is just little bit newer than that commit, calling it v0.0.0-xxx is confusing but also harmful: the go command sees the change from v1.10.1 to the v0.0.0 pseudoversion as a downgrade, and it downgrades other modules in the build. In particular if some other module has a requirement of github.com/my/proj v1.9.0 (or later), the pseudo-version appears to be before that, so go get would downgrade that module too. It might even remove it entirely, if every available version needs a post-v0.0.0 version of my/proj. This CL introduces new pseudo-version forms that can be used to slot in after the most recent explicit tag before the commit. If the most recent tagged commit before abcdef is v1.10.1, then now we will use v1.10.2-0.yyyymmddhhmmss-abcdef123456 This has the right properties for downgrades and the like, since it is after v1.10.1 but before almost any possible successor, such as v1.10.2, v1.10.2-1, or v1.10.2-pre. This CL also uses those pseudo-version forms as appropriate when mapping a hash to a pseudo-version. This fixes the downgrade problem. Overall, this CL reflects our growing recognition of pseudo-versions as being like "untagged prereleases". Issue #26150 was about documenting best practices for how to work around this kind of accidental downgrade problem with additional steps. Now there are no additional steps: the problem is avoided by default. Fixes #26150. Change-Id: I402feeccb93e8e937bafcaa26402d88572e9b14c Reviewed-on: https://go-review.googlesource.com/124515 Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 472e926 commit 9430c1a

File tree

10 files changed

+356
-96
lines changed

10 files changed

+356
-96
lines changed

src/cmd/go/internal/modfetch/codehost/codehost.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ type Repo interface {
7777
// contained in the zip file. All files in the zip file are expected to be
7878
// nested in a single top-level directory, whose name is not specified.
7979
ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error)
80+
81+
// RecentTag returns the most recent tag at or before the given rev
82+
// with the given prefix. It should make a best-effort attempt to
83+
// find a tag that is a valid semantic version (following the prefix),
84+
// or else the result is not useful to the caller, but it need not
85+
// incur great expense in doing so. For example, the git implementation
86+
// of RecentTag limits git's search to tags matching the glob expression
87+
// "v[0-9]*.[0-9]*.[0-9]*" (after the prefix).
88+
RecentTag(rev, prefix string) (tag string, err error)
8089
}
8190

8291
// A Rev describes a single revision in a source code repository.

src/cmd/go/internal/modfetch/codehost/git.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,18 @@ func (r *gitRepo) readFileRevs(tags []string, file string, fileMap map[string]*F
602602
return missing, nil
603603
}
604604

605+
func (r *gitRepo) RecentTag(rev, prefix string) (tag string, err error) {
606+
_, err = r.Stat(rev)
607+
if err != nil {
608+
return "", err
609+
}
610+
out, err := Run(r.dir, "git", "describe", "--first-parent", "--tags", "--always", "--abbrev=0", "--match", prefix+"v[0-9]*.[0-9]*.[0-9]*", "--tags", rev)
611+
if err != nil {
612+
return "", err
613+
}
614+
return strings.TrimSpace(string(out)), nil
615+
}
616+
605617
func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error) {
606618
// TODO: Use maxSize or drop it.
607619
args := []string{}

src/cmd/go/internal/modfetch/codehost/vcs.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ func (r *vcsRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[s
329329
return nil, fmt.Errorf("ReadFileRevs not implemented")
330330
}
331331

332+
func (r *vcsRepo) RecentTag(rev, prefix string) (tag string, err error) {
333+
return "", fmt.Errorf("RecentTags not implemented")
334+
}
335+
332336
func (r *vcsRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error) {
333337
if rev == "latest" {
334338
rev = r.cmd.latest

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

Lines changed: 9 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,12 @@ package modfetch
66

77
import (
88
"archive/zip"
9-
"errors"
109
"fmt"
1110
"io"
1211
"io/ioutil"
1312
"os"
1413
"path"
15-
"regexp"
1614
"strings"
17-
"time"
1815

1916
"cmd/go/internal/modfetch/codehost"
2017
"cmd/go/internal/modfile"
@@ -194,7 +191,7 @@ func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, e
194191
}
195192
}
196193

197-
tagOK := func(v string) string {
194+
tagToVersion := func(v string) string {
198195
if !strings.HasPrefix(v, p) {
199196
return ""
200197
}
@@ -212,26 +209,28 @@ func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, e
212209
}
213210

214211
// If info.Version is OK, use it.
215-
if v := tagOK(info.Version); v != "" {
212+
if v := tagToVersion(info.Version); v != "" {
216213
info2.Version = v
217214
} else {
218215
// Otherwise look through all known tags for latest in semver ordering.
219216
for _, tag := range info.Tags {
220-
if v := tagOK(tag); v != "" && semver.Compare(info2.Version, v) < 0 {
217+
if v := tagToVersion(tag); v != "" && semver.Compare(info2.Version, v) < 0 {
221218
info2.Version = v
222219
}
223220
}
224221
// Otherwise make a pseudo-version.
225222
if info2.Version == "" {
226-
info2.Version = PseudoVersion(r.pseudoMajor, info.Time, info.Short)
223+
tag, _ := r.code.RecentTag(statVers, p)
224+
v = tagToVersion(tag)
225+
// TODO: Check that v is OK for r.pseudoMajor or else is OK for incompatible.
226+
info2.Version = PseudoVersion(r.pseudoMajor, v, info.Time, info.Short)
227227
}
228228
}
229229
}
230230

231231
// Do not allow a successful stat of a pseudo-version for a subdirectory
232232
// unless the subdirectory actually does have a go.mod.
233233
if IsPseudoVersion(info2.Version) && r.codeDir != "" {
234-
// TODO: git describe --first-parent --match 'v[0-9]*' --tags
235234
_, _, _, err := r.findDir(info2.Version)
236235
if err != nil {
237236
// TODO: It would be nice to return an error like "not a module".
@@ -246,9 +245,8 @@ func (r *codeRepo) convert(info *codehost.RevInfo, statVers string) (*RevInfo, e
246245
func (r *codeRepo) revToRev(rev string) string {
247246
if semver.IsValid(rev) {
248247
if IsPseudoVersion(rev) {
249-
i := strings.Index(rev, "-")
250-
j := strings.Index(rev[i+1:], "-")
251-
return rev[i+1+j+1:]
248+
r, _ := PseudoVersionRev(rev)
249+
return r
252250
}
253251
if semver.Build(rev) == "+incompatible" {
254252
rev = rev[:len(rev)-len("+incompatible")]
@@ -598,71 +596,3 @@ func isVendoredPackage(name string) bool {
598596
}
599597
return strings.Contains(name[i:], "/")
600598
}
601-
602-
func PseudoVersion(major string, t time.Time, rev string) string {
603-
if major == "" {
604-
major = "v0"
605-
}
606-
return fmt.Sprintf("%s.0.0-%s-%s", major, t.UTC().Format("20060102150405"), rev)
607-
}
608-
609-
var ErrNotPseudoVersion = errors.New("not a pseudo-version")
610-
611-
/*
612-
func ParsePseudoVersion(repo Repo, version string) (rev string, err error) {
613-
major := semver.Major(version)
614-
if major == "" {
615-
return "", ErrNotPseudoVersion
616-
}
617-
majorPrefix := major + ".0.0-"
618-
if !strings.HasPrefix(version, majorPrefix) || !strings.Contains(version[len(majorPrefix):], "-") {
619-
return "", ErrNotPseudoVersion
620-
}
621-
versionSuffix := version[len(majorPrefix):]
622-
for i := 0; versionSuffix[i] != '-'; i++ {
623-
c := versionSuffix[i]
624-
if c < '0' || '9' < c {
625-
return "", ErrNotPseudoVersion
626-
}
627-
}
628-
rev = versionSuffix[strings.Index(versionSuffix, "-")+1:]
629-
if rev == "" {
630-
return "", ErrNotPseudoVersion
631-
}
632-
if proxyURL != "" {
633-
return version, nil
634-
}
635-
fullRev, t, err := repo.CommitInfo(rev)
636-
if err != nil {
637-
return "", fmt.Errorf("unknown pseudo-version %s: loading %v: %v", version, rev, err)
638-
}
639-
v := PseudoVersion(major, t, repo.ShortRev(fullRev))
640-
if v != version {
641-
return "", fmt.Errorf("unknown pseudo-version %s: %v is %v", version, rev, v)
642-
}
643-
return fullRev, nil
644-
}
645-
*/
646-
647-
var pseudoVersionRE = regexp.MustCompile(`^v[0-9]+\.0\.0-[0-9]{14}-[A-Za-z0-9]+$`)
648-
649-
// IsPseudoVersion reports whether v is a pseudo-version.
650-
func IsPseudoVersion(v string) bool {
651-
return pseudoVersionRE.MatchString(v)
652-
}
653-
654-
// PseudoVersionTime returns the time stamp of the pseudo-version v.
655-
// It returns an error if v is not a pseudo-version or if the time stamp
656-
// embedded in the pseudo-version is not a valid time.
657-
func PseudoVersionTime(v string) (time.Time, error) {
658-
if !IsPseudoVersion(v) {
659-
return time.Time{}, fmt.Errorf("not a pseudo-version")
660-
}
661-
i := strings.Index(v, "-") + 1
662-
j := i + strings.Index(v[i:], "-")
663-
t, err := time.Parse("20060102150405", v[i:j])
664-
if err != nil {
665-
return time.Time{}, fmt.Errorf("malformed pseudo-version %q", v)
666-
}
667-
return t, nil
668-
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ var codeRepoTests = []struct {
237237
// redirect to googlesource
238238
path: "golang.org/x/text",
239239
rev: "4e4a3210bb",
240-
version: "v0.0.0-20180208041248-4e4a3210bb54",
240+
version: "v0.3.1-0.20180208041248-4e4a3210bb54",
241241
name: "4e4a3210bb54bb31f6ab2cdca2edcc0b50c420c1",
242242
short: "4e4a3210bb54",
243243
time: time.Date(2018, 2, 8, 4, 12, 48, 0, time.UTC),
@@ -611,6 +611,9 @@ func (ch *fixedTagsRepo) ReadFileRevs([]string, string, int64) (map[string]*code
611611
func (ch *fixedTagsRepo) ReadZip(string, string, int64) (io.ReadCloser, string, error) {
612612
panic("not impl")
613613
}
614+
func (ch *fixedTagsRepo) RecentTag(string, string) (string, error) {
615+
panic("not impl")
616+
}
614617
func (ch *fixedTagsRepo) Stat(string) (*codehost.RevInfo, error) { panic("not impl") }
615618

616619
func TestNonCanonicalSemver(t *testing.T) {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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.
4+
5+
// Pseudo-versions
6+
//
7+
// Code authors are expected to tag the revisions they want users to use,
8+
// including prereleases. However, not all authors tag versions at all,
9+
// and not all commits a user might want to try will have tags.
10+
// A pseudo-version is a version with a special form that allows us to
11+
// address an untagged commit and order that version with respect to
12+
// other versions we might encounter.
13+
//
14+
// A pseudo-version takes one of the general forms:
15+
//
16+
// (1) vX.0.0-yyyymmddhhmmss-abcdef123456
17+
// (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456
18+
// (3) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible
19+
// (4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456
20+
// (5) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible
21+
//
22+
// If there is no recently tagged version with the right major version vX,
23+
// then form (1) is used, creating a space of pseudo-versions at the bottom
24+
// of the vX version range, less than any tagged version, including the unlikely v0.0.0.
25+
//
26+
// If the most recent tagged version before the target commit is vX.Y.Z or vX.Y.Z+incompatible,
27+
// then the pseudo-version uses form (2) or (3), making it a prerelease for the next
28+
// possible semantic version after vX.Y.Z. The leading 0 segment in the prerelease string
29+
// ensures that the pseudo-version compares less than possible future explicit prereleases
30+
// like vX.Y.(Z+1)-rc1 or vX.Y.(Z+1)-1.
31+
//
32+
// If the most recent tagged version before the target commit is vX.Y.Z-pre or vX.Y.Z-pre+incompatible,
33+
// then the pseudo-version uses form (4) or (5), making it a slightly later prerelease.
34+
35+
package modfetch
36+
37+
import (
38+
"cmd/go/internal/semver"
39+
"fmt"
40+
"regexp"
41+
"strings"
42+
"time"
43+
)
44+
45+
// PseudoVersion returns a pseudo-version for the given major version ("v1")
46+
// preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time,
47+
// and revision identifier (usually a 12-byte commit hash prefix).
48+
func PseudoVersion(major, older string, t time.Time, rev string) string {
49+
if major == "" {
50+
major = "v0"
51+
}
52+
segment := fmt.Sprintf("%s-%s", t.UTC().Format("20060102150405"), rev)
53+
build := semver.Build(older)
54+
older = semver.Canonical(older)
55+
if older == "" {
56+
return major + ".0.0-" + segment // form (1)
57+
}
58+
if semver.Prerelease(older) != "" {
59+
return older + ".0." + segment + build // form (4), (5)
60+
}
61+
62+
// Form (2), (3).
63+
// Extract patch from vMAJOR.MINOR.PATCH
64+
v := older[:len(older)]
65+
i := strings.LastIndex(v, ".") + 1
66+
v, patch := v[:i], v[i:]
67+
68+
// Increment PATCH by adding 1 to decimal:
69+
// scan right to left turning 9s to 0s until you find a digit to increment.
70+
// (Number might exceed int64, but math/big is overkill.)
71+
digits := []byte(patch)
72+
for i = len(digits) - 1; i >= 0 && digits[i] == '9'; i-- {
73+
digits[i] = '0'
74+
}
75+
if i >= 0 {
76+
digits[i]++
77+
} else {
78+
// digits is all zeros
79+
digits[0] = '1'
80+
digits = append(digits, '0')
81+
}
82+
patch = string(digits)
83+
84+
// Reassemble.
85+
return v + patch + "-0." + segment + build
86+
}
87+
88+
var pseudoVersionRE = regexp.MustCompile(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+incompatible)?$`)
89+
90+
// IsPseudoVersion reports whether v is a pseudo-version.
91+
func IsPseudoVersion(v string) bool {
92+
return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v)
93+
}
94+
95+
// PseudoVersionTime returns the time stamp of the pseudo-version v.
96+
// It returns an error if v is not a pseudo-version or if the time stamp
97+
// embedded in the pseudo-version is not a valid time.
98+
func PseudoVersionTime(v string) (time.Time, error) {
99+
timestamp, _, err := parsePseudoVersion(v)
100+
t, err := time.Parse("20060102150405", timestamp)
101+
if err != nil {
102+
return time.Time{}, fmt.Errorf("pseudo-version with malformed time %s: %q", timestamp, v)
103+
}
104+
return t, nil
105+
}
106+
107+
// PseudoVersionRev returns the revision identifier of the pseudo-version v.
108+
// It returns an error if v is not a pseudo-version.
109+
func PseudoVersionRev(v string) (rev string, err error) {
110+
_, rev, err = parsePseudoVersion(v)
111+
return
112+
}
113+
114+
func parsePseudoVersion(v string) (timestamp, rev string, err error) {
115+
if !IsPseudoVersion(v) {
116+
return "", "", fmt.Errorf("malformed pseudo-version %q", v)
117+
}
118+
v = strings.TrimSuffix(v, "+incompatible")
119+
j := strings.LastIndex(v, "-")
120+
v, rev = v[:j], v[j+1:]
121+
i := strings.LastIndex(v, "-")
122+
if j := strings.LastIndex(v, "."); j > i {
123+
timestamp = v[j+1:]
124+
} else {
125+
timestamp = v[i+1:]
126+
}
127+
return timestamp, rev, nil
128+
}

0 commit comments

Comments
 (0)