cmd/go/internal/modconv: support convert replacements in Gopkg.lock

Fixes #24087.
Updates #26711.

Change-Id: I7fe6b21fd391253a19cb1d35709a061872ea7b6e
Reviewed-on: https://go-review.googlesource.com/c/go/+/126915
Run-TryBot: Baokun Lee <nototon@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/src/cmd/go/internal/modconv/convert.go b/src/cmd/go/internal/modconv/convert.go
index 6fc6718..558664a 100644
--- a/src/cmd/go/internal/modconv/convert.go
+++ b/src/cmd/go/internal/modconv/convert.go
@@ -41,19 +41,29 @@
 
 	// Convert requirements block, which may use raw SHA1 hashes as versions,
 	// to valid semver requirement list, respecting major versions.
-	var work par.Work
+	var (
+		work    par.Work
+		mu      sync.Mutex
+		need    = make(map[string]string)
+		replace = make(map[string]*modfile.Replace)
+	)
+
+	for _, r := range mf.Replace {
+		replace[r.New.Path] = r
+		replace[r.Old.Path] = r
+	}
 	for _, r := range mf.Require {
 		m := r.Mod
 		if m.Path == "" {
 			continue
 		}
+		if re, ok := replace[m.Path]; ok {
+			work.Add(re.New)
+			continue
+		}
 		work.Add(r.Mod)
 	}
 
-	var (
-		mu   sync.Mutex
-		need = make(map[string]string)
-	)
 	work.Do(10, func(item interface{}) {
 		r := item.(module.Version)
 		repo, info, err := modfetch.ImportRepoRev(r.Path, r.Version)
@@ -76,15 +86,15 @@
 	}
 	sort.Strings(paths)
 	for _, path := range paths {
+		if re, ok := replace[path]; ok {
+			err := f.AddReplace(re.Old.Path, re.Old.Version, path, need[path])
+			if err != nil {
+				return fmt.Errorf("add replace: %v", err)
+			}
+		}
 		f.AddNewRequire(path, need[path], false)
 	}
 
-	for _, r := range mf.Replace {
-		err := f.AddReplace(r.Old.Path, r.Old.Version, r.New.Path, r.New.Version)
-		if err != nil {
-			return fmt.Errorf("add replace: %v", err)
-		}
-	}
 	f.Cleanup()
 	return nil
 }
