cmd/go/internal/modcmd: remove unused modules during -sync

Fixes golang/go#24101.

Change-Id: I3900502372281515b49e98ed1465c2aa8d9a016b
Reviewed-on: https://go-review.googlesource.com/118877
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 55c7a61..b97cd8c 100644
--- a/vendor/cmd/go/internal/modcmd/mod.go
+++ b/vendor/cmd/go/internal/modcmd/mod.go
@@ -236,15 +236,34 @@
 	needBuildList := *modFix
 
 	if *modSync || *modVendor || needBuildList {
+		var pkgs []string
 		if *modSync || *modVendor {
-			fmt.Println(vgo.ImportPaths([]string{"ALL"}))
+			pkgs = vgo.ImportPaths([]string{"ALL"})
 		} else {
 			vgo.LoadBuildList()
 		}
 		if *modSync {
 			// ImportPaths(ALL) already added missing modules.
 			// Remove unused modules.
-			panic("TODO sync unused")
+			used := map[module.Version]bool{vgo.Target: true}
+			for _, pkg := range pkgs {
+				used[vgo.PackageModule(pkg)] = true
+			}
+
+			inGoMod := make(map[string]bool)
+			for _, r := range vgo.ModFile().Require {
+				inGoMod[r.Mod.Path] = true
+			}
+
+			var keep []module.Version
+			for _, m := range vgo.BuildList() {
+				if used[m] {
+					keep = append(keep, m)
+				} else if *modV && inGoMod[m.Path] {
+					fmt.Fprintf(os.Stderr, "unused %s\n", m.Path)
+				}
+			}
+			vgo.SetBuildList(keep)
 		}
 		vgo.WriteGoMod()
 		if *modVendor {
diff --git a/vendor/cmd/go/internal/vgo/load.go b/vendor/cmd/go/internal/vgo/load.go
index 30349cc..25cb0dd 100644
--- a/vendor/cmd/go/internal/vgo/load.go
+++ b/vendor/cmd/go/internal/vgo/load.go
@@ -85,6 +85,12 @@
 	return buildList
 }
 
+// SetBuildList sets the module build list.
+// The caller is responsible for ensuring that the list is valid.
+func SetBuildList(list []module.Version) {
+	buildList = list
+}
+
 // ImportMap returns the actual package import path
 // for an import path found in source code.
 // If the given import path does not appear in the source code
diff --git a/vendor/cmd/go/vgo_test.go b/vendor/cmd/go/vgo_test.go
index b659c28..f140dc8 100644
--- a/vendor/cmd/go/vgo_test.go
+++ b/vendor/cmd/go/vgo_test.go
@@ -363,6 +363,82 @@
 	tg.grepStderr("tcp.*nonexistent.rsc.io", "expected error for nonexistent.rsc.io")
 }
 
+func TestVgoSync(t *testing.T) {
+	tg := testgo(t)
+	defer tg.cleanup()
+	tg.makeTempdir()
+
+	write := func(name, text string) {
+		name = tg.path(name)
+		dir := filepath.Dir(name)
+		tg.must(os.MkdirAll(dir, 0777))
+		tg.must(ioutil.WriteFile(name, []byte(text), 0666))
+	}
+
+	write("m/go.mod", `
+module m
+
+require (
+	x.1 v1.0.0
+	y.1 v1.0.0
+	w.1 v1.2.0
+)
+
+replace x.1 v1.0.0 => ../x
+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
+`)
+	write("m/m.go", `
+package m
+
+import _ "x.1"
+import _ "z.1/sub"
+`)
+
+	write("w/go.mod", `
+module w
+`)
+	write("w/w.go", `
+package w
+`)
+
+	write("x/go.mod", `
+module x
+require w.1 v1.1.0
+require z.1 v1.1.0
+`)
+	write("x/x.go", `
+package x
+
+import _ "w.1"
+`)
+
+	write("y/go.mod", `
+module y
+require z.1 v1.2.0
+`)
+
+	write("z/go.mod", `
+module z
+`)
+	write("z/sub/sub.go", `
+package sub
+`)
+
+	tg.cd(tg.path("m"))
+	tg.run("-vgo", "mod", "-sync", "-v")
+	tg.grepStderr(`^unused y.1`, "need y.1 unused")
+	tg.grepStderrNot(`^unused [^y]`, "only y.1 should be unused")
+
+	tg.run("-vgo", "list", "-m")
+	tg.grepStdoutNot(`^y.1`, "y should be gone")
+	tg.grepStdout(`^w.1\s+v1.2.0`, "need w.1 to stay at v1.2.0")
+	tg.grepStdout(`^z.1\s+v1.2.0`, "need z.1 to stay at v1.2.0 even though y is gone")
+}
+
 func TestVgoVendor(t *testing.T) {
 	tg := testgo(t)
 	defer tg.cleanup()