Skip to content

Commit a20c86d

Browse files
author
Jay Conrod
committed
cmd/gorelease: support comparison of modules with different paths
For golang/go#39666 Change-Id: I7e69ef14a3f6b3996ceea2a70ec4ce1e33f912c6 Reviewed-on: https://go-review.googlesource.com/c/exp/+/238839 Trust: Jay Conrod <[email protected]> Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent fa01524 commit a20c86d

13 files changed

+232
-32
lines changed

cmd/gorelease/gorelease.go

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@
6464
// "latest", or "none". If the version is "none", gorelease will not compare the
6565
// current version against any previous version; it will only validate the
6666
// current version. This is useful for checking the first release of a new major
67-
// version. If -base is not specified, gorelease will attempt to infer a base
68-
// version from the -version flag and available released versions.
67+
// version. The version may be preceded by a different module path and an '@',
68+
// like -base=example.com/mod/[email protected]. This is useful to compare against
69+
// an earlier major version or a fork. If -base is not specified, gorelease will
70+
// attempt to infer a base version from the -version flag and available released
71+
// versions.
6972
//
7073
// -version=version: The proposed version to be released. If specified,
7174
// gorelease will confirm whether this version is consistent with changes made
@@ -160,8 +163,8 @@ func runRelease(w io.Writer, dir string, args []string) (success bool, err error
160163
fs := flag.NewFlagSet("gorelease", flag.ContinueOnError)
161164
fs.Usage = func() {}
162165
fs.SetOutput(ioutil.Discard)
163-
var baseVersion, releaseVersion string
164-
fs.StringVar(&baseVersion, "base", "", "previous version to compare against")
166+
var baseOpt, releaseVersion string
167+
fs.StringVar(&baseOpt, "base", "", "previous version to compare against")
165168
fs.StringVar(&releaseVersion, "version", "", "proposed version to be released")
166169
if err := fs.Parse(args); err != nil {
167170
return false, &usageError{err: err}
@@ -170,6 +173,7 @@ func runRelease(w io.Writer, dir string, args []string) (success bool, err error
170173
if len(fs.Args()) > 0 {
171174
return false, usageErrorf("no arguments allowed")
172175
}
176+
173177
if releaseVersion != "" {
174178
if semver.Build(releaseVersion) != "" {
175179
return false, usageErrorf("release version %q is not a canonical semantic version: build metadata is not supported", releaseVersion)
@@ -178,12 +182,26 @@ func runRelease(w io.Writer, dir string, args []string) (success bool, err error
178182
return false, usageErrorf("release version %q is not a canonical semantic version", releaseVersion)
179183
}
180184
}
181-
if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && releaseVersion != "" {
182-
if cmp := semver.Compare(baseVersion, releaseVersion); cmp == 0 {
183-
return false, usageErrorf("-base and -version must be different")
184-
} else if cmp > 0 {
185-
return false, usageErrorf("base version (%q) must be lower than release version (%q)", baseVersion, releaseVersion)
185+
186+
var baseModPath, baseVersion string
187+
if at := strings.Index(baseOpt, "@"); at >= 0 {
188+
baseModPath = baseOpt[:at]
189+
baseVersion = baseOpt[at+1:]
190+
} else if dot, slash := strings.Index(baseOpt, "."), strings.Index(baseOpt, "/"); dot >= 0 && slash >= 0 && dot < slash {
191+
baseModPath = baseOpt
192+
} else {
193+
baseVersion = baseOpt
194+
}
195+
if baseModPath == "" {
196+
if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && releaseVersion != "" {
197+
if cmp := semver.Compare(baseOpt, releaseVersion); cmp == 0 {
198+
return false, usageErrorf("-base and -version must be different")
199+
} else if cmp > 0 {
200+
return false, usageErrorf("base version (%q) must be lower than release version (%q)", baseVersion, releaseVersion)
201+
}
186202
}
203+
} else if baseModPath != "" && baseVersion == "none" {
204+
return false, usageErrorf(`base version (%q) cannot have version "none" with explicit module path`, baseOpt)
187205
}
188206

189207
// Find the local module and repository root directories.
@@ -201,8 +219,12 @@ func runRelease(w io.Writer, dir string, args []string) (success bool, err error
201219

202220
// Find the base version if there is one, download it, and load packages from
203221
// the module cache.
204-
baseModPath := release.modPath // TODO(golang.org/issue/39666): allow different module path
205-
base, err := loadDownloadedModule(baseModPath, baseVersion, releaseVersion)
222+
var max string
223+
if baseModPath == "" {
224+
baseModPath = release.modPath
225+
max = releaseVersion
226+
}
227+
base, err := loadDownloadedModule(baseModPath, baseVersion, max)
206228
if err != nil {
207229
return false, err
208230
}
@@ -381,6 +403,9 @@ func loadLocalModule(modRoot, repoRoot, version string) (m moduleInfo, err error
381403
// to max will not be considered. Typically, loadDownloadedModule is used to
382404
// load the base version, and max is the release version.
383405
func loadDownloadedModule(modPath, version, max string) (m moduleInfo, err error) {
406+
// TODO(#39666): support downloaded modules that are "soft forks", where the
407+
// module path in go.mod is different from modPath.
408+
384409
// Check the module path and version.
385410
// If the version is a query, resolve it to a canonical version.
386411
m = moduleInfo{modPath: modPath}
@@ -474,18 +499,12 @@ func loadDownloadedModule(modPath, version, max string) (m moduleInfo, err error
474499
// version control tag to use (with an appropriate prefix, for modules not
475500
// in the repository root directory).
476501
func makeReleaseReport(base, release moduleInfo) (report, error) {
477-
if base.modPath != release.modPath {
478-
// TODO(golang.org/issue/39666): allow base and release path to be different.
479-
panic(fmt.Sprintf("base module path %q is different than release module path %q", base.modPath, release.modPath))
480-
}
481-
modPath := release.modPath
482-
483502
// Compare each pair of packages.
484503
// Ignore internal packages.
485504
// If we don't have a base version to compare against,
486505
// just check the new packages for errors.
487506
shouldCompare := base.version != "none"
488-
isInternal := func(pkgPath string) bool {
507+
isInternal := func(modPath, pkgPath string) bool {
489508
if !hasPathPrefix(pkgPath, modPath) {
490509
panic(fmt.Sprintf("package %s not in module %s", pkgPath, modPath))
491510
}
@@ -501,17 +520,17 @@ func makeReleaseReport(base, release moduleInfo) (report, error) {
501520
base: base,
502521
release: release,
503522
}
504-
for _, pair := range zipPackages(base.pkgs, release.pkgs) {
523+
for _, pair := range zipPackages(base.modPath, base.pkgs, release.modPath, release.pkgs) {
505524
basePkg, releasePkg := pair.base, pair.release
506525
switch {
507526
case releasePkg == nil:
508527
// Package removed
509-
if !isInternal(basePkg.PkgPath) || len(basePkg.Errors) > 0 {
528+
if internal := isInternal(base.modPath, basePkg.PkgPath); !internal || len(basePkg.Errors) > 0 {
510529
pr := packageReport{
511530
path: basePkg.PkgPath,
512531
baseErrors: basePkg.Errors,
513532
}
514-
if !isInternal(basePkg.PkgPath) {
533+
if !internal {
515534
pr.Report = apidiff.Report{
516535
Changes: []apidiff.Change{{
517536
Message: "package removed",
@@ -524,12 +543,12 @@ func makeReleaseReport(base, release moduleInfo) (report, error) {
524543

525544
case basePkg == nil:
526545
// Package added
527-
if !isInternal(releasePkg.PkgPath) && shouldCompare || len(releasePkg.Errors) > 0 {
546+
if internal := isInternal(release.modPath, releasePkg.PkgPath); !internal && shouldCompare || len(releasePkg.Errors) > 0 {
528547
pr := packageReport{
529548
path: releasePkg.PkgPath,
530549
releaseErrors: releasePkg.Errors,
531550
}
532-
if !isInternal(releasePkg.PkgPath) && shouldCompare {
551+
if !internal && shouldCompare {
533552
// If we aren't comparing against a base version, don't say
534553
// "package added". Only report packages with errors.
535554
pr.Report = apidiff.Report{
@@ -544,7 +563,10 @@ func makeReleaseReport(base, release moduleInfo) (report, error) {
544563

545564
default:
546565
// Matched packages
547-
if !isInternal(basePkg.PkgPath) && basePkg.Name != "main" && releasePkg.Name != "main" {
566+
// Both packages are internal or neither; we only consider path components
567+
// after the module path.
568+
internal := isInternal(release.modPath, releasePkg.PkgPath)
569+
if !internal && basePkg.Name != "main" && releasePkg.Name != "main" {
548570
pr := packageReport{
549571
path: basePkg.PkgPath,
550572
baseErrors: basePkg.Errors,
@@ -558,7 +580,7 @@ func makeReleaseReport(base, release moduleInfo) (report, error) {
558580

559581
if release.version != "" {
560582
r.validateVersion()
561-
} else {
583+
} else if r.similarModPaths() {
562584
r.suggestVersion()
563585
}
564586

@@ -1056,24 +1078,27 @@ type packagePair struct {
10561078
// If a package is in one list but not the other (because it was added or
10571079
// removed between releases), a pair will be returned with a nil
10581080
// base or release field.
1059-
func zipPackages(basePkgs, releasePkgs []*packages.Package) []packagePair {
1081+
func zipPackages(baseModPath string, basePkgs []*packages.Package, releaseModPath string, releasePkgs []*packages.Package) []packagePair {
10601082
baseIndex, releaseIndex := 0, 0
10611083
var pairs []packagePair
10621084
for baseIndex < len(basePkgs) || releaseIndex < len(releasePkgs) {
10631085
var basePkg, releasePkg *packages.Package
1086+
var baseSuffix, releaseSuffix string
10641087
if baseIndex < len(basePkgs) {
10651088
basePkg = basePkgs[baseIndex]
1089+
baseSuffix = trimPathPrefix(basePkg.PkgPath, baseModPath)
10661090
}
10671091
if releaseIndex < len(releasePkgs) {
10681092
releasePkg = releasePkgs[releaseIndex]
1093+
releaseSuffix = trimPathPrefix(releasePkg.PkgPath, releaseModPath)
10691094
}
10701095

10711096
var pair packagePair
1072-
if basePkg != nil && (releasePkg == nil || basePkg.PkgPath < releasePkg.PkgPath) {
1097+
if basePkg != nil && (releasePkg == nil || baseSuffix < releaseSuffix) {
10731098
// Package removed
10741099
pair = packagePair{basePkg, nil}
10751100
baseIndex++
1076-
} else if releasePkg != nil && (basePkg == nil || releasePkg.PkgPath < basePkg.PkgPath) {
1101+
} else if releasePkg != nil && (basePkg == nil || releaseSuffix < baseSuffix) {
10771102
// Package added
10781103
pair = packagePair{nil, releasePkg}
10791104
releaseIndex++

cmd/gorelease/report.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,14 @@ func (r *report) Text(w io.Writer) error {
6565
}
6666
}
6767

68+
baseVersion := r.base.version
69+
if r.base.modPath != r.release.modPath {
70+
baseVersion = r.base.modPath + "@" + baseVersion
71+
}
6872
if r.base.versionInferred {
69-
fmt.Fprintf(buf, "Inferred base version: %s\n", r.base.version)
73+
fmt.Fprintf(buf, "Inferred base version: %s\n", baseVersion)
7074
} else if r.base.versionQuery != "" {
71-
fmt.Fprintf(buf, "Base version: %s (%s)\n", r.base.version, r.base.versionQuery)
75+
fmt.Fprintf(buf, "Base version: %s (%s)\n", baseVersion, r.base.versionQuery)
7276
}
7377

7478
if len(r.release.diagnostics) > 0 {
@@ -83,7 +87,7 @@ func (r *report) Text(w io.Writer) error {
8387
} else {
8488
fmt.Fprintf(buf, "Suggested version: %[1]s (with tag %[2]s%[1]s)\n", r.release.version, r.release.tagPrefix)
8589
}
86-
} else {
90+
} else if r.release.version != "" && r.similarModPaths() {
8791
if r.release.tagPrefix == "" {
8892
fmt.Fprintf(buf, "%s is a valid semantic version for this release.\n", r.release.version)
8993

@@ -175,7 +179,7 @@ which is required for major versions v2 or greater.`, major)
175179
}
176180

177181
// Check that compatible / incompatible changes are consistent.
178-
if semver.Major(r.base.version) == "v0" {
182+
if semver.Major(r.base.version) == "v0" || r.base.modPath != r.release.modPath {
179183
return
180184
}
181185
if r.haveIncompatibleChanges {
@@ -202,6 +206,11 @@ func (r *report) suggestVersion() {
202206
r.release.versionInferred = true
203207
}
204208

209+
if r.base.modPath != r.release.modPath {
210+
setNotValid("Base module path is different from release.")
211+
return
212+
}
213+
205214
if r.haveReleaseErrors || r.haveBaseErrors {
206215
setNotValid("Errors were found.")
207216
return
@@ -245,6 +254,14 @@ func (r *report) suggestVersion() {
245254
setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch))
246255
}
247256

257+
// similarModPaths returns true if r.base and r.release are versions of the same
258+
// module or different major versions of the same module.
259+
func (r *report) similarModPaths() bool {
260+
basePath := strings.TrimSuffix(r.base.modPath, r.base.modPathMajor)
261+
releasePath := strings.TrimSuffix(r.release.modPath, r.release.modPathMajor)
262+
return basePath == releasePath
263+
}
264+
248265
// requirementsChanged reports whether requirements have changed from base to
249266
// version.
250267
//
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
mod=example.com/basicfork
2+
base=example.com/[email protected]
3+
version=v1.1.2
4+
release=v1.1.2
5+
-- want --
6+
example.com/basic/a
7+
-------------------
8+
Incompatible changes:
9+
- A2: removed
10+
Compatible changes:
11+
- A3: added
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
mod=example.com/basic/v2
2+
base=example.com/basic@>=v1.1.0
3+
version=v2.0.1
4+
release=v2.0.1
5+
-- want --
6+
example.com/basic/a
7+
-------------------
8+
Incompatible changes:
9+
- A2: removed
10+
11+
example.com/basic/b
12+
-------------------
13+
Incompatible changes:
14+
- package removed
15+
16+
Base version: example.com/[email protected] (>=v1.1.0)
17+
v2.0.1 is a valid semantic version for this release.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
mod=example.com/basic/v2
2+
base=example.com/basic
3+
version=v2.1.0
4+
success=false
5+
-- want --
6+
example.com/basic/a
7+
-------------------
8+
Compatible changes:
9+
- A2: added
10+
11+
example.com/basic/v2/b
12+
----------------------
13+
Compatible changes:
14+
- package added
15+
16+
Inferred base version: example.com/[email protected]
17+
Cannot suggest a release version.
18+
Base module path is different from release.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
mod=example.com/basic/v2
2+
base=example.com/basic
3+
version=v2.1.0
4+
release=v2.1.0
5+
-- want --
6+
example.com/basic/a
7+
-------------------
8+
Compatible changes:
9+
- A2: added
10+
11+
example.com/basic/v2/b
12+
----------------------
13+
Compatible changes:
14+
- package added
15+
16+
Inferred base version: example.com/[email protected]
17+
v2.1.0 is a valid semantic version for this release.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
mod=example.com/basic/v2
2+
base=example.com/[email protected]
3+
version=v2.0.1
4+
release=v2.0.1
5+
-- want --
6+
example.com/basic/a
7+
-------------------
8+
Incompatible changes:
9+
- A2: removed
10+
11+
example.com/basic/b
12+
-------------------
13+
Incompatible changes:
14+
- package removed
15+
16+
v2.0.1 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/v2
2+
base=example.com/basic@none
3+
error=true
4+
5+
-- want --
6+
usage: gorelease [-base=version] [-version=version]
7+
base version ("example.com/basic@none") cannot have version "none" with explicit module path
8+
For more information, run go doc golang.org/x/exp/cmd/gorelease
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Modules example.com/internalcompat/{a,b} are copies. One could be a fork
2+
of the other. An external package p exposes a type from a package q
3+
within the same module.
4+
5+
gorelease should not report differences between these packages. The types
6+
are distinct, but they correspond (in apidiff terminology), which is the
7+
important property when considering differences between modules.
8+
9+
There are three use cases to consider:
10+
11+
1. One module substitutes for the other via a `replace` directive.
12+
Only the replacement module is used, and the package paths are effectively
13+
identical, so the types are not distinct.
14+
2. One module subsititutes for the other by rewriting `import` statements
15+
globally. All references to the original type become references to the
16+
new type, so there is no conflict.
17+
3. One module substitutes for the other by rewriting some `import` statements
18+
but not others (for example, those within a specific consumer package).
19+
In this case, the types are distinct, and even if there are no changes,
20+
the types are not compatible.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod=example.com/internalcompat/b
2+
version=v1.0.0
3+
release=v1.0.0
4+
base=example.com/internalcompat/[email protected]
5+
6+
-- want --

0 commit comments

Comments
 (0)