diff --git a/src/cmd/go/internal/modconv/dep.go b/src/cmd/go/internal/modconv/dep.go
index 690c206..f433300 100644
--- a/src/cmd/go/internal/modconv/dep.go
+++ b/src/cmd/go/internal/modconv/dep.go
@@ -6,6 +6,9 @@
 
 import (
 	"fmt"
+	"net/url"
+	"path"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -15,9 +18,14 @@
 )
 
 func ParseGopkgLock(file string, data []byte) (*modfile.File, error) {
+	type pkg struct {
+		Path    string
+		Version string
+		Source  string
+	}
 	mf := new(modfile.File)
-	var list []module.Version
-	var r *module.Version
+	var list []pkg
+	var r *pkg
 	for lineno, line := range strings.Split(string(data), "\n") {
 		lineno++
 		if i := strings.Index(line, "#"); i >= 0 {
@@ -25,7 +33,7 @@
 		}
 		line = strings.TrimSpace(line)
 		if line == "[[projects]]" {
-			list = append(list, module.Version{})
+			list = append(list, pkg{})
 			r = &list[len(list)-1]
 			continue
 		}
@@ -52,6 +60,8 @@
 		switch key {
 		case "name":
 			r.Path = val
+		case "source":
+			r.Source = val
 		case "revision", "version":
 			// Note: key "version" should take priority over "revision",
 			// and it does, because dep writes toml keys in alphabetical order,
@@ -68,7 +78,55 @@
 		if r.Path == "" || r.Version == "" {
 			return nil, fmt.Errorf("%s: empty [[projects]] stanza (%s)", file, r.Path)
 		}
-		mf.Require = append(mf.Require, &modfile.Require{Mod: r})
+		mf.Require = append(mf.Require, &modfile.Require{Mod: module.Version{Path: r.Path, Version: r.Version}})
+
+		if r.Source != "" {
+			// Convert "source" to import path, such as
+			// git@test.com:x/y.git and https://test.com/x/y.git.
+			// We get "test.com/x/y" at last.
+			source, err := decodeSource(r.Source)
+			if err != nil {
+				return nil, err
+			}
+			old := module.Version{Path: r.Path, Version: r.Version}
+			new := module.Version{Path: source, Version: r.Version}
+			mf.Replace = append(mf.Replace, &modfile.Replace{Old: old, New: new})
+		}
 	}
 	return mf, nil
 }
+
+var scpSyntaxReg = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
+
+func decodeSource(source string) (string, error) {
+	var u *url.URL
+	var p string
+	if m := scpSyntaxReg.FindStringSubmatch(source); m != nil {
+		// Match SCP-like syntax and convert it to a URL.
+		// Eg, "git@github.com:user/repo" becomes
+		// "ssh://git@github.com/user/repo".
+		u = &url.URL{
+			Scheme: "ssh",
+			User:   url.User(m[1]),
+			Host:   m[2],
+			Path:   "/" + m[3],
+		}
+	} else {
+		var err error
+		u, err = url.Parse(source)
+		if err != nil {
+			return "", fmt.Errorf("%q is not a valid URI", source)
+		}
+	}
+
+	// If no scheme was passed, then the entire path will have been put into
+	// u.Path. Either way, construct the normalized path correctly.
+	if u.Host == "" {
+		p = source
+	} else {
+		p = path.Join(u.Host, u.Path)
+	}
+	p = strings.TrimSuffix(p, ".git")
+	p = strings.TrimSuffix(p, ".hg")
+	return p, nil
+}
diff --git a/src/cmd/go/internal/modconv/modconv_test.go b/src/cmd/go/internal/modconv/modconv_test.go
index 353161b..ccc4f3d 100644
--- a/src/cmd/go/internal/modconv/modconv_test.go
+++ b/src/cmd/go/internal/modconv/modconv_test.go
@@ -58,6 +58,9 @@
 			for _, r := range out.Require {
 				fmt.Fprintf(&buf, "%s %s\n", r.Mod.Path, r.Mod.Version)
 			}
+			for _, r := range out.Replace {
+				fmt.Fprintf(&buf, "replace: %s %s %s %s\n", r.Old.Path, r.Old.Version, r.New.Path, r.New.Version)
+			}
 			if !bytes.Equal(buf.Bytes(), want) {
 				t.Errorf("have:\n%s\nwant:\n%s", buf.Bytes(), want)
 			}
diff --git a/src/cmd/go/internal/modconv/testdata/traefik.dep b/src/cmd/go/internal/modconv/testdata/traefik.dep
new file mode 100644
index 0000000..8510f0f
--- /dev/null
+++ b/src/cmd/go/internal/modconv/testdata/traefik.dep
@@ -0,0 +1,79 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+[[projects]]
+  name = "github.com/Nvveen/Gotty"
+  packages = ["."]
+  revision = "a8b993ba6abdb0e0c12b0125c603323a71c7790c"
+  source = "github.com/ijc25/Gotty"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/OpenDNS/vegadns2client"
+  packages = ["."]
+  revision = "a3fa4a771d87bda2514a90a157e1fed1b6897d2e"
+
+[[projects]]
+  name = "github.com/PuerkitoBio/purell"
+  packages = ["."]
+  revision = "8a290539e2e8629dbc4e6bad948158f790ec31f4"
+  version = "v1.0.0"
+
+[[projects]]
+  name = "github.com/PuerkitoBio/urlesc"
+  packages = ["."]
+  revision = "5bd2802263f21d8788851d5305584c82a5c75d7e"
+
+[[projects]]
+  name = "github.com/Shopify/sarama"
+  packages = ["."]
+  revision = "70f6a705d4a17af059acbc6946fb2bd30762acd7"
+
+[[projects]]
+  name = "github.com/VividCortex/gohistogram"
+  packages = ["."]
+  revision = "51564d9861991fb0ad0f531c99ef602d0f9866e6"
+  version = "v1.0.0"
+
+[[projects]]
+  branch = "containous-fork"
+  name = "github.com/abbot/go-http-auth"
+  packages = ["."]
+  revision = "65b0cdae8d7fe5c05c7430e055938ef6d24a66c9"
+  source = "github.com/containous/go-http-auth"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/abronan/valkeyrie"
+  packages = [
+    ".",
+    "store",
+    "store/boltdb",
+    "store/consul",
+    "store/etcd/v2",
+    "store/etcd/v3",
+    "store/zookeeper"
+  ]
+  revision = "063d875e3c5fd734fa2aa12fac83829f62acfc70"
+  
+[[projects]]
+  branch = "master"
+  name = "github.com/mesosphere/mesos-dns"
+  packages = [
+    "detect",
+    "errorutil",
+    "logging",
+    "models",
+    "records",
+    "records/labels",
+    "records/state",
+    "util"
+  ]
+  revision = "b47dc4c19f215e98da687b15b4c64e70f629bea5"
+  source = "git@github.com:containous/mesos-dns.git"
+
+  [[projects]]
+  name = "gopkg.in/fsnotify.v1"
+  packages = ["."]
+  revision = "629574ca2a5df945712d3079857300b5e4da0236"
+  source = "github.com/fsnotify/fsnotify"
+  version = "v1.4.2"
\ No newline at end of file
diff --git a/src/cmd/go/internal/modconv/testdata/traefik.out b/src/cmd/go/internal/modconv/testdata/traefik.out
new file mode 100644
index 0000000..5054295
--- /dev/null
+++ b/src/cmd/go/internal/modconv/testdata/traefik.out
@@ -0,0 +1,14 @@
+github.com/Nvveen/Gotty a8b993ba6abdb0e0c12b0125c603323a71c7790c
+github.com/OpenDNS/vegadns2client a3fa4a771d87bda2514a90a157e1fed1b6897d2e
+github.com/PuerkitoBio/purell v1.0.0
+github.com/PuerkitoBio/urlesc 5bd2802263f21d8788851d5305584c82a5c75d7e
+github.com/Shopify/sarama 70f6a705d4a17af059acbc6946fb2bd30762acd7
+github.com/VividCortex/gohistogram v1.0.0
+github.com/abbot/go-http-auth 65b0cdae8d7fe5c05c7430e055938ef6d24a66c9
+github.com/abronan/valkeyrie 063d875e3c5fd734fa2aa12fac83829f62acfc70
+github.com/mesosphere/mesos-dns b47dc4c19f215e98da687b15b4c64e70f629bea5
+gopkg.in/fsnotify.v1 v1.4.2
+replace: github.com/Nvveen/Gotty a8b993ba6abdb0e0c12b0125c603323a71c7790c github.com/ijc25/Gotty a8b993ba6abdb0e0c12b0125c603323a71c7790c
+replace: github.com/abbot/go-http-auth 65b0cdae8d7fe5c05c7430e055938ef6d24a66c9 github.com/containous/go-http-auth 65b0cdae8d7fe5c05c7430e055938ef6d24a66c9
+replace: github.com/mesosphere/mesos-dns b47dc4c19f215e98da687b15b4c64e70f629bea5 github.com/containous/mesos-dns b47dc4c19f215e98da687b15b4c64e70f629bea5
+replace: gopkg.in/fsnotify.v1 v1.4.2 github.com/fsnotify/fsnotify v1.4.2
diff --git a/src/cmd/go/testdata/script/mod_init_dep.txt b/src/cmd/go/testdata/script/mod_init_dep.txt
index 29c840b..8cb3fa8 100644
--- a/src/cmd/go/testdata/script/mod_init_dep.txt
+++ b/src/cmd/go/testdata/script/mod_init_dep.txt
@@ -21,6 +21,11 @@
 go list -m all
 stdout 'rsc.io/sampler v1.0.0'
 
+# test dep replacement
+cd y
+go mod init
+cmp go.mod go.mod.replace
+
 -- go.mod1 --
 module x
 
@@ -32,3 +37,21 @@
   name = "rsc.io/sampler"
   version = "v1.0.0"
 
+-- y/Gopkg.lock --
+[[projects]]
+  name = "z"
+  revision = "v1.0.0"
+  source = "rsc.io/quote"
+
+-- y/y.go --
+package y // import "y"
+import _ "z"
+
+-- y/go.mod.replace --
+module y
+
+go 1.13
+
+replace z v1.0.0 => rsc.io/quote v1.0.0
+
+require rsc.io/quote v1.0.0
\ No newline at end of file