cmd/go: add -retract and -dropretract flags to 'go mod edit'

'go mod edit' can now add and remove 'retract' directives from go.mod
files.

Also, retractions are now included in the 'go mod edit -json' output.

For #24031

Change-Id: Ife7915e259fa508626d6ec5f786b5c860b489599
Reviewed-on: https://go-review.googlesource.com/c/go/+/228381
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go
index f50529c..609ede4 100644
--- a/src/cmd/go/alldocs.go
+++ b/src/cmd/go/alldocs.go
@@ -1100,9 +1100,14 @@
 // module path and version pair. If the @v is omitted, a replacement without
 // a version on the left side is dropped.
 //
+// The -retract=version and -dropretract=version flags add and drop a
+// retraction on the given version. The version may be a single version
+// like "v1.2.3" or a closed interval like "[v1.1.0-v1.1.9]". Note that
+// -retract=version is a no-op if that retraction already exists.
+//
 // The -require, -droprequire, -exclude, -dropexclude, -replace,
-// and -dropreplace editing flags may be repeated, and the changes
-// are applied in the order given.
+// -dropreplace, -retract, and -dropretract editing flags may be repeated,
+// and the changes are applied in the order given.
 //
 // The -go=version flag sets the expected Go language version.
 //
@@ -1136,6 +1141,15 @@
 // 		New Module
 // 	}
 //
+// 	type Retract struct {
+// 		Low       string
+// 		High      string
+// 		Rationale string
+// 	}
+//
+// Retract entries representing a single version (not an interval) will have
+// the "Low" and "High" fields set to the same value.
+//
 // Note that this only describes the go.mod file itself, not other modules
 // referred to indirectly. For the full set of modules available to a build,
 // use 'go list -m -json all'.
diff --git a/src/cmd/go/internal/modcmd/edit.go b/src/cmd/go/internal/modcmd/edit.go
index a81c252..18bdd34 100644
--- a/src/cmd/go/internal/modcmd/edit.go
+++ b/src/cmd/go/internal/modcmd/edit.go
@@ -68,9 +68,14 @@
 module path and version pair. If the @v is omitted, a replacement without
 a version on the left side is dropped.
 
+The -retract=version and -dropretract=version flags add and drop a
+retraction on the given version. The version may be a single version
+like "v1.2.3" or a closed interval like "[v1.1.0-v1.1.9]". Note that
+-retract=version is a no-op if that retraction already exists.
+
 The -require, -droprequire, -exclude, -dropexclude, -replace,
-and -dropreplace editing flags may be repeated, and the changes
-are applied in the order given.
+-dropreplace, -retract, and -dropretract editing flags may be repeated,
+and the changes are applied in the order given.
 
 The -go=version flag sets the expected Go language version.
 
@@ -104,6 +109,15 @@
 		New Module
 	}
 
+	type Retract struct {
+		Low       string
+		High      string
+		Rationale string
+	}
+
+Retract entries representing a single version (not an interval) will have
+the "Low" and "High" fields set to the same value.
+
 Note that this only describes the go.mod file itself, not other modules
 referred to indirectly. For the full set of modules available to a build,
 use 'go list -m -json all'.
@@ -137,6 +151,8 @@
 	cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "")
 	cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "")
 	cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "")
+	cmdEdit.Flag.Var(flagFunc(flagRetract), "retract", "")
+	cmdEdit.Flag.Var(flagFunc(flagDropRetract), "dropretract", "")
 
 	work.AddModCommonFlags(cmdEdit)
 	base.AddBuildFlagsNX(&cmdEdit.Flag)
@@ -252,12 +268,7 @@
 		base.Fatalf("go mod: -%s=%s: invalid path: %v", flag, arg, err)
 	}
 
-	// We don't call modfile.CheckPathVersion, because that insists
-	// on versions being in semver form, but here we want to allow
-	// versions like "master" or "1234abcdef", which the go command will resolve
-	// the next time it runs (or during -fix).
-	// Even so, we need to make sure the version is a valid token.
-	if modfile.MustQuote(version) {
+	if !allowedVersionArg(version) {
 		base.Fatalf("go mod: -%s=%s: invalid version %q", flag, arg, version)
 	}
 
@@ -289,12 +300,48 @@
 			return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
 		}
 	}
-	if path != arg && modfile.MustQuote(version) {
+	if path != arg && !allowedVersionArg(version) {
 		return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
 	}
 	return path, version, nil
 }
 
