Skip to content

Commit fcea875

Browse files
author
Jay Conrod
committed
cmd/gorelease: infer base version if unspecified
If the -base flag is not set on the command line, it is now inferred from the -version flag and available versions. If the -version flag is given, the base version will be the highest available release version lower than -version. Otherwise, the base version will be the highest release version. Pre-release versions are not considered. If there are no appropriate versions, and the release version is unspecified or appears to be the first release (e.g., v0.1.0, v2.0.0), the inferred base version will be "none" (meaning no comparison will be made). Otherwise, an error will be shown. Updates golang/go#26420 Change-Id: Iee45a4183f3e4a219c02a69b5d16a3cc5478644c Reviewed-on: https://go-review.googlesource.com/c/exp/+/216078 Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 6cc2880 commit fcea875

15 files changed

+182
-31
lines changed

cmd/gorelease/errors.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package main
66

77
import (
8+
"errors"
89
"flag"
910
"fmt"
1011
"os/exec"
@@ -22,7 +23,7 @@ func usageErrorf(format string, args ...interface{}) error {
2223
return &usageError{err: fmt.Errorf(format, args...)}
2324
}
2425

25-
const usageText = `usage: gorelease -base=version [-version=version]`
26+
const usageText = `usage: gorelease [-base=version] [-version=version]`
2627

2728
func (e *usageError) Error() string {
2829
msg := ""
@@ -32,21 +33,43 @@ func (e *usageError) Error() string {
3233
return usageText + "\n" + msg + "\nFor more information, run go doc golang.org/x/exp/cmd/gorelease"
3334
}
3435

36+
type baseVersionError struct {
37+
err error
38+
}
39+
40+
func (e *baseVersionError) Error() string {
41+
return fmt.Sprintf("could not find base version: %v", e.err)
42+
}
43+
44+
func (e *baseVersionError) Unwrap() error {
45+
return e.err
46+
}
47+
3548
type downloadError struct {
3649
m module.Version
3750
err error
3851
}
3952

4053
func (e *downloadError) Error() string {
41-
var msg string
42-
if xerr, ok := e.err.(*exec.ExitError); ok {
43-
msg = strings.TrimSpace(string(xerr.Stderr))
44-
} else {
45-
msg = e.err.Error()
46-
}
54+
msg := e.err.Error()
4755
sep := " "
4856
if strings.Contains(msg, "\n") {
4957
sep = "\n"
5058
}
5159
return fmt.Sprintf("error downloading module %s@%s:%s%s", e.m.Path, e.m.Version, sep, msg)
5260
}
61+
62+
// cleanCmdError simplifies error messages from os/exec.Cmd.Run.
63+
// For ExitErrors, it trims and returns stderr. This is useful for go commands
64+
// that print well-formatted errors. By default, ExitError prints the exit
65+
// status but not stderr.
66+
//
67+
// cleanCmdError returns other errors unmodified.
68+
func cleanCmdError(err error) error {
69+
if xerr, ok := err.(*exec.ExitError); ok {
70+
if stderr := strings.TrimSpace(string(xerr.Stderr)); stderr != "" {
71+
return errors.New(stderr)
72+
}
73+
}
74+
return err
75+
}

cmd/gorelease/gorelease.go

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
// "v2.3.4") or "none". If the version is "none", gorelease will not compare the
4343
// current version against any previous version; it will only validate the
4444
// current version. This is useful for checking the first release of a new major
45-
// version.
45+
// version. If -base is not specified, gorelease will attempt to infer a base
46+
// version from the -version flag and available released versions.
4647
//
4748
// -version=version: The proposed version to be released. If specified,
4849
// gorelease will confirm whether this version is consistent with changes made
@@ -66,6 +67,7 @@ import (
6667
"os/exec"
6768
"path"
6869
"path/filepath"
70+
"sort"
6971
"strings"
7072

7173
"golang.org/x/mod/modfile"
@@ -102,12 +104,6 @@ import (
102104
// the APIs are still compatible, just with a different module split).
103105

104106
// TODO(jayconrod):
105-
// * Automatically detect base version if unspecified.
106-
// If -version is vX.Y.(Z+1), use vX.Y.Z (with a message if it doesn't exist)
107-
// If -version is vX.(Y+1).0, use vX.Y.0
108-
// If -version is vX.0.0, use none
109-
// If -version is a prerelease, use same base as if it were a release.
110-
// If -version is not set, use latest release version or none.
111107
// * Allow -base to be an arbitrary revision name that resolves to a version
112108
// or pseudo-version.
113109
// * Don't accept -version that increments minor or patch version by more than 1
@@ -176,10 +172,7 @@ func runRelease(w io.Writer, dir string, args []string) (success bool, err error
176172
if len(fs.Args()) > 0 {
177173
return false, usageErrorf("no arguments allowed")
178174
}
179-
if baseVersion == "" {
180-
return false, usageErrorf("-base flag must be specified.\nUse -base=none if there is no previous version.")
181-
}
182-
if baseVersion != "none" {
175+
if baseVersion != "" && baseVersion != "none" {
183176
if c := semver.Canonical(baseVersion); c != baseVersion {
184177
return false, usageErrorf("base version %q is not a canonical semantic version", baseVersion)
185178
}
@@ -189,7 +182,7 @@ func runRelease(w io.Writer, dir string, args []string) (success bool, err error
189182
return false, usageErrorf("release version %q is not a canonical semantic version", releaseVersion)
190183
}
191184
}
192-
if baseVersion != "none" && releaseVersion != "" {
185+
if baseVersion != "" && baseVersion != "none" && releaseVersion != "" {
193186
if cmp := semver.Compare(baseVersion, releaseVersion); cmp == 0 {
194187
return false, usageErrorf("-base and -version must be different")
195188
} else if cmp > 0 {
@@ -232,6 +225,8 @@ func runRelease(w io.Writer, dir string, args []string) (success bool, err error
232225
// should be set to modRoot.
233226
//
234227
// baseVersion is a previously released version of the module to compare.
228+
// If baseVersion is "", a base version will be detected automatically, based
229+
// on releaseVersion or the latest available version of the module.
235230
// If baseVersion is "none", no comparison will be performed, and
236231
// the returned report will only describe problems with the release version.
237232
//
@@ -267,6 +262,12 @@ func makeReleaseReport(modRoot, repoRoot, baseVersion, releaseVersion string) (r
267262
panic(fmt.Sprintf("could not find version suffix in module path %q", modPath))
268263
}
269264

265+
baseVersionInferred := baseVersion == ""
266+
if baseVersionInferred {
267+
if baseVersion, err = inferBaseVersion(modPath, releaseVersion); err != nil {
268+
return report{}, err
269+
}
270+
}
270271
if baseVersion != "none" {
271272
if err := module.Check(modPath, baseVersion); err != nil {
272273
return report{}, fmt.Errorf("can't compare major versions: base version %s does not belong to module %s", baseVersion, modPath)
@@ -388,11 +389,12 @@ func makeReleaseReport(modRoot, repoRoot, baseVersion, releaseVersion string) (r
388389
return false
389390
}
390391
r := report{
391-
modulePath: modPath,
392-
baseVersion: baseVersion,
393-
releaseVersion: releaseVersion,
394-
tagPrefix: tagPrefix,
395-
diagnostics: diagnostics,
392+
modulePath: modPath,
393+
baseVersion: baseVersion,
394+
baseVersionInferred: baseVersionInferred,
395+
releaseVersion: releaseVersion,
396+
tagPrefix: tagPrefix,
397+
diagnostics: diagnostics,
396398
}
397399
for _, pair := range zipPackages(basePkgs, releasePkgs) {
398400
basePkg, releasePkg := pair.base, pair.release
@@ -512,6 +514,85 @@ func checkModPath(modPath string) error {
512514
return module.CheckPath(modPath)
513515
}
514516

517+
// inferBaseVersion returns an appropriate base version if one was not
518+
// specified explicitly.
519+
//
520+
// If releaseVersion is not "", inferBaseVersion returns the highest available
521+
// release version of the module lower than releaseVersion.
522+
// Otherwise, inferBaseVersion returns the highest available release version.
523+
// Pre-release versions are not considered. If there is no available version,
524+
// and releaseVersion appears to be the first release version (for example,
525+
// "v0.1.0", "v2.0.0"), "none" is returned.
526+
func inferBaseVersion(modPath, releaseVersion string) (baseVersion string, err error) {
527+
defer func() {
528+
if err != nil {
529+
err = &baseVersionError{err: err}
530+
}
531+
}()
532+
533+
versions, err := loadVersions(modPath)
534+
if err != nil {
535+
return "", err
536+
}
537+
538+
for i := len(versions) - 1; i >= 0; i-- {
539+
v := versions[i]
540+
if semver.Prerelease(v) == "" &&
541+
(releaseVersion == "" || semver.Compare(v, releaseVersion) < 0) {
542+
return v, nil
543+
}
544+
}
545+
546+
if releaseVersion == "" || maybeFirstVersion(releaseVersion) {
547+
return "none", nil
548+
}
549+
return "", fmt.Errorf("no versions found lower than %s", releaseVersion)
550+
}
551+
552+
// loadVersions loads the list of versions for the given module using
553+
// 'go list -m -versions'. The returned versions are sorted in ascending
554+
// semver order.
555+
func loadVersions(modPath string) ([]string, error) {
556+
tmpDir, err := ioutil.TempDir("", "")
557+
if err != nil {
558+
return nil, err
559+
}
560+
defer os.Remove(tmpDir)
561+
cmd := exec.Command("go", "list", "-m", "-versions", "--", modPath)
562+
cmd.Dir = tmpDir
563+
cmd.Env = append(os.Environ(), "GO111MODULE=on")
564+
out, err := cmd.Output()
565+
if err != nil {
566+
return nil, cleanCmdError(err)
567+
}
568+
versions := strings.Fields(string(out))
569+
if len(versions) > 0 {
570+
versions = versions[1:] // skip module path
571+
}
572+
573+
// Sort versions defensively. 'go list -m -versions' should always returns
574+
// a sorted list of versions, but it's fast and easy to sort them here, too.
575+
sort.Slice(versions, func(i, j int) bool {
576+
return semver.Compare(versions[i], versions[j]) < 0
577+
})
578+
return versions, nil
579+
}
580+
581+
// maybeFirstVersion returns whether v appears to be the first version
582+
// of a module.
583+
func maybeFirstVersion(v string) bool {
584+
major, minor, patch, _, _, err := parseVersion(v)
585+
if err != nil {
586+
return false
587+
}
588+
if major == "0" {
589+
return minor == "0" && patch == "0" ||
590+
minor == "0" && patch == "1" ||
591+
minor == "1" && patch == "0"
592+
}
593+
return minor == "0" && patch == "0"
594+
}
595+
515596
// dirMajorSuffix returns a major version suffix for a slash-separated path.
516597
// For example, for the path "foo/bar/v2", dirMajorSuffix would return "v2".
517598
// If no major version suffix is found, "" is returned.
@@ -625,7 +706,7 @@ func copyModuleToTempDir(modPath, modRoot string) (dir string, err error) {
625706
func downloadModule(m module.Version) (modRoot string, err error) {
626707
defer func() {
627708
if err != nil {
628-
err = &downloadError{m: m, err: err}
709+
err = &downloadError{m: m, err: cleanCmdError(err)}
629710
}
630711
}()
631712

cmd/gorelease/report.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ type report struct {
2323
modulePath string
2424

2525
// baseVersion is the "old" version of the module to compare against.
26-
// It may be empty if there is no base version (for example, if this is
27-
// the first release).
26+
// It may be "none" if there is no base version (for example, if this is
27+
// the first release). It may not be "".
2828
baseVersion string
2929

30+
// baseVersionInferred is true if the base version was determined
31+
// automatically (not specified with -base).
32+
baseVersionInferred bool
33+
3034
// releaseVersion is the version of the module to release, either
3135
// proposed with -version or inferred with suggestVersion.
3236
releaseVersion string
@@ -81,6 +85,10 @@ func (r *report) Text(w io.Writer) error {
8185
}
8286
}
8387

88+
if r.baseVersionInferred {
89+
fmt.Fprintf(buf, "Inferred base version: %s\n", r.baseVersion)
90+
}
91+
8492
if len(r.diagnostics) > 0 {
8593
for _, d := range r.diagnostics {
8694
fmt.Fprintln(buf, d)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod=example.com/basic
2+
version=v0.1.2
3+
-- want --
4+
Inferred base version: v1.1.2
5+
Suggested version: v1.1.3
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod=example.com/basic
2+
version=v1.0.1
3+
release=v1.0.2
4+
-- want --
5+
Inferred base version: v1.0.1
6+
v1.0.2 is a valid semantic version for this release.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
mod=example.com/basic/v3
2+
-- go.mod --
3+
module example.com/basic/v3
4+
5+
go 1.13
6+
-- want --
7+
Inferred base version: none
8+
Suggested version: v3.0.0
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod=example.com/basic/v3
2+
version=v3.0.0-ignore
3+
release=v3.1.0
4+
error=true
5+
-- want --
6+
could not find base version: no versions found lower than v3.1.0
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod=example.com/basic/v3
2+
version=v3.0.0-ignore
3+
release=v3.0.0
4+
-- want --
5+
Inferred base version: none
6+
v3.0.0 is a valid semantic version for this release.

cmd/gorelease/testdata/errors/bad_base.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ base=master
33
error=true
44

55
-- want --
6-
usage: gorelease -base=version [-version=version]
6+
usage: gorelease [-base=version] [-version=version]
77
base version "master" is not a canonical semantic version
88
For more information, run go doc golang.org/x/exp/cmd/gorelease

cmd/gorelease/testdata/errors/bad_release.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ release=master
44
error=true
55

66
-- want --
7-
usage: gorelease -base=version [-version=version]
7+
usage: gorelease [-base=version] [-version=version]
88
release version "master" is not a canonical semantic version
99
For more information, run go doc golang.org/x/exp/cmd/gorelease

cmd/gorelease/testdata/errors/base_higher.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ release=v0.1.0
44
error=true
55

66
-- want --
7-
usage: gorelease -base=version [-version=version]
7+
usage: gorelease [-base=version] [-version=version]
88
base version ("v0.2.0") must be lower than release version ("v0.1.0")
99
For more information, run go doc golang.org/x/exp/cmd/gorelease

cmd/gorelease/testdata/errors/same_base_release.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ release=v0.1.0
44
error=true
55

66
-- want --
7-
usage: gorelease -base=version [-version=version]
7+
usage: gorelease [-base=version] [-version=version]
88
-base and -version must be different
99
For more information, run go doc golang.org/x/exp/cmd/gorelease
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- .info --
2+
{"Version":"v3.0.0-ignore"}
3+
-- go.mod --
4+
module example.com/basic/v3
5+
6+
go 1.13

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
1010
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028
1111
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b
12+
golang.org/x/sync v0.0.0-20190423024810-112230192c58
1213
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24
1314
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa
1415
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b h1:GgiSbuUyC0BlbUmHQBgFqu3
1818
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
1919
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
2020
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
21+
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
2122
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
2223
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
2324
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

0 commit comments

Comments
 (0)