x/exp/cmd/gorelease: suggest a minor version increment if requirements increase

Fixes golang/go#37564

Change-Id: I1aa8a4178adfad99542a478c1cd0fd6a00e23428
Reviewed-on: https://go-review.googlesource.com/c/exp/+/255880
Trust: Jean de Klerk <deklerk@google.com>
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jean de Klerk <deklerk@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/cmd/gorelease/gorelease.go b/cmd/gorelease/gorelease.go
index cc65a28..380e25a 100644
--- a/cmd/gorelease/gorelease.go
+++ b/cmd/gorelease/gorelease.go
@@ -438,6 +438,19 @@
 		return moduleInfo{}, err
 	}
 
+	// Attempt to load the mod file, if it exists.
+	m.goModPath = filepath.Join(m.modRoot, "go.mod")
+	if m.goModData, err = ioutil.ReadFile(m.goModPath); err != nil && !os.IsNotExist(err) {
+		return moduleInfo{}, fmt.Errorf("reading go.mod: %v", err)
+	}
+	if err == nil {
+		m.goModFile, err = modfile.ParseLax(m.goModPath, m.goModData, nil)
+		if err != nil {
+			return moduleInfo{}, err
+		}
+	}
+	// The modfile might not exist, leading to err != nil. That's OK - continue.
+
 	return m, nil
 }
 
diff --git a/cmd/gorelease/report.go b/cmd/gorelease/report.go
index 8985991..32311c4 100644
--- a/cmd/gorelease/report.go
+++ b/cmd/gorelease/report.go
@@ -231,7 +231,7 @@
 
 	if pre != "" {
 		// suggest non-prerelease version
-	} else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") {
+	} else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") || r.requirementsChanged() {
 		minor = incDecimal(minor)
 		patch = "0"
 	} else {
@@ -240,6 +240,52 @@
 	setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch))
 }
 