+// parseVersionInterval parses a single version like "v1.2.3" or a closed
+// interval like "[v1.2.3,v1.4.5]". Note that a single version has the same
+// representation as an interval with equal upper and lower bounds: both
+// Low and High are set.
+func parseVersionInterval(arg string) (modfile.VersionInterval, error) {
+	if !strings.HasPrefix(arg, "[") {
+		if !allowedVersionArg(arg) {
+			return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg)
+		}
+		return modfile.VersionInterval{Low: arg, High: arg}, nil
+	}
+	if !strings.HasSuffix(arg, "]") {
+		return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
+	}
+	s := arg[1 : len(arg)-1]
+	i := strings.Index(s, ",")
+	if i < 0 {
+		return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
+	}
+	low := strings.TrimSpace(s[:i])
+	high := strings.TrimSpace(s[i+1:])
+	if !allowedVersionArg(low) || !allowedVersionArg(high) {
+		return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
+	}
+	return modfile.VersionInterval{Low: low, High: high}, nil
+}
+
+// allowedVersionArg returns whether a token may be used as a version in go.mod.
+// We don't call modfile.CheckPathVersion, because that insists on versions
+// being in semver form, but here we want to allow versions like "master" or
+// "1234abcdef", which the go command will resolve the next time it runs (or
+// during -fix).  Even so, we need to make sure the version is a valid token.
+func allowedVersionArg(arg string) bool {
+	return !modfile.MustQuote(arg)
+}
+
 // flagRequire implements the -require flag.
 func flagRequire(arg string) {
 	path, version := parsePathVersion("require", arg)
@@ -377,6 +424,32 @@
 	})
 }
 
+// flagRetract implements the -retract flag.
+func flagRetract(arg string) {
+	vi, err := parseVersionInterval(arg)
+	if err != nil {
+		base.Fatalf("go mod: -retract=%s: %v", arg, err)
+	}
+	edits = append(edits, func(f *modfile.File) {
+		if err := f.AddRetract(vi, ""); err != nil {
+			base.Fatalf("go mod: -retract=%s: %v", arg, err)
+		}
+	})
+}
+
+// flagDropRetract implements the -dropretract flag.
+func flagDropRetract(arg string) {
+	vi, err := parseVersionInterval(arg)
+	if err != nil {
+		base.Fatalf("go mod: -dropretract=%s: %v", arg, err)
+	}
+	edits = append(edits, func(f *modfile.File) {
+		if err := f.DropRetract(vi); err != nil {
+			base.Fatalf("go mod: -dropretract=%s: %v", arg, err)
+		}
+	})
+}
+
 // fileJSON is the -json output data structure.
 type fileJSON struct {
 	Module  module.Version
@@ -384,6 +457,7 @@
 	Require []requireJSON
 	Exclude []module.Version
 	Replace []replaceJSON
+	Retract []retractJSON
 }
 
 type requireJSON struct {
@@ -397,6 +471,12 @@
 	New module.Version
 }
 
+type retractJSON struct {
+	Low       string `json:",omitempty"`
+	High      string `json:",omitempty"`
+	Rationale string `json:",omitempty"`
+}
+
 // editPrintJSON prints the -json output.
 func editPrintJSON(modFile *modfile.File) {
 	var f fileJSON
@@ -415,6 +495,9 @@
 	for _, r := range modFile.Replace {
 		f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
 	}
+	for _, r := range modFile.Retract {
+		f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale})
+	}
 	data, err := json.MarshalIndent(&f, "", "\t")
 	if err != nil {
 		base.Fatalf("go: internal error: %v", err)
diff --git a/src/cmd/go/testdata/script/mod_edit.txt b/src/cmd/go/testdata/script/mod_edit.txt
index 898d852..78485eb 100644
--- a/src/cmd/go/testdata/script/mod_edit.txt
+++ b/src/cmd/go/testdata/script/mod_edit.txt
@@ -16,15 +16,19 @@
 cmpenv go.mod $WORK/go.mod.init
 
 # go mod edits
-go mod edit -droprequire=x.1 -require=x.1@v1.0.0 -require=x.2@v1.1.0 -droprequire=x.2 -exclude='x.1 @ v1.2.0' -exclude=x.1@v1.2.1 -replace=x.1@v1.3.0=y.1@v1.4.0 -replace='x.1@v1.4.0 = ../z'
+go mod edit -droprequire=x.1 -require=x.1@v1.0.0 -require=x.2@v1.1.0 -droprequire=x.2 -exclude='x.1 @ v1.2.0' -exclude=x.1@v1.2.1 -replace=x.1@v1.3.0=y.1@v1.4.0 -replace='x.1@v1.4.0 = ../z' -retract=v1.6.0 -retract=[v1.1.0,v1.2.0] -retract=[v1.3.0,v1.4.0] -retract=v1.0.0
 cmpenv go.mod $WORK/go.mod.edit1
-go mod edit -droprequire=x.1 -dropexclude=x.1@v1.2.1 -dropreplace=x.1@v1.3.0 -require=x.3@v1.99.0
+go mod edit -droprequire=x.1 -dropexclude=x.1@v1.2.1 -dropreplace=x.1@v1.3.0 -require=x.3@v1.99.0 -dropretract=v1.0.0 -dropretract=[v1.1.0,v1.2.0]
 cmpenv go.mod $WORK/go.mod.edit2
 
 # go mod edit -json
 go mod edit -json
 cmpenv stdout $WORK/go.mod.json
 
+# go mod edit -json (retractions with rationales)
+go mod edit -json $WORK/go.mod.retractrationale
+cmp stdout $WORK/go.mod.retractrationale.json
+
 # go mod edit -json (empty mod file)
 go mod edit -json $WORK/go.mod.empty
 cmp stdout $WORK/go.mod.empty.json
@@ -40,11 +44,11 @@
 # go mod edit -fmt
 cp $WORK/go.mod.badfmt go.mod
 go mod edit -fmt -print # -print should avoid writing file
-cmpenv stdout $WORK/go.mod.edit6
+cmpenv stdout $WORK/go.mod.goodfmt
 cmp go.mod $WORK/go.mod.badfmt
 go mod edit -fmt # without -print, should write file (and nothing to stdout)
 ! stdout .
-cmpenv go.mod $WORK/go.mod.edit6
+cmpenv go.mod $WORK/go.mod.goodfmt
 
 # go mod edit -module
 cd $WORK/m
@@ -84,6 +88,13 @@
 	x.1 v1.3.0 => y.1 v1.4.0
 	x.1 v1.4.0 => ../z
 )
