cmd/go/internal/modfile: allow replace without old version

Replace directives to date have looked like

	replace old v1.2.3 => new v1.4.5

As pointed out in golang/go#24825, this can result in fragile go.mod
where the replacement stops kicking in after an upgrade that
affects the selected old version.

The fact that the replacement specifies exactly which version
it is capable of replacing might be a feature, but in other contexts
it's just a hassle. Allow not specifying the version, in which case
the replacement applies to all versions of that module.

Fixes golang/go#24825.

Change-Id: Iad97dde25c06fcba3c1181d650f9fa5e95f83157
Reviewed-on: https://go-review.googlesource.com/122400
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/internal/modcmd/mod.go b/vendor/cmd/go/internal/modcmd/mod.go
index 8e0e51a..aa7a11a 100644
--- a/vendor/cmd/go/internal/modcmd/mod.go
+++ b/vendor/cmd/go/internal/modcmd/mod.go
@@ -59,6 +59,9 @@
 
 The -replace=old@v=>new@w and -dropreplace=old@v flags
 add and drop a replacement of the given module path and version pair.
+If the @v in old@v is omitted, the replacement applies to all versions
+with the old module path. If the @v in new@v is omitted, the
+new path should be a directory on the local system, not a module path.
 Note that -replace overrides any existing replacements for old@v.
 
 These editing flags (-require, -droprequire, -exclude, -dropexclude,
@@ -252,6 +255,7 @@
 			edit(modFile)
 		}
 	}
+	modFile.SortBlocks()
 	modload.WriteGoMod() // write back syntactic changes
 
 	// Semantic edits.
@@ -397,14 +401,16 @@
 		base.Fatalf("go mod: -replace=%s: need old@v=>new[@v] (missing =>)", arg)
 	}
 	old, new := strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+2:])
+	var oldPath, oldVersion string
 	if i = strings.Index(old, "@"); i < 0 {
-		base.Fatalf("go mod: -replace=%s: need old@v=>new[@v] (missing @ in old@v)", arg)
+		oldPath = old
+	} else {
+		oldPath, oldVersion = strings.TrimSpace(old[:i]), strings.TrimSpace(old[i+1:])
 	}
-	oldPath, oldVersion := strings.TrimSpace(old[:i]), strings.TrimSpace(old[i+1:])
 	if err := module.CheckPath(oldPath); err != nil {
 		base.Fatalf("go mod: -replace=%s: invalid old path: %v", arg, err)
 	}
-	if modfile.MustQuote(oldVersion) {
+	if oldPath != old && modfile.MustQuote(oldVersion) {
 		base.Fatalf("go mod: -replace=%s: invalid old version %q", arg, oldVersion)
 	}
 	var newPath, newVersion string
diff --git a/vendor/cmd/go/internal/modfile/rule.go b/vendor/cmd/go/internal/modfile/rule.go
index 1f15c97..80a3bbc 100644
--- a/vendor/cmd/go/internal/modfile/rule.go
+++ b/vendor/cmd/go/internal/modfile/rule.go
@@ -189,8 +189,12 @@
 			})
 		}
 	case "replace":
-		if len(args) < 4 || len(args) > 5 || args[2] != "=>" {
-			fmt.Fprintf(errs, "%s:%d: usage: %s module/path v1.2.3 => other/module v1.4\n\t or %s module/path v1.2.3 => ../local/directory", f.Syntax.Name, line.Start.Line, verb, verb)
+		arrow := 2
+		if len(args) >= 2 && args[1] == "=>" {
+			arrow = 1
+		}
+		if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
+			fmt.Fprintf(errs, "%s:%d: usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", f.Syntax.Name, line.Start.Line, verb, verb)
 			return
 		}
 		s, err := parseString(&args[0])
@@ -198,28 +202,31 @@
 			fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
 			return
 		}
-		old := args[1]
-		v, err := parseVersion(s, &args[1], fix)
-		if err != nil {
-			fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
-			return
-		}
 		v1, err := moduleMajorVersion(s)
 		if err != nil {
 			fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err)
 			return
 		}
-		if v2 := semver.Major(v); v1 != v2 && (v1 != "v1" || v2 != "v0") {
-			fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, v1, v2, v)
-			return
+		var v string
+		if arrow == 2 {
+			old := args[1]
+			v, err = parseVersion(s, &args[1], fix)
+			if err != nil {
+				fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
+				return
+			}
+			if v2 := semver.Major(v); v1 != v2 && (v1 != "v1" || v2 != "v0") {
+				fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, v1, v2, v)
+				return
+			}
 		}
-		ns, err := parseString(&args[3])
+		ns, err := parseString(&args[arrow+1])
 		if err != nil {
 			fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err)
 			return
 		}
 		nv := ""