+// requirementsChanged reports whether requirements have changed from base to
+// version.
+//
+// requirementsChanged reports true for,
+//   - A requirement was upgraded to a higher minor version.
+//   - A requirement was added.
+//   - The version of Go was incremented.
+//
+// It does not report true when, for example, a requirement was downgraded or
+// remove. We care more about the former since that might force dependent
+// modules that have the same dependency to upgrade.
+func (r *report) requirementsChanged() bool {
+	if r.base.goModFile == nil {
+		// There wasn't a modfile before, and now there is.
+		return true
+	}
+
+	// baseReqs is a map of module path to MajorMinor of the base module
+	// requirements.
+	baseReqs := make(map[string]string)
+	for _, r := range r.base.goModFile.Require {
+		baseReqs[r.Mod.Path] = r.Mod.Version
+	}
+
+	for _, r := range r.release.goModFile.Require {
+		if _, ok := baseReqs[r.Mod.Path]; !ok {
+			// A module@version was added to the "require" block between base
+			// and release.
+			return true
+		}
+		if semver.Compare(semver.MajorMinor(r.Mod.Version), semver.MajorMinor(baseReqs[r.Mod.Path])) > 0 {
+			// The version of r.Mod.Path increased from base to release.
+			return true
+		}
+	}
+
+	if r.release.goModFile.Go != nil && r.base.goModFile.Go != nil {
+		if r.release.goModFile.Go.Version > r.base.goModFile.Go.Version {
+			// The Go version increased from base to release.
+			return true
+		}
+	}
+
+	return false
+}
+
 // isSuccessful returns true the module appears to be safe to release at the
 // proposed or suggested version.
 func (r *report) isSuccessful() bool {
diff --git a/cmd/gorelease/testdata/mod/example.com_require_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_require_v0.0.1.txt
new file mode 100644
index 0000000..bc7200f
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_require_v0.0.1.txt
@@ -0,0 +1,6 @@
+-- go.mod --
+module example.com/require
+
+go 1.12
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/tidy/extra_req.test b/cmd/gorelease/testdata/mod/example.com_require_v0.1.0.txt
similarity index 76%
copy from cmd/gorelease/testdata/tidy/extra_req.test
copy to cmd/gorelease/testdata/mod/example.com_require_v0.1.0.txt
index 58973c3..8f858bf 100644
--- a/cmd/gorelease/testdata/tidy/extra_req.test
+++ b/cmd/gorelease/testdata/mod/example.com_require_v0.1.0.txt
@@ -1,9 +1,5 @@
-mod=example.com/tidy
-base=v0.0.1
--- want --
-Suggested version: v0.0.2
 -- go.mod --
-module example.com/tidy
+module example.com/require
 
 go 1.12
 
@@ -13,5 +9,6 @@
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
--- tidy.go --
-package tidy
+-- require.go --
+package require
+
diff --git a/cmd/gorelease/testdata/mod/example.com_require_v0.1.1.txt b/cmd/gorelease/testdata/mod/example.com_require_v0.1.1.txt
new file mode 100644
index 0000000..620bf66
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_require_v0.1.1.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/require
+
+go 1.12
+
+require example.com/basic v1.1.0
+-- go.sum --
+example.com/basic v1.1.0/go.mod h1:pv9xTX7lhV6R1XNYo1EcI/DQqKxDyhNTN+K1DjHW2Oo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/require/README.txt b/cmd/gorelease/testdata/require/README.txt
new file mode 100644
index 0000000..f905232
--- /dev/null
+++ b/cmd/gorelease/testdata/require/README.txt
@@ -0,0 +1,2 @@
+This directory contain tests that assert gorelease behavior when module
+requirements (and require statements in the go.mod) have changed.
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/tidy/extra_req.test b/cmd/gorelease/testdata/require/add_requirement.test
similarity index 79%
rename from cmd/gorelease/testdata/tidy/extra_req.test
rename to cmd/gorelease/testdata/require/add_requirement.test
index 58973c3..3cbc78b 100644
--- a/cmd/gorelease/testdata/tidy/extra_req.test
+++ b/cmd/gorelease/testdata/require/add_requirement.test
@@ -1,9 +1,9 @@
-mod=example.com/tidy
+mod=example.com/require
 base=v0.0.1
 -- want --
-Suggested version: v0.0.2
+Suggested version: v0.1.0
 -- go.mod --
-module example.com/tidy
+module example.com/require
 
 go 1.12
 
@@ -13,5 +13,5 @@
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
--- tidy.go --
-package tidy
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/require/decrement_go_version.test b/cmd/gorelease/testdata/require/decrement_go_version.test
new file mode 100644
index 0000000..abcd56f
--- /dev/null
+++ b/cmd/gorelease/testdata/require/decrement_go_version.test
@@ -0,0 +1,11 @@
+mod=example.com/require
+base=v0.0.1
+-- want --
+Suggested version: v0.0.2
+-- go.mod --
+module example.com/require
+
+go 1.11
+-- go.sum --
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/require/decrement_requirement_minor.test b/cmd/gorelease/testdata/require/decrement_requirement_minor.test
new file mode 100644
index 0000000..02f3683
--- /dev/null
+++ b/cmd/gorelease/testdata/require/decrement_requirement_minor.test
@@ -0,0 +1,17 @@
+mod=example.com/require
+base=v0.1.1
+-- want --
+Suggested version: v0.1.2
+-- go.mod --
+module example.com/require
+
+go 1.12
+
+require example.com/basic v0.0.1
+-- go.sum --
+example.com/basic v0.0.1/go.mod h1:pv9xTX7lhV6R1XNYo1EcI/DQqKxDyhNTN+K1DjHW2Oo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/require/increment_go_version.test b/cmd/gorelease/testdata/require/increment_go_version.test
new file mode 100644
index 0000000..2f7c993
--- /dev/null
+++ b/cmd/gorelease/testdata/require/increment_go_version.test
@@ -0,0 +1,11 @@
+mod=example.com/require
+base=v0.0.1
+-- want --
+Suggested version: v0.1.0
+-- go.mod --
+module example.com/require
+
+go 1.13
+-- go.sum --
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/require/increment_requirement_minor.test b/cmd/gorelease/testdata/require/increment_requirement_minor.test
new file mode 100644
index 0000000..a453b3e
--- /dev/null
+++ b/cmd/gorelease/testdata/require/increment_requirement_minor.test
@@ -0,0 +1,17 @@
+mod=example.com/require
+base=v0.1.0
+-- want --
+Suggested version: v0.2.0
+-- go.mod --
+module example.com/require
+
+go 1.12
+
+require example.com/basic v1.1.0
+-- go.sum --
+example.com/basic v1.1.0/go.mod h1:pv9xTX7lhV6R1XNYo1EcI/DQqKxDyhNTN+K1DjHW2Oo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/require/increment_requirement_patch.test b/cmd/gorelease/testdata/require/increment_requirement_patch.test
new file mode 100644
index 0000000..0469ae4
--- /dev/null
+++ b/cmd/gorelease/testdata/require/increment_requirement_patch.test
@@ -0,0 +1,17 @@
+mod=example.com/require
+base=v0.1.1
+-- want --
+Suggested version: v0.1.2
+-- go.mod --
+module example.com/require
+
+go 1.12
+
+require example.com/basic v1.1.1
+-- go.sum --
+example.com/basic v1.1.1/go.mod h1:pv9xTX7lhV6R1XNYo1EcI/DQqKxDyhNTN+K1DjHW2Oo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+-- require.go --
+package require
diff --git a/cmd/gorelease/testdata/require/remove_requirements.test b/cmd/gorelease/testdata/require/remove_requirements.test
new file mode 100644
index 0000000..f57d12a
--- /dev/null
+++ b/cmd/gorelease/testdata/require/remove_requirements.test
@@ -0,0 +1,11 @@
+mod=example.com/require
+base=v0.0.1
+-- want --
+Suggested version: v0.0.2
+-- go.mod --
+module example.com/require
+
+go 1.12
+-- go.sum --
+-- require.go --
+package require