+
+retract (
+	v1.6.0
+	[v1.3.0, v1.4.0]
+	[v1.1.0, v1.2.0]
+	v1.0.0
+)
 -- $WORK/go.mod.edit2 --
 module x.x/y/z
 
@@ -93,6 +104,11 @@
 
 replace x.1 v1.4.0 => ../z
 
+retract (
+	v1.6.0
+	[v1.3.0, v1.4.0]
+)
+
 require x.3 v1.99.0
 -- $WORK/go.mod.json --
 {
@@ -122,6 +138,16 @@
 				"Path": "../z"
 			}
 		}
+	],
+	"Retract": [
+		{
+			"Low": "v1.6.0",
+			"High": "v1.6.0"
+		},
+		{
+			"Low": "v1.3.0",
+			"High": "v1.4.0"
+		}
 	]
 }
 -- $WORK/go.mod.edit3 --
@@ -136,6 +162,11 @@
 	x.1 v1.4.0 => y.1/v2 v2.3.5
 )
 
+retract (
+	v1.6.0
+	[v1.3.0, v1.4.0]
+)
+
 require x.3 v1.99.0
 -- $WORK/go.mod.edit4 --
 module x.x/y/z
@@ -146,6 +177,11 @@
 
 replace x.1 => y.1/v2 v2.3.6
 
+retract (
+	v1.6.0
+	[v1.3.0, v1.4.0]
+)
+
 require x.3 v1.99.0
 -- $WORK/go.mod.edit5 --
 module x.x/y/z
@@ -154,15 +190,10 @@
 
 exclude x.1 v1.2.0
 
-require x.3 v1.99.0
--- $WORK/go.mod.edit6 --
-module x.x/y/z
-
-go 1.10
-
-exclude x.1 v1.2.0
-
-replace x.1 => y.1/v2 v2.3.6
+retract (
+	v1.6.0
+	[v1.3.0, v1.4.0]
+)
 
 require x.3 v1.99.0
 -- $WORK/local/go.mod.edit --
@@ -183,10 +214,64 @@
 replace x.1    =>   y.1/v2 v2.3.6
 
 require x.3   v1.99.0
+
+retract [  "v1.8.1" , "v1.8.2" ]
+-- $WORK/go.mod.goodfmt --
+module x.x/y/z
+
+go 1.10
+
+exclude x.1 v1.2.0
+
+replace x.1 => y.1/v2 v2.3.6
+
+require x.3 v1.99.0
+
+retract [v1.8.1, v1.8.2]
 -- $WORK/m/go.mod.edit --
 module x.x/y/z
 
 go $goversion
+-- $WORK/go.mod.retractrationale --
+module x.x/y/z
+
+go 1.15
+
+// a
+retract v1.0.0
+
+// b
+retract (
+  v1.0.1
+  v1.0.2 // c
+)
+-- $WORK/go.mod.retractrationale.json --
+{
+	"Module": {
+		"Path": "x.x/y/z"
+	},
+	"Go": "1.15",
+	"Require": null,
+	"Exclude": null,
+	"Replace": null,
+	"Retract": [
+		{
+			"Low": "v1.0.0",
+			"High": "v1.0.0",
+			"Rationale": "a"
+		},
+		{
+			"Low": "v1.0.1",
+			"High": "v1.0.1",
+			"Rationale": "b"
+		},
+		{
+			"Low": "v1.0.2",
+			"High": "v1.0.2",
+			"Rationale": "c"
+		}
+	]
+}
 -- $WORK/go.mod.empty --
 -- $WORK/go.mod.empty.json --
 {
@@ -195,5 +280,6 @@
 	},
 	"Require": null,
 	"Exclude": null,
-	"Replace": null
+	"Replace": null,
+	"Retract": null
 }