-		if len(args) == 4 {
+		if len(args) == arrow+2 {
 			if !IsDirectoryPath(ns) {
 				fmt.Fprintf(errs, "%s:%d: replacement module without version must be directory path (rooted or starting with ./ or ../)", f.Syntax.Name, line.Start.Line)
 				return
@@ -229,9 +236,9 @@
 				return
 			}
 		}
-		if len(args) == 5 {
-			old := args[4]
-			nv, err = parseVersion(ns, &args[4], fix)
+		if len(args) == arrow+3 {
+			old := args[arrow+1]
+			nv, err = parseVersion(ns, &args[arrow+2], fix)
 			if err != nil {
 				fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err)
 				return
@@ -241,7 +248,6 @@
 				return
 			}
 		}
-		// TODO: More sanity checks about directories vs module paths.
 		f.Replace = append(f.Replace, &Replace{
 			Old:    module.Version{Path: s, Version: v},
 			New:    module.Version{Path: ns, Version: nv},
@@ -554,14 +560,18 @@
 	need := true
 	old := module.Version{Path: oldPath, Version: oldVers}
 	new := module.Version{Path: newPath, Version: newVers}
-	tokens := []string{"replace", AutoQuote(oldPath), oldVers, "=>", AutoQuote(newPath)}
+	tokens := []string{"replace", AutoQuote(oldPath)}
+	if oldVers != "" {
+		tokens = append(tokens, oldVers)
+	}
+	tokens = append(tokens, "=>", AutoQuote(newPath))
 	if newVers != "" {
 		tokens = append(tokens, newVers)
 	}
 
 	var hint *Line
 	for _, r := range f.Replace {
-		if r.Old == old {
+		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
 			if need {
 				// Found replacement for old; update to use new.
 				r.New = new
diff --git a/vendor/cmd/go/internal/modfile/testdata/replace2.golden b/vendor/cmd/go/internal/modfile/testdata/replace2.golden
index 1d18a3b..e1d9c72 100644
--- a/vendor/cmd/go/internal/modfile/testdata/replace2.golden
+++ b/vendor/cmd/go/internal/modfile/testdata/replace2.golden
@@ -5,4 +5,6 @@
 	xyz v1.3.4 => my/xyz v1.3.4-me
 	xyz v1.4.5 => "/tmp/my dir"
 	xyz v1.5.6 => my/xyz v1.5.6
+
+	xyz => my/other/xyz v1.5.4
 )
diff --git a/vendor/cmd/go/internal/modfile/testdata/replace2.in b/vendor/cmd/go/internal/modfile/testdata/replace2.in
index 78c4669..7864698 100644
--- a/vendor/cmd/go/internal/modfile/testdata/replace2.in
+++ b/vendor/cmd/go/internal/modfile/testdata/replace2.in
@@ -5,4 +5,6 @@
 	"xyz" v1.3.4 => "my/xyz" "v1.3.4-me"
 	xyz "v1.4.5" => "/tmp/my dir"
 	xyz v1.5.6 => my/xyz v1.5.6
+
+	xyz => my/other/xyz v1.5.4
 )
diff --git a/vendor/cmd/go/internal/modload/load.go b/vendor/cmd/go/internal/modload/load.go
index 2528db7..5333c65 100644
--- a/vendor/cmd/go/internal/modload/load.go
+++ b/vendor/cmd/go/internal/modload/load.go
@@ -646,7 +646,7 @@
 	}
 	var found *modfile.Replace
 	for _, r := range modFile.Replace {
-		if r.Old == mod {
+		if r.Old.Path == mod.Path && (r.Old.Version == "" || r.Old.Version == mod.Version) {
 			found = r // keep going
 		}
 	}
diff --git a/vendor/cmd/go/mod_test.go b/vendor/cmd/go/mod_test.go
index dbff0ca..26e6ea1 100644
--- a/vendor/cmd/go/mod_test.go
+++ b/vendor/cmd/go/mod_test.go
@@ -208,6 +208,7 @@
 	tg.must(ioutil.WriteFile(tg.path("w/w.go"), []byte("package w\n"), 0666))
 
 	mustHaveGoMod := func(text string) {
+		t.Helper()
 		data, err := ioutil.ReadFile(tg.path("go.mod"))
 		tg.must(err)
 		if string(data) != text {
@@ -305,6 +306,33 @@
 		t.Fatalf("go mod -json mismatch:\nhave:<<<\n%s>>>\nwant:<<<\n%s\n", have, want)
 	}
 
+	tg.run("mod",
+		"-replace=x.1@v1.3.0=>y.1/v2@v2.3.5",
+		"-replace=x.1@v1.4.0=>y.1/v2@v2.3.5",
+	)
+	mustHaveGoMod(`module x.x/y/z
+
+exclude x.1 v1.2.0
+
+replace (
+	x.1 v1.3.0 => y.1/v2 v2.3.5
+	x.1 v1.4.0 => y.1/v2 v2.3.5
+)
+
+require x.3 v1.99.0
+`)
+	tg.run("mod",
+		"-replace=x.1=>y.1/v2@v2.3.6",
+	)
+	mustHaveGoMod(`module x.x/y/z
+
+exclude x.1 v1.2.0
+
+replace x.1 => y.1/v2 v2.3.6
+
+require x.3 v1.99.0
+`)
+
 	tg.run("mod", "-packages")
 	want = `x.x/y/z
 x.x/y/z/w
@@ -324,7 +352,7 @@
 
 exclude x.1 v1.2.0
 
-replace x.1 v1.4.0 => ../z
+replace x.1 => y.1/v2 v2.3.6
 
 require x.3 v1.99.0
 `)
@@ -696,8 +724,7 @@
 replace y.1 v1.0.0 => ../y
 replace z.1 v1.1.0 => ../z
 replace z.1 v1.2.0 => ../z
-replace w.1 v1.1.0 => ../w
-replace w.1 v1.2.0 => ../w
+replace w.1 => ../w
 `)
 	write("m/m.go", `
 package m