diff --git a/src/cmd/go/go_test.go b/src/cmd/go/go_test.go index 450925f727c..96d67e1c4cd 100644 --- a/src/cmd/go/go_test.go +++ b/src/cmd/go/go_test.go @@ -33,6 +33,8 @@ import ( "cmd/go/internal/cfg" "cmd/go/internal/robustio" "cmd/go/internal/search" + "cmd/go/internal/vcs" + "cmd/go/internal/vcweb/vcstest" "cmd/go/internal/work" "cmd/internal/sys" @@ -129,6 +131,12 @@ func TestMain(m *testing.M) { return fmt.Errorf("%stestgo must not write to GOROOT (installing to %s)", callerPos, filepath.Join("GOROOT", rel)) } } + + if vcsTest := os.Getenv("TESTGO_VCSTEST_URL"); vcsTest != "" { + vcs.VCSTestRepoURL = vcsTest + vcs.VCSTestHosts = vcstest.Hosts + } + cmdgo.Main() os.Exit(0) } diff --git a/src/cmd/go/internal/get/get.go b/src/cmd/go/internal/get/get.go index 02289bf7f46..1c1f10354ba 100644 --- a/src/cmd/go/internal/get/get.go +++ b/src/cmd/go/internal/get/get.go @@ -495,21 +495,21 @@ func downloadPackage(p *load.Package) error { vcsCmd, repo, rootPath = rr.VCS, rr.Repo, rr.Root } if !blindRepo && !vcsCmd.IsSecure(repo) && security != web.Insecure { - return fmt.Errorf("cannot download, %v uses insecure protocol", repo) + return fmt.Errorf("cannot download: %v uses insecure protocol", repo) } if p.Internal.Build.SrcRoot == "" { // Package not found. Put in first directory of $GOPATH. list := filepath.SplitList(cfg.BuildContext.GOPATH) if len(list) == 0 { - return fmt.Errorf("cannot download, $GOPATH not set. For more details see: 'go help gopath'") + return fmt.Errorf("cannot download: $GOPATH not set. For more details see: 'go help gopath'") } // Guard against people setting GOPATH=$GOROOT. if filepath.Clean(list[0]) == filepath.Clean(cfg.GOROOT) { - return fmt.Errorf("cannot download, $GOPATH must not be set to $GOROOT. For more details see: 'go help gopath'") + return fmt.Errorf("cannot download: $GOPATH must not be set to $GOROOT. For more details see: 'go help gopath'") } if _, err := os.Stat(filepath.Join(list[0], "src/cmd/go/alldocs.go")); err == nil { - return fmt.Errorf("cannot download, %s is a GOROOT, not a GOPATH. For more details see: 'go help gopath'", list[0]) + return fmt.Errorf("cannot download: %s is a GOROOT, not a GOPATH. For more details see: 'go help gopath'", list[0]) } p.Internal.Build.Root = list[0] p.Internal.Build.SrcRoot = filepath.Join(list[0], "src") diff --git a/src/cmd/go/internal/modfetch/codehost/git_test.go b/src/cmd/go/internal/modfetch/codehost/git_test.go index 6a4212fc5ae..ec95097d04a 100644 --- a/src/cmd/go/internal/modfetch/codehost/git_test.go +++ b/src/cmd/go/internal/modfetch/codehost/git_test.go @@ -7,6 +7,8 @@ package codehost import ( "archive/zip" "bytes" + "cmd/go/internal/cfg" + "cmd/go/internal/vcweb/vcstest" "flag" "internal/testenv" "io" @@ -26,17 +28,18 @@ func TestMain(m *testing.M) { // needed for initializing the test environment variables as testing.Short // and HasExternalNetwork flag.Parse() - os.Exit(testMain(m)) + if err := testMain(m); err != nil { + log.Fatal(err) + } } -const ( - gitrepo1 = "https://vcs-test.golang.org/git/gitrepo1" - hgrepo1 = "https://vcs-test.golang.org/hg/hgrepo1" -) +var gitrepo1, hgrepo1 string -var altRepos = []string{ - "localGitRepo", - hgrepo1, +var altRepos = func() []string { + return []string{ + "localGitRepo", + hgrepo1, + } } // TODO: Convert gitrepo1 to svn, bzr, fossil and add tests. @@ -45,14 +48,38 @@ var altRepos = []string{ // localGitRepo is like gitrepo1 but allows archive access. var localGitRepo, localGitURL string -func testMain(m *testing.M) int { +func testMain(m *testing.M) (err error) { + cfg.BuildX = true + + srv, err := vcstest.NewServer() + if err != nil { + return err + } + defer func() { + if closeErr := srv.Close(); err == nil { + err = closeErr + } + }() + + gitrepo1 = srv.HTTP.URL + "/git/gitrepo1" + hgrepo1 = srv.HTTP.URL + "/hg/hgrepo1" + dir, err := os.MkdirTemp("", "gitrepo-test-") if err != nil { - log.Fatal(err) + return err } - defer os.RemoveAll(dir) + defer func() { + if rmErr := os.RemoveAll(dir); err == nil { + err = rmErr + } + }() - if testenv.HasExternalNetwork() && testenv.HasExec() { + // Redirect the module cache to a fresh directory to avoid crosstalk, and make + // it read/write so that the test can still clean it up easily when done. + cfg.GOMODCACHE = filepath.Join(dir, "modcache") + cfg.ModCacheRW = true + + if !testing.Short() && testenv.HasExec() { if _, err := exec.LookPath("git"); err == nil { // Clone gitrepo1 into a local directory. // If we use a file:// URL to access the local directory, @@ -60,10 +87,10 @@ func testMain(m *testing.M) int { // which will let us test remote git archive invocations. localGitRepo = filepath.Join(dir, "gitrepo2") if _, err := Run("", "git", "clone", "--mirror", gitrepo1, localGitRepo); err != nil { - log.Fatal(err) + return err } if _, err := Run(localGitRepo, "git", "config", "daemon.uploadarch", "true"); err != nil { - log.Fatal(err) + return err } // Convert absolute path to file URL. LocalGitRepo will not accept @@ -77,7 +104,8 @@ func testMain(m *testing.M) int { } } - return m.Run() + m.Run() + return nil } func testRepo(t *testing.T, remote string) (Repo, error) { @@ -85,49 +113,31 @@ func testRepo(t *testing.T, remote string) (Repo, error) { testenv.MustHaveExecPath(t, "git") return LocalGitRepo(localGitURL) } - vcs := "git" + vcsName := "git" for _, k := range []string{"hg"} { if strings.Contains(remote, "/"+k+"/") { - vcs = k + vcsName = k } } - testenv.MustHaveExecPath(t, vcs) - return NewRepo(vcs, remote) -} - -var tagsTests = []struct { - repo string - prefix string - tags []Tag -}{ - {gitrepo1, "xxx", []Tag{}}, - {gitrepo1, "", []Tag{ - {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"}, - {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"}, - {"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, - {"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"}, - {"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, - }}, - {gitrepo1, "v", []Tag{ - {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"}, - {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"}, - {"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, - {"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"}, - {"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, - }}, - {gitrepo1, "v1", []Tag{ - {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"}, - {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"}, - }}, - {gitrepo1, "2", []Tag{}}, + testenv.MustHaveExecPath(t, vcsName) + return NewRepo(vcsName, remote) } func TestTags(t *testing.T) { testenv.MustHaveExternalNetwork(t) testenv.MustHaveExec(t) + t.Parallel() + + type tagsTest struct { + repo string + prefix string + tags []Tag + } + + runTest := func(tt tagsTest) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() - for _, tt := range tagsTests { - f := func(t *testing.T) { r, err := testRepo(t, tt.repo) if err != nil { t.Fatal(err) @@ -140,7 +150,31 @@ func TestTags(t *testing.T) { t.Errorf("Tags(%q): incorrect tags\nhave %v\nwant %v", tt.prefix, tags, tt.tags) } } - t.Run(path.Base(tt.repo)+"/"+tt.prefix, f) + } + + for _, tt := range []tagsTest{ + {gitrepo1, "xxx", []Tag{}}, + {gitrepo1, "", []Tag{ + {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"}, + {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"}, + {"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, + {"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"}, + {"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, + }}, + {gitrepo1, "v", []Tag{ + {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"}, + {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"}, + {"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, + {"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"}, + {"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"}, + }}, + {gitrepo1, "v1", []Tag{ + {"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"}, + {"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"}, + }}, + {gitrepo1, "2", []Tag{}}, + } { + t.Run(path.Base(tt.repo)+"/"+tt.prefix, runTest(tt)) if tt.repo == gitrepo1 { // Clear hashes. clearTags := []Tag{} @@ -148,60 +182,31 @@ func TestTags(t *testing.T) { clearTags = append(clearTags, Tag{tag.Name, ""}) } tags := tt.tags - for _, tt.repo = range altRepos { + for _, tt.repo = range altRepos() { if strings.Contains(tt.repo, "Git") { tt.tags = tags } else { tt.tags = clearTags } - t.Run(path.Base(tt.repo)+"/"+tt.prefix, f) + t.Run(path.Base(tt.repo)+"/"+tt.prefix, runTest(tt)) } } } } -var latestTests = []struct { - repo string - info *RevInfo -}{ - { - gitrepo1, - &RevInfo{ - Origin: &Origin{ - VCS: "git", - URL: "https://vcs-test.golang.org/git/gitrepo1", - Ref: "HEAD", - Hash: "ede458df7cd0fdca520df19a33158086a8a68e81", - }, - Name: "ede458df7cd0fdca520df19a33158086a8a68e81", - Short: "ede458df7cd0", - Version: "ede458df7cd0fdca520df19a33158086a8a68e81", - Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), - Tags: []string{"v1.2.3", "v1.2.4-annotated"}, - }, - }, - { - hgrepo1, - &RevInfo{ - Origin: &Origin{ - VCS: "hg", - URL: "https://vcs-test.golang.org/hg/hgrepo1", - Hash: "18518c07eb8ed5c80221e997e518cccaa8c0c287", - }, - Name: "18518c07eb8ed5c80221e997e518cccaa8c0c287", - Short: "18518c07eb8e", - Version: "18518c07eb8ed5c80221e997e518cccaa8c0c287", - Time: time.Date(2018, 6, 27, 16, 16, 30, 0, time.UTC), - }, - }, -} - func TestLatest(t *testing.T) { testenv.MustHaveExternalNetwork(t) testenv.MustHaveExec(t) + t.Parallel() + + type latestTest struct { + repo string + info *RevInfo + } + runTest := func(tt latestTest) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() - for _, tt := range latestTests { - f := func(t *testing.T) { r, err := testRepo(t, tt.repo) if err != nil { t.Fatal(err) @@ -214,7 +219,41 @@ func TestLatest(t *testing.T) { t.Errorf("Latest: incorrect info\nhave %+v (origin %+v)\nwant %+v (origin %+v)", info, info.Origin, tt.info, tt.info.Origin) } } - t.Run(path.Base(tt.repo), f) + } + + for _, tt := range []latestTest{ + { + gitrepo1, + &RevInfo{ + Origin: &Origin{ + VCS: "git", + URL: gitrepo1, + Ref: "HEAD", + Hash: "ede458df7cd0fdca520df19a33158086a8a68e81", + }, + Name: "ede458df7cd0fdca520df19a33158086a8a68e81", + Short: "ede458df7cd0", + Version: "ede458df7cd0fdca520df19a33158086a8a68e81", + Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), + Tags: []string{"v1.2.3", "v1.2.4-annotated"}, + }, + }, + { + hgrepo1, + &RevInfo{ + Origin: &Origin{ + VCS: "hg", + URL: hgrepo1, + Hash: "18518c07eb8ed5c80221e997e518cccaa8c0c287", + }, + Name: "18518c07eb8ed5c80221e997e518cccaa8c0c287", + Short: "18518c07eb8e", + Version: "18518c07eb8ed5c80221e997e518cccaa8c0c287", + Time: time.Date(2018, 6, 27, 16, 16, 30, 0, time.UTC), + }, + }, + } { + t.Run(path.Base(tt.repo), runTest(tt)) if tt.repo == gitrepo1 { tt.repo = "localGitRepo" info := *tt.info @@ -222,44 +261,27 @@ func TestLatest(t *testing.T) { o := *info.Origin info.Origin = &o o.URL = localGitURL - t.Run(path.Base(tt.repo), f) + t.Run(path.Base(tt.repo), runTest(tt)) } } } -var readFileTests = []struct { - repo string - rev string - file string - err string - data string -}{ - { - repo: gitrepo1, - rev: "latest", - file: "README", - data: "", - }, - { - repo: gitrepo1, - rev: "v2", - file: "another.txt", - data: "another\n", - }, - { - repo: gitrepo1, - rev: "v2.3.4", - file: "another.txt", - err: fs.ErrNotExist.Error(), - }, -} - func TestReadFile(t *testing.T) { testenv.MustHaveExternalNetwork(t) testenv.MustHaveExec(t) + t.Parallel() + + type readFileTest struct { + repo string + rev string + file string + err string + data string + } + runTest := func(tt readFileTest) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() - for _, tt := range readFileTests { - f := func(t *testing.T) { r, err := testRepo(t, tt.repo) if err != nil { t.Fatal(err) @@ -284,162 +306,37 @@ func TestReadFile(t *testing.T) { t.Errorf("ReadFile: incorrect data\nhave %q\nwant %q", data, tt.data) } } - t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, f) + } + + for _, tt := range []readFileTest{ + { + repo: gitrepo1, + rev: "latest", + file: "README", + data: "", + }, + { + repo: gitrepo1, + rev: "v2", + file: "another.txt", + data: "another\n", + }, + { + repo: gitrepo1, + rev: "v2.3.4", + file: "another.txt", + err: fs.ErrNotExist.Error(), + }, + } { + t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, runTest(tt)) if tt.repo == gitrepo1 { - for _, tt.repo = range altRepos { - t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, f) + for _, tt.repo = range altRepos() { + t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, runTest(tt)) } } } } -var readZipTests = []struct { - repo string - rev string - subdir string - err string - files map[string]uint64 -}{ - { - repo: gitrepo1, - rev: "v2.3.4", - subdir: "", - files: map[string]uint64{ - "prefix/": 0, - "prefix/README": 0, - "prefix/v2": 3, - }, - }, - { - repo: hgrepo1, - rev: "v2.3.4", - subdir: "", - files: map[string]uint64{ - "prefix/.hg_archival.txt": ^uint64(0), - "prefix/README": 0, - "prefix/v2": 3, - }, - }, - - { - repo: gitrepo1, - rev: "v2", - subdir: "", - files: map[string]uint64{ - "prefix/": 0, - "prefix/README": 0, - "prefix/v2": 3, - "prefix/another.txt": 8, - "prefix/foo.txt": 13, - }, - }, - { - repo: hgrepo1, - rev: "v2", - subdir: "", - files: map[string]uint64{ - "prefix/.hg_archival.txt": ^uint64(0), - "prefix/README": 0, - "prefix/v2": 3, - "prefix/another.txt": 8, - "prefix/foo.txt": 13, - }, - }, - - { - repo: gitrepo1, - rev: "v3", - subdir: "", - files: map[string]uint64{ - "prefix/": 0, - "prefix/v3/": 0, - "prefix/v3/sub/": 0, - "prefix/v3/sub/dir/": 0, - "prefix/v3/sub/dir/file.txt": 16, - "prefix/README": 0, - }, - }, - { - repo: hgrepo1, - rev: "v3", - subdir: "", - files: map[string]uint64{ - "prefix/.hg_archival.txt": ^uint64(0), - "prefix/.hgtags": 405, - "prefix/v3/sub/dir/file.txt": 16, - "prefix/README": 0, - }, - }, - - { - repo: gitrepo1, - rev: "v3", - subdir: "v3/sub/dir", - files: map[string]uint64{ - "prefix/": 0, - "prefix/v3/": 0, - "prefix/v3/sub/": 0, - "prefix/v3/sub/dir/": 0, - "prefix/v3/sub/dir/file.txt": 16, - }, - }, - { - repo: hgrepo1, - rev: "v3", - subdir: "v3/sub/dir", - files: map[string]uint64{ - "prefix/v3/sub/dir/file.txt": 16, - }, - }, - - { - repo: gitrepo1, - rev: "v3", - subdir: "v3/sub", - files: map[string]uint64{ - "prefix/": 0, - "prefix/v3/": 0, - "prefix/v3/sub/": 0, - "prefix/v3/sub/dir/": 0, - "prefix/v3/sub/dir/file.txt": 16, - }, - }, - { - repo: hgrepo1, - rev: "v3", - subdir: "v3/sub", - files: map[string]uint64{ - "prefix/v3/sub/dir/file.txt": 16, - }, - }, - - { - repo: gitrepo1, - rev: "aaaaaaaaab", - subdir: "", - err: "unknown revision", - }, - { - repo: hgrepo1, - rev: "aaaaaaaaab", - subdir: "", - err: "unknown revision", - }, - - { - repo: "https://github.com/rsc/vgotest1", - rev: "submod/v1.0.4", - subdir: "submod", - files: map[string]uint64{ - "prefix/": 0, - "prefix/submod/": 0, - "prefix/submod/go.mod": 53, - "prefix/submod/pkg/": 0, - "prefix/submod/pkg/p.go": 31, - }, - }, -} - type zipFile struct { name string size int64 @@ -448,9 +345,19 @@ type zipFile struct { func TestReadZip(t *testing.T) { testenv.MustHaveExternalNetwork(t) testenv.MustHaveExec(t) + t.Parallel() + + type readZipTest struct { + repo string + rev string + subdir string + err string + files map[string]uint64 + } + runTest := func(tt readZipTest) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() - for _, tt := range readZipTests { - f := func(t *testing.T) { r, err := testRepo(t, tt.repo) if err != nil { t.Fatal(err) @@ -498,10 +405,152 @@ func TestReadZip(t *testing.T) { } } } - t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, f) + } + + for _, tt := range []readZipTest{ + { + repo: gitrepo1, + rev: "v2.3.4", + subdir: "", + files: map[string]uint64{ + "prefix/": 0, + "prefix/README": 0, + "prefix/v2": 3, + }, + }, + { + repo: hgrepo1, + rev: "v2.3.4", + subdir: "", + files: map[string]uint64{ + "prefix/.hg_archival.txt": ^uint64(0), + "prefix/README": 0, + "prefix/v2": 3, + }, + }, + + { + repo: gitrepo1, + rev: "v2", + subdir: "", + files: map[string]uint64{ + "prefix/": 0, + "prefix/README": 0, + "prefix/v2": 3, + "prefix/another.txt": 8, + "prefix/foo.txt": 13, + }, + }, + { + repo: hgrepo1, + rev: "v2", + subdir: "", + files: map[string]uint64{ + "prefix/.hg_archival.txt": ^uint64(0), + "prefix/README": 0, + "prefix/v2": 3, + "prefix/another.txt": 8, + "prefix/foo.txt": 13, + }, + }, + + { + repo: gitrepo1, + rev: "v3", + subdir: "", + files: map[string]uint64{ + "prefix/": 0, + "prefix/v3/": 0, + "prefix/v3/sub/": 0, + "prefix/v3/sub/dir/": 0, + "prefix/v3/sub/dir/file.txt": 16, + "prefix/README": 0, + }, + }, + { + repo: hgrepo1, + rev: "v3", + subdir: "", + files: map[string]uint64{ + "prefix/.hg_archival.txt": ^uint64(0), + "prefix/.hgtags": 405, + "prefix/v3/sub/dir/file.txt": 16, + "prefix/README": 0, + }, + }, + + { + repo: gitrepo1, + rev: "v3", + subdir: "v3/sub/dir", + files: map[string]uint64{ + "prefix/": 0, + "prefix/v3/": 0, + "prefix/v3/sub/": 0, + "prefix/v3/sub/dir/": 0, + "prefix/v3/sub/dir/file.txt": 16, + }, + }, + { + repo: hgrepo1, + rev: "v3", + subdir: "v3/sub/dir", + files: map[string]uint64{ + "prefix/v3/sub/dir/file.txt": 16, + }, + }, + + { + repo: gitrepo1, + rev: "v3", + subdir: "v3/sub", + files: map[string]uint64{ + "prefix/": 0, + "prefix/v3/": 0, + "prefix/v3/sub/": 0, + "prefix/v3/sub/dir/": 0, + "prefix/v3/sub/dir/file.txt": 16, + }, + }, + { + repo: hgrepo1, + rev: "v3", + subdir: "v3/sub", + files: map[string]uint64{ + "prefix/v3/sub/dir/file.txt": 16, + }, + }, + + { + repo: gitrepo1, + rev: "aaaaaaaaab", + subdir: "", + err: "unknown revision", + }, + { + repo: hgrepo1, + rev: "aaaaaaaaab", + subdir: "", + err: "unknown revision", + }, + + { + repo: "https://github.com/rsc/vgotest1", + rev: "submod/v1.0.4", + subdir: "submod", + files: map[string]uint64{ + "prefix/": 0, + "prefix/submod/": 0, + "prefix/submod/go.mod": 53, + "prefix/submod/pkg/": 0, + "prefix/submod/pkg/p.go": 31, + }, + }, + } { + t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, runTest(tt)) if tt.repo == gitrepo1 { tt.repo = "localGitRepo" - t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, f) + t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, runTest(tt)) } } } @@ -514,112 +563,21 @@ var hgmap = map[string]string{ "97f6aa59c81c623494825b43d39e445566e429a4": "c0cbbfb24c7c3c50c35c7b88e7db777da4ff625d", } -var statTests = []struct { - repo string - rev string - err string - info *RevInfo -}{ - { - repo: gitrepo1, - rev: "HEAD", - info: &RevInfo{ - Name: "ede458df7cd0fdca520df19a33158086a8a68e81", - Short: "ede458df7cd0", - Version: "ede458df7cd0fdca520df19a33158086a8a68e81", - Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), - Tags: []string{"v1.2.3", "v1.2.4-annotated"}, - }, - }, - { - repo: gitrepo1, - rev: "v2", // branch - info: &RevInfo{ - Name: "9d02800338b8a55be062c838d1f02e0c5780b9eb", - Short: "9d02800338b8", - Version: "9d02800338b8a55be062c838d1f02e0c5780b9eb", - Time: time.Date(2018, 4, 17, 20, 00, 32, 0, time.UTC), - Tags: []string{"v2.0.2"}, - }, - }, - { - repo: gitrepo1, - rev: "v2.3.4", // badly-named branch (semver should be a tag) - info: &RevInfo{ - Name: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f", - Short: "76a00fb249b7", - Version: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f", - Time: time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC), - Tags: []string{"v2.0.1", "v2.3"}, - }, - }, - { - repo: gitrepo1, - rev: "v2.3", // badly-named tag (we only respect full semver v2.3.0) - info: &RevInfo{ - Name: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f", - Short: "76a00fb249b7", - Version: "v2.3", - Time: time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC), - Tags: []string{"v2.0.1", "v2.3"}, - }, - }, - { - repo: gitrepo1, - rev: "v1.2.3", // tag - info: &RevInfo{ - Name: "ede458df7cd0fdca520df19a33158086a8a68e81", - Short: "ede458df7cd0", - Version: "v1.2.3", - Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), - Tags: []string{"v1.2.3", "v1.2.4-annotated"}, - }, - }, - { - repo: gitrepo1, - rev: "ede458df", // hash prefix in refs - info: &RevInfo{ - Name: "ede458df7cd0fdca520df19a33158086a8a68e81", - Short: "ede458df7cd0", - Version: "ede458df7cd0fdca520df19a33158086a8a68e81", - Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), - Tags: []string{"v1.2.3", "v1.2.4-annotated"}, - }, - }, - { - repo: gitrepo1, - rev: "97f6aa59", // hash prefix not in refs - info: &RevInfo{ - Name: "97f6aa59c81c623494825b43d39e445566e429a4", - Short: "97f6aa59c81c", - Version: "97f6aa59c81c623494825b43d39e445566e429a4", - Time: time.Date(2018, 4, 17, 20, 0, 19, 0, time.UTC), - }, - }, - { - repo: gitrepo1, - rev: "v1.2.4-annotated", // annotated tag uses unwrapped commit hash - info: &RevInfo{ - Name: "ede458df7cd0fdca520df19a33158086a8a68e81", - Short: "ede458df7cd0", - Version: "v1.2.4-annotated", - Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), - Tags: []string{"v1.2.3", "v1.2.4-annotated"}, - }, - }, - { - repo: gitrepo1, - rev: "aaaaaaaaab", - err: "unknown revision", - }, -} - func TestStat(t *testing.T) { testenv.MustHaveExternalNetwork(t) testenv.MustHaveExec(t) + t.Parallel() + + type statTest struct { + repo string + rev string + err string + info *RevInfo + } + runTest := func(tt statTest) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() - for _, tt := range statTests { - f := func(t *testing.T) { r, err := testRepo(t, tt.repo) if err != nil { t.Fatal(err) @@ -642,9 +600,105 @@ func TestStat(t *testing.T) { t.Errorf("Stat: incorrect info\nhave %+v\nwant %+v", *info, *tt.info) } } - t.Run(path.Base(tt.repo)+"/"+tt.rev, f) + } + + for _, tt := range []statTest{ + { + repo: gitrepo1, + rev: "HEAD", + info: &RevInfo{ + Name: "ede458df7cd0fdca520df19a33158086a8a68e81", + Short: "ede458df7cd0", + Version: "ede458df7cd0fdca520df19a33158086a8a68e81", + Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), + Tags: []string{"v1.2.3", "v1.2.4-annotated"}, + }, + }, + { + repo: gitrepo1, + rev: "v2", // branch + info: &RevInfo{ + Name: "9d02800338b8a55be062c838d1f02e0c5780b9eb", + Short: "9d02800338b8", + Version: "9d02800338b8a55be062c838d1f02e0c5780b9eb", + Time: time.Date(2018, 4, 17, 20, 00, 32, 0, time.UTC), + Tags: []string{"v2.0.2"}, + }, + }, + { + repo: gitrepo1, + rev: "v2.3.4", // badly-named branch (semver should be a tag) + info: &RevInfo{ + Name: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f", + Short: "76a00fb249b7", + Version: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f", + Time: time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC), + Tags: []string{"v2.0.1", "v2.3"}, + }, + }, + { + repo: gitrepo1, + rev: "v2.3", // badly-named tag (we only respect full semver v2.3.0) + info: &RevInfo{ + Name: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f", + Short: "76a00fb249b7", + Version: "v2.3", + Time: time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC), + Tags: []string{"v2.0.1", "v2.3"}, + }, + }, + { + repo: gitrepo1, + rev: "v1.2.3", // tag + info: &RevInfo{ + Name: "ede458df7cd0fdca520df19a33158086a8a68e81", + Short: "ede458df7cd0", + Version: "v1.2.3", + Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), + Tags: []string{"v1.2.3", "v1.2.4-annotated"}, + }, + }, + { + repo: gitrepo1, + rev: "ede458df", // hash prefix in refs + info: &RevInfo{ + Name: "ede458df7cd0fdca520df19a33158086a8a68e81", + Short: "ede458df7cd0", + Version: "ede458df7cd0fdca520df19a33158086a8a68e81", + Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), + Tags: []string{"v1.2.3", "v1.2.4-annotated"}, + }, + }, + { + repo: gitrepo1, + rev: "97f6aa59", // hash prefix not in refs + info: &RevInfo{ + Name: "97f6aa59c81c623494825b43d39e445566e429a4", + Short: "97f6aa59c81c", + Version: "97f6aa59c81c623494825b43d39e445566e429a4", + Time: time.Date(2018, 4, 17, 20, 0, 19, 0, time.UTC), + }, + }, + { + repo: gitrepo1, + rev: "v1.2.4-annotated", // annotated tag uses unwrapped commit hash + info: &RevInfo{ + Name: "ede458df7cd0fdca520df19a33158086a8a68e81", + Short: "ede458df7cd0", + Version: "v1.2.4-annotated", + Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC), + Tags: []string{"v1.2.3", "v1.2.4-annotated"}, + }, + }, + { + repo: gitrepo1, + rev: "aaaaaaaaab", + err: "unknown revision", + }, + } { + t.Run(path.Base(tt.repo)+"/"+tt.rev, runTest(tt)) if tt.repo == gitrepo1 { - for _, tt.repo = range altRepos { + for _, tt.repo = range altRepos() { old := tt var m map[string]string if tt.repo == hgrepo1 { @@ -658,7 +712,7 @@ func TestStat(t *testing.T) { tt.info.Short = remap(tt.info.Short, m) } tt.rev = remap(tt.rev, m) - t.Run(path.Base(tt.repo)+"/"+tt.rev, f) + t.Run(path.Base(tt.repo)+"/"+tt.rev, runTest(tt)) tt = old } } diff --git a/src/cmd/go/internal/modfetch/coderepo_test.go b/src/cmd/go/internal/modfetch/coderepo_test.go index 3dd1b1cca6a..553946ba369 100644 --- a/src/cmd/go/internal/modfetch/coderepo_test.go +++ b/src/cmd/go/internal/modfetch/coderepo_test.go @@ -8,11 +8,13 @@ import ( "archive/zip" "crypto/sha256" "encoding/hex" + "flag" "hash" "internal/testenv" "io" "log" "os" + "path/filepath" "reflect" "strings" "testing" @@ -20,15 +22,20 @@ import ( "cmd/go/internal/cfg" "cmd/go/internal/modfetch/codehost" + "cmd/go/internal/vcweb/vcstest" "golang.org/x/mod/sumdb/dirhash" ) func TestMain(m *testing.M) { - os.Exit(testMain(m)) + flag.Parse() + if err := testMain(m); err != nil { + log.Fatal(err) + } } -func testMain(m *testing.M) int { +func testMain(m *testing.M) (err error) { + cfg.GOPROXY = "direct" // The sum database is populated using a released version of the go command, @@ -39,12 +46,31 @@ func testMain(m *testing.M) int { dir, err := os.MkdirTemp("", "gitrepo-test-") if err != nil { - log.Fatal(err) + return err } - defer os.RemoveAll(dir) + defer func() { + if rmErr := os.RemoveAll(dir); err == nil { + err = rmErr + } + }() - cfg.GOMODCACHE = dir - return m.Run() + cfg.GOMODCACHE = filepath.Join(dir, "modcache") + if err := os.Mkdir(cfg.GOMODCACHE, 0755); err != nil { + return err + } + + srv, err := vcstest.NewServer() + if err != nil { + return err + } + defer func() { + if closeErr := srv.Close(); err == nil { + err = closeErr + } + }() + + m.Run() + return nil } const ( diff --git a/src/cmd/go/internal/modload/query_test.go b/src/cmd/go/internal/modload/query_test.go index a3f2f84505a..fe9ae9f93f4 100644 --- a/src/cmd/go/internal/modload/query_test.go +++ b/src/cmd/go/internal/modload/query_test.go @@ -6,6 +6,7 @@ package modload import ( "context" + "flag" "internal/testenv" "log" "os" @@ -15,27 +16,47 @@ import ( "testing" "cmd/go/internal/cfg" + "cmd/go/internal/vcweb/vcstest" "golang.org/x/mod/module" ) func TestMain(m *testing.M) { - os.Exit(testMain(m)) + flag.Parse() + if err := testMain(m); err != nil { + log.Fatal(err) + } } -func testMain(m *testing.M) int { +func testMain(m *testing.M) (err error) { cfg.GOPROXY = "direct" + cfg.ModCacheRW = true + + srv, err := vcstest.NewServer() + if err != nil { + return err + } + defer func() { + if closeErr := srv.Close(); err == nil { + err = closeErr + } + }() dir, err := os.MkdirTemp("", "modload-test-") if err != nil { - log.Fatal(err) + return err } - defer os.RemoveAll(dir) + defer func() { + if rmErr := os.RemoveAll(dir); err == nil { + err = rmErr + } + }() os.Setenv("GOPATH", dir) cfg.BuildContext.GOPATH = dir cfg.GOMODCACHE = filepath.Join(dir, "pkg/mod") - return m.Run() + m.Run() + return nil } var ( @@ -55,42 +76,6 @@ var queryTests = []struct { vers string err string }{ - /* - git init - echo module vcs-test.golang.org/git/querytest.git >go.mod - git add go.mod - git commit -m v1 go.mod - git tag start - for i in v0.0.0-pre1 v0.0.0 v0.0.1 v0.0.2 v0.0.3 v0.1.0 v0.1.1 v0.1.2 v0.3.0 v1.0.0 v1.1.0 v1.9.0 v1.9.9 v1.9.10-pre1 v1.9.10-pre2+metadata unversioned; do - echo before $i >status - git add status - git commit -m "before $i" status - echo at $i >status - git commit -m "at $i" status - git tag $i - done - git tag favorite v0.0.3 - - git branch v2 start - git checkout v2 - echo module vcs-test.golang.org/git/querytest.git/v2 >go.mod - git commit -m v2 go.mod - for i in v2.0.0 v2.1.0 v2.2.0 v2.5.5 v2.6.0-pre1; do - echo before $i >status - git add status - git commit -m "before $i" status - echo at $i >status - git commit -m "at $i" status - git tag $i - done - git checkout v2.5.5 - echo after v2.5.5 >status - git commit -m 'after v2.5.5' status - git checkout master - zip -r ../querytest.zip - gsutil cp ../querytest.zip gs://vcs-test/git/querytest.zip - curl 'https://vcs-test.golang.org/git/querytest?go-get=1' - */ {path: queryRepo, query: " 2 { + return nil, script.ErrUsage + } + + sc, err := getScriptCtx(st) + if err != nil { + return nil, err + } + + if sc.handler != nil { + return nil, fmt.Errorf("server handler already set to %s", sc.handlerName) + } + + name := args[0] + h, ok := sc.server.vcsHandlers[name] + if !ok { + return nil, fmt.Errorf("unrecognized VCS %q", name) + } + sc.handlerName = name + if !h.Available() { + return nil, ServerNotInstalledError{name} + } + + dir := st.Getwd() + if len(args) >= 2 { + dir = st.Path(args[1]) + } + sc.handler, err = h.Handler(dir, st.Environ(), sc.server.logger) + return nil, err + }) +} + +func scriptUnquote() script.Cmd { + return script.Command( + script.CmdUsage{ + Summary: "unquote the argument as a Go string", + Args: "string", + }, + func(st *script.State, args ...string) (script.WaitFunc, error) { + if len(args) != 1 { + return nil, script.ErrUsage + } + + s, err := strconv.Unquote(`"` + args[0] + `"`) + if err != nil { + return nil, err + } + + wait := func(*script.State) (stdout, stderr string, err error) { + return s, "", nil + } + return wait, nil + }) +} diff --git a/src/cmd/go/internal/vcweb/vcstest/vcstest.go b/src/cmd/go/internal/vcweb/vcstest/vcstest.go new file mode 100644 index 00000000000..5402aad3972 --- /dev/null +++ b/src/cmd/go/internal/vcweb/vcstest/vcstest.go @@ -0,0 +1,83 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package vcstest serves the repository scripts in cmd/go/testdata/vcstest +// using the [vcweb] script engine. +package vcstest + +import ( + "cmd/go/internal/vcs" + "cmd/go/internal/vcweb" + "fmt" + "internal/testenv" + "io" + "log" + "net/http/httptest" + "os" + "path/filepath" + "testing" +) + +var Hosts = []string{ + "vcs-test.golang.org", +} + +type Server struct { + workDir string + HTTP *httptest.Server +} + +// NewServer returns a new test-local vcweb server that serves VCS requests +// for modules with paths that begin with "vcs-test.golang.org" using the +// scripts in cmd/go/testdata/vcstest. +func NewServer() (srv *Server, err error) { + if vcs.VCSTestRepoURL != "" { + panic("vcs URL hooks already set") + } + + scriptDir := filepath.Join(testenv.GOROOT(nil), "src/cmd/go/testdata/vcstest") + + workDir, err := os.MkdirTemp("", "vcstest") + if err != nil { + return nil, err + } + defer func() { + if err != nil { + os.RemoveAll(workDir) + } + }() + + logger := log.Default() + if !testing.Verbose() { + logger = log.New(io.Discard, "", log.LstdFlags) + } + handler, err := vcweb.NewServer(scriptDir, workDir, logger) + if err != nil { + return nil, err + } + + srvHTTP := httptest.NewServer(handler) + + srv = &Server{ + workDir: workDir, + HTTP: srvHTTP, + } + vcs.VCSTestRepoURL = srv.HTTP.URL + vcs.VCSTestHosts = Hosts + + fmt.Fprintln(os.Stderr, "vcs-test.golang.org rerouted to "+srv.HTTP.URL) + + return srv, nil +} + +func (srv *Server) Close() error { + if vcs.VCSTestRepoURL != srv.HTTP.URL { + panic("vcs URL hooks modified before Close") + } + vcs.VCSTestRepoURL = "" + vcs.VCSTestHosts = nil + + srv.HTTP.Close() + return os.RemoveAll(srv.workDir) +} diff --git a/src/cmd/go/internal/vcweb/vcstest/vcstest_test.go b/src/cmd/go/internal/vcweb/vcstest/vcstest_test.go new file mode 100644 index 00000000000..d45782d807b --- /dev/null +++ b/src/cmd/go/internal/vcweb/vcstest/vcstest_test.go @@ -0,0 +1,151 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vcstest_test + +import ( + "cmd/go/internal/vcweb" + "errors" + "flag" + "fmt" + "io" + "io/fs" + "log" + "net" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +var ( + dir = flag.String("dir", "../../../testdata/vcstest", "directory containing scripts to serve") + host = flag.String("host", "localhost", "hostname on which to serve HTTP") + port = flag.Int("port", -1, "port on which to serve HTTP; if nonnegative, skips running tests") +) + +func TestMain(m *testing.M) { + flag.Parse() + + if *port >= 0 { + err := serveStandalone(*host, *port) + if err != nil { + log.Fatal(err) + } + os.Exit(0) + } + + m.Run() +} + +// serveStandalone serves the vcweb testdata in a standalone HTTP server. +func serveStandalone(host string, port int) (err error) { + scriptDir, err := filepath.Abs(*dir) + if err != nil { + return err + } + work, err := os.MkdirTemp("", "vcweb") + if err != nil { + return err + } + defer func() { + if rmErr := os.RemoveAll(work); err == nil { + err = rmErr + } + }() + + log.Printf("running scripts in %s", work) + + v, err := vcweb.NewServer(scriptDir, work, log.Default()) + if err != nil { + return err + } + + l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) + if err != nil { + return err + } + log.Printf("serving on http://%s:%d/", host, l.Addr().(*net.TCPAddr).Port) + + return http.Serve(l, v) +} + +// TestScripts verifies that the VCS setup scripts in cmd/go/testdata/vcstest +// run successfully. +func TestScripts(t *testing.T) { + scriptDir, err := filepath.Abs(*dir) + if err != nil { + t.Fatal(err) + } + s, err := vcweb.NewServer(scriptDir, t.TempDir(), log.Default()) + if err != nil { + t.Fatal(err) + } + srv := httptest.NewServer(s) + + t.Cleanup(func() { + // The subtests spawned by WalkDir run in parallel. When they complete, this + // Cleanup callback will run. At that point we fetch the root URL (which + // contains a status page), both to test that the root handler runs without + // crashing and to display a nice summary of the server's view of the test + // coverage. + resp, err := http.Get(srv.URL) + if err == nil { + var body []byte + body, err = io.ReadAll(resp.Body) + if err == nil && testing.Verbose() { + t.Logf("GET %s:\n%s", srv.URL, body) + } + resp.Body.Close() + } + if err != nil { + t.Error(err) + } + + srv.Close() + }) + + err = filepath.WalkDir(scriptDir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + + rel, err := filepath.Rel(scriptDir, path) + if err != nil { + return err + } + if rel == "README" { + return nil + } + + t.Run(filepath.ToSlash(rel), func(t *testing.T) { + t.Parallel() + + buf := new(strings.Builder) + logger := log.New(buf, "", log.LstdFlags) + // Load the script but don't try to serve the results: + // different VCS tools have different handler protocols, + // and the tests that actually use these repos will ensure + // that they are served correctly as a side effect anyway. + err := s.HandleScript(rel, logger, func(http.Handler) {}) + if buf.Len() > 0 { + t.Log(buf) + } + if err != nil { + if notInstalled := (vcweb.ServerNotInstalledError{}); errors.As(err, ¬Installed) || errors.Is(err, exec.ErrNotFound) { + t.Skip(err) + } + t.Error(err) + } + }) + return nil + }) + + if err != nil { + t.Error(err) + } +} diff --git a/src/cmd/go/internal/vcweb/vcweb.go b/src/cmd/go/internal/vcweb/vcweb.go new file mode 100644 index 00000000000..c9303ce2abd --- /dev/null +++ b/src/cmd/go/internal/vcweb/vcweb.go @@ -0,0 +1,407 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package vcweb serves version control repos for testing the go command. +// +// It is loosely derived from golang.org/x/build/vcs-test/vcweb, +// which ran as a service hosted at vcs-test.golang.org. +// +// When a repository URL is first requested, the vcweb [Server] dynamically +// regenerates the repository using a script interpreted by a [script.Engine]. +// The script produces the server's contents for a corresponding root URL and +// all subdirectories of that URL, which are then cached: subsequent requests +// for any URL generated by the script will serve the script's previous output +// until the script is modified. +// +// The script engine includes all of the engine's default commands and +// conditions, as well as commands for each supported VCS binary (bzr, fossil, +// git, hg, and svn), a "handle" command that informs the script which protocol +// or handler to use to serve the request, and utilities "at" (which sets +// environment variables for Git timestamps) and "unquote" (which unquotes its +// argument as if it were a Go string literal). +// +// The server's "/" endpoint provides a summary of the available scripts, +// and "/help" provides documentation for the script environment. +// +// To run a standalone server based on the vcweb engine, use: +// +// go test cmd/go/internal/vcweb/vcstest -v --port=0 +package vcweb + +import ( + "bufio" + "cmd/go/internal/script" + "context" + "crypto/sha256" + "errors" + "fmt" + "io" + "io/fs" + "log" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "runtime/debug" + "strings" + "sync" + "text/tabwriter" + "time" +) + +// A Server serves cached, dynamically-generated version control repositories. +type Server struct { + env []string + logger *log.Logger + + scriptDir string + workDir string + homeDir string // $workdir/home + engine *script.Engine + + scriptCache sync.Map // script path → *scriptResult + + vcsHandlers map[string]vcsHandler +} + +// A vcsHandler serves repositories over HTTP for a known version-control tool. +type vcsHandler interface { + Available() bool + Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) +} + +// A scriptResult describes the cached result of executing a vcweb script. +type scriptResult struct { + mu sync.RWMutex + + hash [sha256.Size]byte // hash of the script file, for cache invalidation + hashTime time.Time // timestamp at which the script was run, for diagnostics + + handler http.Handler // HTTP handler configured by the script + err error // error from executing the script, if any +} + +// NewServer returns a Server that generates and serves repositories in workDir +// using the scripts found in scriptDir and its subdirectories. +// +// A request for the path /foo/bar/baz will be handled by the first script along +// that path that exists: $scriptDir/foo.txt, $scriptDir/foo/bar.txt, or +// $scriptDir/foo/bar/baz.txt. +func NewServer(scriptDir, workDir string, logger *log.Logger) (*Server, error) { + if scriptDir == "" { + panic("vcweb.NewServer: scriptDir is required") + } + var err error + scriptDir, err = filepath.Abs(scriptDir) + if err != nil { + return nil, err + } + + if workDir == "" { + workDir, err = os.MkdirTemp("", "vcweb-*") + if err != nil { + return nil, err + } + logger.Printf("vcweb work directory: %s", workDir) + } else { + workDir, err = filepath.Abs(workDir) + if err != nil { + return nil, err + } + } + + homeDir := filepath.Join(workDir, "home") + if err := os.MkdirAll(homeDir, 0755); err != nil { + return nil, err + } + + env := scriptEnviron(homeDir) + + s := &Server{ + env: env, + logger: logger, + scriptDir: scriptDir, + workDir: workDir, + homeDir: homeDir, + engine: newScriptEngine(), + vcsHandlers: map[string]vcsHandler{ + "dir": new(dirHandler), + "bzr": new(bzrHandler), + "fossil": new(fossilHandler), + "git": new(gitHandler), + "hg": new(hgHandler), + }, + } + + if err := os.WriteFile(filepath.Join(s.homeDir, ".gitconfig"), []byte(gitConfig), 0644); err != nil { + return nil, err + } + gitConfigDir := filepath.Join(s.homeDir, ".config", "git") + if err := os.MkdirAll(gitConfigDir, 0755); err != nil { + return nil, err + } + if err := os.WriteFile(filepath.Join(gitConfigDir, "ignore"), []byte(""), 0644); err != nil { + return nil, err + } + + if err := os.WriteFile(filepath.Join(s.homeDir, ".hgrc"), []byte(hgrc), 0644); err != nil { + return nil, err + } + + return s, nil +} + +// gitConfig contains a ~/.gitconfg file that attempts to provide +// deterministic, platform-agnostic behavior for the 'git' command. +var gitConfig = ` +[user] + name = Go Gopher + email = gopher@golang.org +[init] + defaultBranch = main +[core] + eol = lf +[gui] + encoding = utf-8 +`[1:] + +// hgrc contains a ~/.hgrc file that attempts to provide +// deterministic, platform-agnostic behavior for the 'hg' command. +var hgrc = ` +[ui] +username=Go Gopher +[phases] +new-commit=public +[extensions] +convert= +`[1:] + +// ServeHTTP implements [http.Handler] for version-control repositories. +func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.logger.Printf("serving %s", req.URL) + + defer func() { + if v := recover(); v != nil { + debug.PrintStack() + s.logger.Fatal(v) + } + }() + + urlPath := req.URL.Path + if !strings.HasPrefix(urlPath, "/") { + urlPath = "/" + urlPath + } + clean := path.Clean(urlPath)[1:] + if clean == "" { + s.overview(w, req) + return + } + if clean == "help" { + s.help(w, req) + return + } + + // Locate the script that generates the requested path. + // We follow directories all the way to the end, then look for a ".txt" file + // matching the first component that doesn't exist. That guarantees + // uniqueness: if a path exists as a directory, then it cannot exist as a + // ".txt" script (because the search would ignore that file). + scriptPath := "." + for _, part := range strings.Split(clean, "/") { + scriptPath = filepath.Join(scriptPath, part) + dir := filepath.Join(s.scriptDir, scriptPath) + if _, err := os.Stat(dir); err != nil { + if !os.IsNotExist(err) { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // scriptPath does not exist as a directory, so it either is the script + // location or the script doesn't exist. + break + } + } + scriptPath += ".txt" + + err := s.HandleScript(scriptPath, s.logger, func(handler http.Handler) { + handler.ServeHTTP(w, req) + }) + if err != nil { + s.logger.Print(err) + if notFound := (ScriptNotFoundError{}); errors.As(err, ¬Found) { + http.NotFound(w, req) + } else if notInstalled := (ServerNotInstalledError{}); errors.As(err, ¬Installed) || errors.Is(err, exec.ErrNotFound) { + http.Error(w, err.Error(), http.StatusNotImplemented) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +// A ScriptNotFoundError indicates that the requested script file does not exist. +// (It typically wraps a "stat" error for the script file.) +type ScriptNotFoundError struct{ err error } + +func (e ScriptNotFoundError) Error() string { return e.err.Error() } +func (e ScriptNotFoundError) Unwrap() error { return e.err } + +// A ServerNotInstalledError indicates that the server binary required for the +// indicated VCS does not exist. +type ServerNotInstalledError struct{ name string } + +func (v ServerNotInstalledError) Error() string { + return fmt.Sprintf("server for %#q VCS is not installed", v.name) +} + +// HandleScript ensures that the script at scriptRelPath has been evaluated +// with its current contents. +// +// If the script completed successfully, HandleScript invokes f on the handler +// with the script's result still read-locked, and waits for it to return. (That +// ensures that cache invalidation does not race with an in-flight handler.) +// +// Otherwise, HandleScript returns the (cached) error from executing the script. +func (s *Server) HandleScript(scriptRelPath string, logger *log.Logger, f func(http.Handler)) error { + ri, ok := s.scriptCache.Load(scriptRelPath) + if !ok { + ri, _ = s.scriptCache.LoadOrStore(scriptRelPath, new(scriptResult)) + } + r := ri.(*scriptResult) + + relDir := strings.TrimSuffix(scriptRelPath, filepath.Ext(scriptRelPath)) + workDir := filepath.Join(s.workDir, relDir) + prefix := path.Join("/", filepath.ToSlash(relDir)) + + r.mu.RLock() + defer r.mu.RUnlock() + for { + // For efficiency, we cache the script's output (in the work directory) + // across invocations. However, to allow for rapid iteration, we hash the + // script's contents and regenerate its output if the contents change. + // + // That way, one can use 'go run main.go' in this directory to stand up a + // server and see the output of the test script in order to fine-tune it. + content, err := os.ReadFile(filepath.Join(s.scriptDir, scriptRelPath)) + if err != nil { + if !os.IsNotExist(err) { + return err + } + return ScriptNotFoundError{err} + } + + hash := sha256.Sum256(content) + if prevHash := r.hash; prevHash != hash { + // The script's hash has changed, so regenerate its output. + func() { + r.mu.RUnlock() + r.mu.Lock() + defer func() { + r.mu.Unlock() + r.mu.RLock() + }() + if r.hash != prevHash { + // The cached result changed while we were waiting on the lock. + // It may have been updated to our hash or something even newer, + // so don't overwrite it. + return + } + + r.hash = hash + r.hashTime = time.Now() + r.handler, r.err = nil, nil + + if err := os.RemoveAll(workDir); err != nil { + r.err = err + return + } + + // Note: we use context.Background here instead of req.Context() so that we + // don't cache a spurious error (and lose work) if the request is canceled + // while the script is still running. + scriptHandler, err := s.loadScript(context.Background(), logger, scriptRelPath, content, workDir) + if err != nil { + r.err = err + return + } + r.handler = http.StripPrefix(prefix, scriptHandler) + }() + } + + if r.hash != hash { + continue // Raced with an update from another handler; try again. + } + + if r.err != nil { + return r.err + } + f(r.handler) + return nil + } +} + +// overview serves an HTML summary of the status of the scripts in the server's +// script directory. +func (s *Server) overview(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "\n") + fmt.Fprintf(w, "vcweb\n
\n")
+	fmt.Fprintf(w, "vcweb\n\n")
+	fmt.Fprintf(w, "This server serves various version control repos for testing the go command.\n\n")
+	fmt.Fprintf(w, "For an overview of the script lanugage, see /help.\n\n")
+
+	fmt.Fprintf(w, "cache\n")
+
+	tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
+	err := filepath.WalkDir(s.scriptDir, func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		if filepath.Ext(path) != ".txt" {
+			return nil
+		}
+
+		rel, err := filepath.Rel(s.scriptDir, path)
+		if err != nil {
+			return err
+		}
+		hashTime := "(not loaded)"
+		status := ""
+		if ri, ok := s.scriptCache.Load(rel); ok {
+			r := ri.(*scriptResult)
+			if !r.hashTime.IsZero() {
+				hashTime = r.hashTime.Format(time.RFC3339)
+			}
+			if r.err == nil {
+				status = "ok"
+			} else {
+				status = r.err.Error()
+			}
+		}
+		fmt.Fprintf(tw, "%s\t%s\t%s\n", rel, hashTime, status)
+		return nil
+	})
+	tw.Flush()
+
+	if err != nil {
+		fmt.Fprintln(w, err)
+	}
+}
+
+// help serves a plain-text summary of the server's supported script language.
+func (s *Server) help(w http.ResponseWriter, req *http.Request) {
+	st, err := s.newState(req.Context(), s.workDir)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	scriptLog := new(strings.Builder)
+	err = s.engine.Execute(st, "help", bufio.NewReader(strings.NewReader("help")), scriptLog)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
+	io.WriteString(w, scriptLog.String())
+}
diff --git a/src/cmd/go/internal/vcweb/vcweb_test.go b/src/cmd/go/internal/vcweb/vcweb_test.go
new file mode 100644
index 00000000000..20b213786fe
--- /dev/null
+++ b/src/cmd/go/internal/vcweb/vcweb_test.go
@@ -0,0 +1,63 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcweb_test
+
+import (
+	"cmd/go/internal/vcweb"
+	"io"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+)
+
+func TestHelp(t *testing.T) {
+	s, err := vcweb.NewServer(os.DevNull, t.TempDir(), log.Default())
+	if err != nil {
+		t.Fatal(err)
+	}
+	srv := httptest.NewServer(s)
+	defer srv.Close()
+
+	resp, err := http.Get(srv.URL + "/help")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		t.Fatal(resp.Status)
+	}
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("%s", body)
+}
+
+func TestOverview(t *testing.T) {
+	s, err := vcweb.NewServer(os.DevNull, t.TempDir(), log.Default())
+	if err != nil {
+		t.Fatal(err)
+	}
+	srv := httptest.NewServer(s)
+	defer srv.Close()
+
+	resp, err := http.Get(srv.URL)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		t.Fatal(resp.Status)
+	}
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("%s", body)
+}
diff --git a/src/cmd/go/internal/web/api.go b/src/cmd/go/internal/web/api.go
index 1e2ba9c4197..7a6e0c310c9 100644
--- a/src/cmd/go/internal/web/api.go
+++ b/src/cmd/go/internal/web/api.go
@@ -238,3 +238,9 @@ func (b *errorDetailBuffer) Read(p []byte) (n int, err error) {
 
 	return n, err
 }
+
+// IsLocalHost reports whether the given URL refers to a local
+// (loopback) host, such as "localhost" or "127.0.0.1:8080".
+func IsLocalHost(u *url.URL) bool {
+	return isLocalHost(u)
+}
diff --git a/src/cmd/go/internal/web/bootstrap.go b/src/cmd/go/internal/web/bootstrap.go
index ab88e9e4781..6312169ef00 100644
--- a/src/cmd/go/internal/web/bootstrap.go
+++ b/src/cmd/go/internal/web/bootstrap.go
@@ -21,3 +21,5 @@ func get(security SecurityMode, url *urlpkg.URL) (*Response, error) {
 }
 
 func openBrowser(url string) bool { return false }
+
+func isLocalHost(u *urlpkg.URL) bool { return false }
diff --git a/src/cmd/go/internal/web/http.go b/src/cmd/go/internal/web/http.go
index a92326db01e..dfa124f869a 100644
--- a/src/cmd/go/internal/web/http.go
+++ b/src/cmd/go/internal/web/http.go
@@ -255,3 +255,20 @@ func getFile(u *urlpkg.URL) (*Response, error) {
 }
 
 func openBrowser(url string) bool { return browser.Open(url) }
+
+func isLocalHost(u *urlpkg.URL) bool {
+	// VCSTestRepoURL itself is secure, and it may redirect requests to other
+	// ports (such as a port serving the "svn" protocol) which should also be
+	// considered secure.
+	host, _, err := net.SplitHostPort(u.Host)
+	if err != nil {
+		host = u.Host
+	}
+	if host == "localhost" {
+		return true
+	}
+	if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
+		return true
+	}
+	return false
+}
diff --git a/src/cmd/go/script_test.go b/src/cmd/go/script_test.go
index f0fe6d04602..a2d2cae658b 100644
--- a/src/cmd/go/script_test.go
+++ b/src/cmd/go/script_test.go
@@ -29,6 +29,8 @@ import (
 	"cmd/go/internal/cfg"
 	"cmd/go/internal/script"
 	"cmd/go/internal/script/scripttest"
+	"cmd/go/internal/vcs"
+	"cmd/go/internal/vcweb/vcstest"
 )
 
 var testSum = flag.String("testsum", "", `may be tidy, listm, or listall. If set, TestScript generates a go.sum file at the beginning of each test and updates test files if they pass.`)
@@ -38,6 +40,16 @@ func TestScript(t *testing.T) {
 	testenv.MustHaveGoBuild(t)
 	testenv.SkipIfShortAndSlow(t)
 
+	srv, err := vcstest.NewServer()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := srv.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
 	StartProxy()
 
 	var (
@@ -187,6 +199,7 @@ func scriptEnv() ([]string, error) {
 		"GOROOT_FINAL=" + testGOROOT_FINAL, // causes spurious rebuilds and breaks the "stale" built-in if not propagated
 		"GOTRACEBACK=system",
 		"TESTGO_GOROOT=" + testGOROOT,
+		"TESTGO_VCSTEST_URL=" + vcs.VCSTestRepoURL,
 		"GOSUMDB=" + testSumDBVerifierKey,
 		"GONOPROXY=",
 		"GONOSUMDB=",
diff --git a/src/cmd/go/testdata/script/reuse_git.txt b/src/cmd/go/testdata/script/reuse_git.txt
index 8df47541be8..4f9e0dd17fd 100644
--- a/src/cmd/go/testdata/script/reuse_git.txt
+++ b/src/cmd/go/testdata/script/reuse_git.txt
@@ -13,7 +13,7 @@ cp stdout hellopseudo.json
 ! stdout '"(Query|TagPrefix|TagSum|Ref)"'
 stdout '"Version": "v0.0.0-20170922010558-fc3a09f3dc5c"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/hello"'
+stdout '"URL": ".*/git/hello"'
 stdout '"Hash": "fc3a09f3dc5cfe0d7a743ea18f1f5226e68b3777"'
 go clean -modcache
 
@@ -23,7 +23,7 @@ stderr 'git fetch'
 cp stdout hello.json
 stdout '"Version": "v0.0.0-20170922010558-fc3a09f3dc5c"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/hello"'
+stdout '"URL": ".*/git/hello"'
 stdout '"Query": "latest"'
 ! stdout '"TagPrefix"'
 stdout '"TagSum": "t1:47DEQpj8HBSa[+]/TImW[+]5JCeuQeRkm5NMpJWZG3hSuFU="'
@@ -36,7 +36,7 @@ stdout '"Hash": "fc3a09f3dc5cfe0d7a743ea18f1f5226e68b3777"'
 go mod download -x -json vcs-test.golang.org/git/hello.git@v0.0.0-20170922010558-fc3a09f3dc5c
 ! stderr 'git fetch'
 cp stdout hellopseudo2.json
-cmp hellopseudo.json hellopseudo2.json
+cmpenv hellopseudo.json hellopseudo2.json
 
 # go mod download vcstest/hello@hash needs to check TagSum to find pseudoversion base.
 go mod download -x -json vcs-test.golang.org/git/hello.git@fc3a09f3dc5c
@@ -45,7 +45,7 @@ cp stdout hellohash.json
 stdout '"Version": "v0.0.0-20170922010558-fc3a09f3dc5c"'
 stdout '"Query": "fc3a09f3dc5c"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/hello"'
+stdout '"URL": ".*/git/hello"'
 stdout '"TagSum": "t1:47DEQpj8HBSa[+]/TImW[+]5JCeuQeRkm5NMpJWZG3hSuFU="'
 stdout '"Hash": "fc3a09f3dc5cfe0d7a743ea18f1f5226e68b3777"'
 
@@ -100,7 +100,7 @@ cp stdout tagtests.json
 stdout '"Version": "v0.2.2"'
 stdout '"Query": "latest"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"TagPrefix"'
 stdout '"TagSum": "t1:Dp7yRKDuE8WjG0429PN9hYWjqhy2te7P9Oki/sMEOGo="'
 stdout '"Ref": "refs/tags/v0.2.2"'
@@ -112,7 +112,7 @@ cp stdout tagtestsv022.json
 stdout '"Version": "v0.2.2"'
 ! stdout '"Query":'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"TagPrefix"'
 ! stdout '"TagSum"'
 stdout '"Ref": "refs/tags/v0.2.2"'
@@ -124,7 +124,7 @@ cp stdout tagtestsmaster.json
 stdout '"Version": "v0.2.3-0.20190509225625-c7818c24fa2f"'
 stdout '"Query": "master"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"TagPrefix"'
 stdout '"TagSum": "t1:Dp7yRKDuE8WjG0429PN9hYWjqhy2te7P9Oki/sMEOGo="'
 stdout '"Ref": "refs/heads/master"'
@@ -137,7 +137,7 @@ cp stdout prefixtagtests.json
 stdout '"Version": "v0.0.10"'
 stdout '"Query": "latest"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/prefixtagtests"'
+stdout '"URL": ".*/git/prefixtagtests"'
 stdout '"Subdir": "sub"'
 stdout '"TagPrefix": "sub/"'
 stdout '"TagSum": "t1:YGSbWkJ8dn9ORAr[+]BlKHFK/2ZhXLb9hVuYfTZ9D8C7g="'
@@ -160,7 +160,7 @@ go mod download -reuse=hello.json -x -json vcs-test.golang.org/git/hello.git@lat
 stdout '"Reuse": true'
 stdout '"Version": "v0.0.0-20170922010558-fc3a09f3dc5c"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/hello"'
+stdout '"URL": ".*/git/hello"'
 ! stdout '"TagPrefix"'
 stdout '"TagSum": "t1:47DEQpj8HBSa[+]/TImW[+]5JCeuQeRkm5NMpJWZG3hSuFU="'
 stdout '"Ref": "HEAD"'
@@ -176,7 +176,7 @@ go mod download -reuse=hellopseudo.json -x -json vcs-test.golang.org/git/hello.g
 stdout '"Reuse": true'
 stdout '"Version": "v0.0.0-20170922010558-fc3a09f3dc5c"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/hello"'
+stdout '"URL": ".*/git/hello"'
 ! stdout '"(Query|TagPrefix|TagSum|Ref)"'
 stdout '"Hash": "fc3a09f3dc5cfe0d7a743ea18f1f5226e68b3777"'
 ! stdout '"(Dir|Info|GoMod|Zip)"'
@@ -188,7 +188,7 @@ stdout '"Reuse": true'
 stdout '"Query": "fc3a09f3dc5c"'
 stdout '"Version": "v0.0.0-20170922010558-fc3a09f3dc5c"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/hello"'
+stdout '"URL": ".*/git/hello"'
 ! stdout '"(TagPrefix|Ref)"'
 stdout '"TagSum": "t1:47DEQpj8HBSa[+]/TImW[+]5JCeuQeRkm5NMpJWZG3hSuFU="'
 stdout '"Hash": "fc3a09f3dc5cfe0d7a743ea18f1f5226e68b3777"'
@@ -251,7 +251,7 @@ stdout '"Reuse": true'
 stdout '"Version": "v0.2.2"'
 stdout '"Query": "latest"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"TagPrefix"'
 stdout '"TagSum": "t1:Dp7yRKDuE8WjG0429PN9hYWjqhy2te7P9Oki/sMEOGo="'
 stdout '"Ref": "refs/tags/v0.2.2"'
@@ -265,7 +265,7 @@ stdout '"Reuse": true'
 stdout '"Version": "v0.2.2"'
 ! stdout '"Query":'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"TagPrefix"'
 ! stdout '"TagSum"'
 stdout '"Ref": "refs/tags/v0.2.2"'
@@ -279,7 +279,7 @@ stdout '"Reuse": true'
 stdout '"Version": "v0.2.3-0.20190509225625-c7818c24fa2f"'
 stdout '"Query": "master"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"TagPrefix"'
 stdout '"TagSum": "t1:Dp7yRKDuE8WjG0429PN9hYWjqhy2te7P9Oki/sMEOGo="'
 stdout '"Ref": "refs/heads/master"'
@@ -293,7 +293,7 @@ stdout '"Reuse": true'
 stdout '"Version": "v0.2.3-0.20190509225625-c7818c24fa2f"'
 stdout '"Query": "master"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"TagPrefix"'
 stdout '"TagSum": "t1:Dp7yRKDuE8WjG0429PN9hYWjqhy2te7P9Oki/sMEOGo="'
 stdout '"Ref": "refs/heads/master"'
@@ -306,7 +306,7 @@ go mod download -reuse=prefixtagtests.json -x -json vcs-test.golang.org/git/pref
 stdout '"Version": "v0.0.10"'
 stdout '"Query": "latest"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/prefixtagtests"'
+stdout '"URL": ".*/git/prefixtagtests"'
 stdout '"Subdir": "sub"'
 stdout '"TagPrefix": "sub/"'
 stdout '"TagSum": "t1:YGSbWkJ8dn9ORAr[+]BlKHFK/2ZhXLb9hVuYfTZ9D8C7g="'
@@ -329,7 +329,7 @@ stderr 'git fetch'
 stdout '"Version": "v0.2.2"'
 ! stdout '"Query"'
 stdout '"VCS": "git"'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 ! stdout '"(TagPrefix|TagSum)"'
 stdout '"Ref": "refs/tags/v0.2.2"'
 stdout '"Hash": "59356c8cd18c5fe9a598167d98a6843e52d57952"'
@@ -343,7 +343,7 @@ cp tagtestsv022.json tagtestsv022badurl.json
 replace 'git/tagtests\"' 'git/tagtestsXXX\"' tagtestsv022badurl.json
 go mod download -reuse=tagtestsv022badurl.json -x -json vcs-test.golang.org/git/tagtests.git@v0.2.2
 ! stdout '"Reuse": true'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 stdout '"Dir"'
 stdout '"Info"'
 stdout '"GoMod"'
@@ -354,14 +354,14 @@ cp tagtestsv022.json tagtestsv022badvcs.json
 replace '\"git\"' '\"gitXXX\"' tagtestsv022badvcs.json
 go mod download -reuse=tagtestsv022badvcs.json -x -json vcs-test.golang.org/git/tagtests.git@v0.2.2
 ! stdout '"Reuse": true'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 
 # reuse with stale Dir
 cp tagtestsv022.json tagtestsv022baddir.json
 replace '\t\t\"Ref\":' '\t\t\"Subdir\": \"subdir\",\n\t\t\"Ref\":' tagtestsv022baddir.json
 go mod download -reuse=tagtestsv022baddir.json -x -json vcs-test.golang.org/git/tagtests.git@v0.2.2
 ! stdout '"Reuse": true'
-stdout '"URL": "https://vcs-test.golang.org/git/tagtests"'
+stdout '"URL": ".*/git/tagtests"'
 
 # reuse with stale TagSum
 cp tagtests.json tagtestsbadtagsum.json
diff --git a/src/cmd/go/testdata/vcstest/README b/src/cmd/go/testdata/vcstest/README
new file mode 100644
index 00000000000..f3a0e155897
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/README
@@ -0,0 +1,14 @@
+The scripts in this directory set up version-control repos for use in
+tests of cmd/go and its subpackages.
+
+They are written in a dialect of the same script language as in
+cmd/go/testdata/script, and the outputs are hosted by the server in
+cmd/go/internal/vcweb.
+
+To see the conditions and commands available for these scripts, run:
+
+	go test cmd/go/internal/vcweb -v --run=TestHelp
+
+To host these scripts in a standalone server, run:
+
+	go test cmd/go/internal/vcweb/vcstest -v --port=0
diff --git a/src/cmd/go/testdata/vcstest/bzr/hello.txt b/src/cmd/go/testdata/vcstest/bzr/hello.txt
new file mode 100644
index 00000000000..7d06503e1af
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/bzr/hello.txt
@@ -0,0 +1,32 @@
+handle bzr
+
+env BZR_EMAIL='Russ Cox '
+
+bzr init-repo .
+
+bzr init b
+cd b
+cp ../hello.go .
+bzr add hello.go
+bzr commit --commit-time='2017-09-21 21:20:12 -0400' -m 'hello world'
+bzr push ..
+cd ..
+rm b
+
+bzr log
+cmp stdout .bzr-log
+
+-- .bzr-log --
+------------------------------------------------------------
+revno: 1
+committer: Russ Cox 
+branch nick: b
+timestamp: Thu 2017-09-21 21:20:12 -0400
+message:
+  hello world
+-- hello.go --
+package main
+
+func main() {
+	println("hello, world")
+}
diff --git a/src/cmd/go/testdata/vcstest/fossil/hello.txt b/src/cmd/go/testdata/vcstest/fossil/hello.txt
new file mode 100644
index 00000000000..48fb774bcf2
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/fossil/hello.txt
@@ -0,0 +1,22 @@
+handle fossil
+
+env USER=rsc
+fossil init --date-override 2017-09-22T01:15:36Z hello.fossil
+fossil open --keep hello.fossil
+
+fossil add hello.go
+fossil commit --no-prompt --nosign --date-override 2017-09-22T01:19:07Z --comment 'hello world'
+
+fossil timeline --oneline
+cmp stdout .fossil-timeline
+
+-- .fossil-timeline --
+d4c7dcdc29 hello world
+58da0d15e9 initial empty check-in
++++ no more data (2) +++
+-- hello.go --
+package main
+
+func main() {
+	println("hello, world")
+}
diff --git a/src/cmd/go/testdata/vcstest/git/commit-after-tag.txt b/src/cmd/go/testdata/vcstest/git/commit-after-tag.txt
new file mode 100644
index 00000000000..b408a4f43df
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/commit-after-tag.txt
@@ -0,0 +1,39 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2019-07-15T17:16:47-04:00
+git add go.mod main.go
+git commit -m 'all: add go.mod and main.go'
+git tag v1.0.0
+
+at 2019-07-15T17:17:27-04:00
+cp _next/main.go main.go
+git add main.go
+git commit -m 'add init function'
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+b325d82 (HEAD -> master) add init function
+8da67e0 (tag: v1.0.0) all: add go.mod and main.go
+-- go.mod --
+module vcs-test.golang.org/git/commit-after-tag.git
+
+go 1.13
+-- main.go --
+package main
+
+func main() {}
+-- _next/main.go --
+package main
+
+func main() {}
+func init() {}
diff --git a/src/cmd/go/testdata/vcstest/git/empty-v2-without-v1.txt b/src/cmd/go/testdata/vcstest/git/empty-v2-without-v1.txt
new file mode 100644
index 00000000000..17a207f2bfb
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/empty-v2-without-v1.txt
@@ -0,0 +1,24 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2019-10-07T14:15:32-04:00
+git add go.mod
+git commit -m 'add go.mod file without go source files'
+git tag v2.0.0
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+122733c (HEAD -> master, tag: v2.0.0) add go.mod file without go source files
+-- go.mod --
+module vcs-test.golang.org/git/empty-v2-without-v1.git/v2
+
+go 1.14
diff --git a/src/cmd/go/testdata/vcstest/git/emptytest.txt b/src/cmd/go/testdata/vcstest/git/emptytest.txt
new file mode 100644
index 00000000000..af9bff3690d
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/emptytest.txt
@@ -0,0 +1,21 @@
+handle git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2018-07-03T22:35:49-04:00
+git add go.mod
+git commit -m 'initial'
+
+git log --oneline
+cmp stdout .git-log
+
+-- .git-log --
+7bb9146 initial
+-- go.mod --
+module vcs-test.golang.org/git/emptytest.git
diff --git a/src/cmd/go/testdata/vcstest/git/gitrepo1.txt b/src/cmd/go/testdata/vcstest/git/gitrepo1.txt
new file mode 100644
index 00000000000..e909d1208d0
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/gitrepo1.txt
@@ -0,0 +1,67 @@
+handle git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2018-04-17T15:43:22-04:00
+unquote ''
+cp stdout README
+git add README
+git commit -a -m 'empty README'
+git tag v1.2.3
+
+at 2018-04-17T15:45:48-04:00
+git branch -c v2
+git checkout v2
+echo 'v2'
+cp stdout v2
+git add v2
+git commit -a -m 'v2'
+git tag v2.3
+git tag v2.0.1
+git branch -c v2.3.4
+
+at 2018-04-17T16:00:19-04:00
+echo 'intermediate'
+cp stdout foo.txt
+git add foo.txt
+git commit -a -m 'intermediate'
+
+at 2018-04-17T16:00:32-04:00
+echo 'another'
+cp stdout another.txt
+git add another.txt
+git commit -a -m 'another'
+git tag v2.0.2
+
+at 2018-04-17T16:16:52-04:00
+git branch -c master v3
+git checkout v3
+mkdir v3/sub/dir
+echo 'v3/sub/dir/file'
+cp stdout v3/sub/dir/file.txt
+git add v3
+git commit -a -m 'add v3/sub/dir/file.txt'
+
+at 2018-04-17T22:23:00-04:00
+git checkout master
+git tag -a v1.2.4-annotated -m 'v1.2.4-annotated'
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+ede458df7cd0fdca520df19a33158086a8a68e81 refs/heads/master
+9d02800338b8a55be062c838d1f02e0c5780b9eb refs/heads/v2
+76a00fb249b7f93091bc2c89a789dab1fc1bc26f refs/heads/v2.3.4
+a8205f853c297ad2c3c502ba9a355b35b7dd3ca5 refs/heads/v3
+ede458df7cd0fdca520df19a33158086a8a68e81 refs/tags/v1.2.3
+b004e48a345a86ed7a2fb7debfa7e0b2f9b0dd91 refs/tags/v1.2.4-annotated
+76a00fb249b7f93091bc2c89a789dab1fc1bc26f refs/tags/v2.0.1
+9d02800338b8a55be062c838d1f02e0c5780b9eb refs/tags/v2.0.2
+76a00fb249b7f93091bc2c89a789dab1fc1bc26f refs/tags/v2.3
diff --git a/src/cmd/go/testdata/vcstest/git/hello.txt b/src/cmd/go/testdata/vcstest/git/hello.txt
new file mode 100644
index 00000000000..a010585ac70
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/hello.txt
@@ -0,0 +1,25 @@
+handle git
+
+env GIT_AUTHOR_NAME=bwk
+env GIT_AUTHOR_EMAIL=bwk
+env GIT_COMMITTER_NAME='Russ Cox'
+env GIT_COMMITTER_EMAIL='rsc@golang.org'
+
+git init
+git branch -m master
+
+at 2017-09-21T21:05:58-04:00
+git add hello.go
+git commit -a -m 'hello'
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+fc3a09f (HEAD -> master) hello
+-- hello.go --
+package main
+
+func main() {
+	println("hello, world")
+}
diff --git a/src/cmd/go/testdata/vcstest/git/insecurerepo.txt b/src/cmd/go/testdata/vcstest/git/insecurerepo.txt
new file mode 100644
index 00000000000..2cf3782992a
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/insecurerepo.txt
@@ -0,0 +1,32 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2019-04-03T13:30:35-04:00
+git add go.mod
+git commit -m 'all: initialize module'
+
+at 2019-09-04T14:39:48-04:00
+git add main.go
+git commit -m 'main: add Go source file'
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+6fecd21 (HEAD -> master) main: add Go source file
+d1a15cd all: initialize module
+-- go.mod --
+module vcs-test.golang.org/insecure/go/insecure
+
+go 1.13
+-- main.go --
+package main
+
+func main() {}
diff --git a/src/cmd/go/testdata/vcstest/git/mainonly.txt b/src/cmd/go/testdata/vcstest/git/mainonly.txt
new file mode 100644
index 00000000000..47b72f8b386
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/mainonly.txt
@@ -0,0 +1,23 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2019-09-05T14:07:43-04:00
+git add main.go
+git commit -a -m 'add main.go'
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+8a27e8b (HEAD -> master) add main.go
+-- main.go --
+package main
+
+func main() {}
diff --git a/src/cmd/go/testdata/vcstest/git/missingrepo.txt b/src/cmd/go/testdata/vcstest/git/missingrepo.txt
new file mode 100644
index 00000000000..b947d8cc991
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/missingrepo.txt
@@ -0,0 +1,10 @@
+handle dir
+
+-- missingrepo-git/index.html --
+
+
+
+-- missingrepo-git/notmissing/index.html --
+
+
+
diff --git a/src/cmd/go/testdata/vcstest/git/modlegacy1-new.txt b/src/cmd/go/testdata/vcstest/git/modlegacy1-new.txt
new file mode 100644
index 00000000000..52fdfa724bc
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/modlegacy1-new.txt
@@ -0,0 +1,33 @@
+handle git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2018-04-25T11:00:57-04:00
+git add go.mod new.go p1 p2
+git commit -m 'initial commit'
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+36cc50a (HEAD -> master) initial commit
+-- go.mod --
+module "vcs-test.golang.org/git/modlegacy1-new.git/v2"
+-- new.go --
+package new
+
+import _ "vcs-test.golang.org/git/modlegacy1-new.git/v2/p2"
+-- p1/p1.go --
+package p1
+
+import _ "vcs-test.golang.org/git/modlegacy1-old.git/p2"
+import _ "vcs-test.golang.org/git/modlegacy1-new.git"
+import _ "vcs-test.golang.org/git/modlegacy1-new.git/p2"
+-- p2/p2.go --
+package p2
diff --git a/src/cmd/go/testdata/vcstest/git/modlegacy1-old.txt b/src/cmd/go/testdata/vcstest/git/modlegacy1-old.txt
new file mode 100644
index 00000000000..06a5179106e
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/modlegacy1-old.txt
@@ -0,0 +1,27 @@
+handle git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2018-04-25T10:59:24-04:00
+git add p1 p2
+git commit -m 'initial commit'
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+6b4ba8b (HEAD -> master) initial commit
+-- p1/p1.go --
+package p1
+
+import _ "vcs-test.golang.org/git/modlegacy1-old.git/p2"
+import _ "vcs-test.golang.org/git/modlegacy1-new.git/p1"
+import _ "vcs-test.golang.org/git/modlegacy1-new.git"
+-- p2/p2.go --
+package p2
diff --git a/src/cmd/go/testdata/vcstest/git/no-tags.txt b/src/cmd/go/testdata/vcstest/git/no-tags.txt
new file mode 100644
index 00000000000..8d4fd4c6342
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/no-tags.txt
@@ -0,0 +1,27 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2019-07-15T17:20:47-04:00
+git add go.mod main.go
+git commit -m 'all: add go.mod and main.go'
+
+git log --oneline --decorate=short
+cmp stdout .git-log
+
+-- .git-log --
+e706ba1 (HEAD -> master) all: add go.mod and main.go
+-- go.mod --
+module vcs-test.golang.org/git/no-tags.git
+
+go 1.13
+-- main.go --
+package main
+
+func main() {}
diff --git a/src/cmd/go/testdata/vcstest/git/odd-tags.txt b/src/cmd/go/testdata/vcstest/git/odd-tags.txt
new file mode 100644
index 00000000000..9775849cf7c
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/odd-tags.txt
@@ -0,0 +1,48 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+
+at 2022-02-23T13:48:02-05:00
+git add README.txt
+git commit -m 'initial state'
+git tag 'v2.0.0+incompatible'
+
+at 2022-02-23T13:48:35-05:00
+git rm -r README.txt
+git add go.mod
+git commit -m 'migrate to Go modules'
+git tag 'v0.1.0+build-metadata'
+
+at 2022-02-23T14:41:55-05:00
+git branch -c v3-dev
+git checkout v3-dev
+cp v3/go.mod go.mod
+git commit go.mod -m 'update to /v3'
+git tag 'v3.0.0-20220223184802-12d19af20458'
+
+git checkout main
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+9d863d525bbfcc8eda09364738c4032393711a56 refs/heads/main
+cce3d0f5d2ec85678cca3c45ac4a87f3be5efaca refs/heads/v3-dev
+9d863d525bbfcc8eda09364738c4032393711a56 refs/tags/v0.1.0+build-metadata
+12d19af204585b0db3d2a876ceddf5b9323f5a4a refs/tags/v2.0.0+incompatible
+cce3d0f5d2ec85678cca3c45ac4a87f3be5efaca refs/tags/v3.0.0-20220223184802-12d19af20458
+-- README.txt --
+This module lacks a go.mod file.
+-- go.mod --
+module vcs-test.golang.org/git/odd-tags.git
+
+go 1.18
+-- v3/go.mod --
+module vcs-test.golang.org/git/odd-tags.git/v3
+
+go 1.18
diff --git a/src/cmd/go/testdata/vcstest/git/prefixtagtests.txt b/src/cmd/go/testdata/vcstest/git/prefixtagtests.txt
new file mode 100644
index 00000000000..372711c73f0
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/prefixtagtests.txt
@@ -0,0 +1,53 @@
+handle git
+
+env GIT_AUTHOR_NAME='Jay Conrod'
+env GIT_AUTHOR_EMAIL='jayconrod@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+at 2019-05-09T18:35:00-04:00
+
+git init
+git branch -m master
+
+git add sub
+git commit -m 'create module sub'
+
+echo 'v0.1.0'
+cp stdout status
+git add status
+git commit -a -m 'v0.1.0'
+git tag 'v0.1.0'
+
+echo 'sub/v0.0.9'
+cp stdout status
+git commit -a -m 'sub/v0.0.9'
+git tag 'sub/v0.0.9'
+
+echo 'sub/v0.0.10'
+cp stdout status
+git commit -a -m 'sub/v0.0.10'
+git tag 'sub/v0.0.10'
+
+echo 'v0.2.0'
+cp stdout status
+git commit -a -m 'v0.2.0'
+git tag 'v0.2.0'
+
+echo 'after last tag'
+cp stdout status
+git commit -a -m 'after last tag'
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+c3ee5d0dfbb9bf3c4d8bb2bce24cd8d14d2d4238 refs/heads/master
+2b7c4692e12c109263cab51b416fcc835ddd7eae refs/tags/sub/v0.0.10
+883885166298d79a0561d571a3044ec5db2e7c28 refs/tags/sub/v0.0.9
+db89fc573cfb939faf0aa0660671eb4cf8b8b673 refs/tags/v0.1.0
+1abe41965749e50828dd41de8d12c6ebc8e4e131 refs/tags/v0.2.0
+-- sub/go.mod --
+module vcs-test.golang.org/git/prefixtagtests.git/sub
+-- sub/sub.go --
+package sub
diff --git a/src/cmd/go/testdata/vcstest/git/querytest.txt b/src/cmd/go/testdata/vcstest/git/querytest.txt
new file mode 100644
index 00000000000..b079027b423
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/querytest.txt
@@ -0,0 +1,273 @@
+handle git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2018-07-03T22:31:01-04:00
+git add go.mod
+git commit -a -m 'v1'
+git tag start
+
+git branch -c v2
+
+at 2018-07-03T22:33:47-04:00
+echo 'before v0.0.0-pre1'
+cp stdout status
+git add status
+git commit -a -m 'before v0.0.0-pre1'
+
+echo 'at v0.0.0-pre1'
+cp stdout status
+git commit -a -m 'at v0.0.0-pre1'
+git tag 'v0.0.0-pre1'
+
+echo 'before v0.0.0'
+cp stdout status
+git commit -a -m 'before v0.0.0'
+
+echo 'at v0.0.0'
+cp stdout status
+git commit -a -m 'at v0.0.0'
+git tag 'v0.0.0'
+
+echo 'before v0.0.1'
+cp stdout status
+git commit -a -m 'before v0.0.1'
+
+echo 'at v0.0.1'
+cp stdout status
+git commit -a -m 'at v0.0.1'
+git tag 'v0.0.1'
+
+echo 'before v0.0.2'
+cp stdout status
+git commit -a -m 'before v0.0.2'
+
+echo 'at v0.0.2'
+cp stdout status
+git commit -a -m 'at v0.0.2'
+git tag 'v0.0.2'
+
+echo 'before v0.0.3'
+cp stdout status
+git commit -a -m 'before v0.0.3'
+
+echo 'at v0.0.3'
+cp stdout status
+git commit -a -m 'at v0.0.3'
+git tag 'v0.0.3'
+git tag favorite
+
+echo 'before v0.1.0'
+cp stdout status
+git commit -a -m 'before v0.1.0'
+
+echo 'at v0.1.0'
+cp stdout status
+git commit -a -m 'at v0.1.0'
+git tag v0.1.0
+
+echo 'before v0.1.1'
+cp stdout status
+git commit -a -m 'before v0.1.1'
+
+echo 'at v0.1.1'
+cp stdout status
+git commit -a -m 'at v0.1.1'
+git tag 'v0.1.1'
+
+echo 'before v0.1.2'
+cp stdout status
+git commit -a -m 'before v0.1.2'
+
+echo 'at v0.1.2'
+cp stdout status
+git commit -a -m 'at v0.1.2'
+git tag 'v0.1.2'
+
+echo 'before v0.3.0'
+cp stdout status
+git commit -a -m 'before v0.3.0'
+
+echo 'at v0.3.0'
+cp stdout status
+git commit -a -m 'at v0.3.0'
+git tag 'v0.3.0'
+
+echo 'before v1.0.0'
+cp stdout status
+git commit -a -m 'before v1.0.0'
+
+echo 'at v1.0.0'
+cp stdout status
+git commit -a -m 'at v1.0.0'
+git tag 'v1.0.0'
+
+echo 'before v1.1.0'
+cp stdout status
+git commit -a -m 'before v1.1.0'
+
+echo 'at v1.1.0'
+cp stdout status
+git commit -a -m 'at v1.1.0'
+git tag 'v1.1.0'
+
+echo 'before v1.9.0'
+cp stdout status
+git commit -a -m 'before v1.9.0'
+
+echo 'at v1.9.0'
+cp stdout status
+git commit -a -m 'at v1.9.0'
+git tag 'v1.9.0'
+
+echo 'before v1.9.9'
+cp stdout status
+git commit -a -m 'before v1.9.9'
+
+echo 'at v1.9.9'
+cp stdout status
+git commit -a -m 'at v1.9.9'
+git tag 'v1.9.9'
+
+at 2018-07-03T22:45:01-04:00
+echo 'before v1.9.10-pre1'
+cp stdout status
+git commit -a -m 'before v1.9.10-pre1'
+
+echo 'at v1.9.10-pre1'
+cp stdout status
+git commit -a -m 'at v1.9.10-pre1'
+git tag 'v1.9.10-pre1'
+
+at 2018-07-03T22:50:24-04:00
+git checkout v2
+cp v2/go.mod go.mod
+git add go.mod
+git commit -a -m 'v2'
+
+at 2018-07-03T22:51:14-04:00
+echo 'before v2.0.0'
+cp stdout status
+git add status
+git commit -a -m 'before v2.0.0'
+
+at 2018-07-03T22:51:14-04:00
+echo 'at v2.0.0'
+cp stdout status
+git commit -a -m 'at v2.0.0'
+git tag 'v2.0.0'
+
+at 2018-07-03T22:51:14-04:00
+echo 'before v2.1.0'
+cp stdout status
+git commit -a -m 'before v2.1.0'
+
+at 2018-07-03T22:51:14-04:00
+echo 'at v2.1.0'
+cp stdout status
+git commit -a -m 'at v2.1.0'
+git tag 'v2.1.0'
+
+at 2018-07-03T22:51:14-04:00
+echo 'before v2.2.0'
+cp stdout status
+git commit -a -m 'before v2.2.0'
+
+at 2018-07-03T22:51:14-04:00
+echo 'at v2.2.0'
+cp stdout status
+git commit -a -m 'at v2.2.0'
+git tag 'v2.2.0'
+
+at 2018-07-03T22:51:14-04:00
+echo 'before v2.5.5'
+cp stdout status
+git commit -a -m 'before v2.5.5'
+
+at 2018-07-03T22:51:14-04:00
+echo 'at v2.5.5'
+cp stdout status
+git commit -a -m 'at v2.5.5'
+git tag 'v2.5.5'
+
+at 2018-07-03T23:35:18-04:00
+echo 'after v2.5.5'
+cp stdout status
+git commit -a -m 'after v2.5.5'
+
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL=bcmills@google.com
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git checkout v2.5.5
+
+at 2019-05-13T17:13:56-04:00
+echo 'before v2.6.0-pre1'
+cp stdout status
+git commit -a -m 'before v2.6.0-pre1'
+
+at 2019-05-13T17:13:56-04:00
+echo 'at v2.6.0-pre1'
+cp stdout status
+git commit -a -m 'at v2.6.0-pre1'
+git tag 'v2.6.0-pre1'
+
+git checkout master
+
+at 2019-05-13T16:11:25-04:00
+echo 'before v1.9.10-pre2+metadata'
+cp stdout status
+git commit -a -m 'before v1.9.10-pre2+metadata'
+
+at 2019-05-13T16:11:26-04:00
+echo 'at v1.9.10-pre2+metadata'
+cp stdout status
+git commit -a -m 'at v1.9.10-pre2+metadata'
+git tag 'v1.9.10-pre2+metadata'
+
+at 2019-12-20T08:46:14-05:00
+echo 'after v1.9.10-pre2+metadata'
+cp stdout status
+git commit -a -m 'after v1.9.10-pre2+metadata'
+
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+ed5ffdaa1f5e7e0be6f5ba2d63097026506224f2 refs/heads/master
+feed8f518cf4a7215a3b2a8268b8b0746dcbb12d refs/heads/v2
+f6abd4e3ed7f2297bc8fd2888bd6d5412e255fcc refs/tags/favorite
+5e9e31667ddfe16e9350f4bd00acc933c8cd5e56 refs/tags/start
+0de900e0063bcc310ea0621bfbc227a9b4e3b020 refs/tags/v0.0.0
+e5ec98b1c15df29e3bd346d538d73b6e8c3b500c refs/tags/v0.0.0-pre1
+179bc86b1be3f6d4553f77ebe68a8b6d750ceff8 refs/tags/v0.0.1
+81da2346e009fa1072fe4de3a9a223398ea8ec39 refs/tags/v0.0.2
+f6abd4e3ed7f2297bc8fd2888bd6d5412e255fcc refs/tags/v0.0.3
+7a1b6bf60ae5bb2b2bd49d152e0bbad806056122 refs/tags/v0.1.0
+daedca9abee3171fe45e0344098a993675ac799e refs/tags/v0.1.1
+ce829e0f1c45a2eca0f1ad16d7c1aca7cddb433b refs/tags/v0.1.2
+44aadfee25d86acb32d6f352afd1d602b0e3a651 refs/tags/v0.3.0
+20756d3a393908b2edb5db0f0bb954e962860168 refs/tags/v1.0.0
+b0bf267f64b7d5b5cabe22fbcad22f3f1642b7e5 refs/tags/v1.1.0
+609dca58c03f0ddf1d8ebe46c1f74fc6a99f3e73 refs/tags/v1.9.0
+e0cf3de987e660c21b6950e85b317ce5f7fbb9d9 refs/tags/v1.9.10-pre1
+42abcb6df8eee6983aeca9a307c28ea40530aceb refs/tags/v1.9.10-pre2+metadata
+5ba9a4ea62136ae86213feba68bc73858f55b7e1 refs/tags/v1.9.9
+9763aa065ae27c6cacec5ca8b6dfa43a1b31dea0 refs/tags/v2.0.0
+23c28cb696ff40a2839ce406f2c173aa6c3cdda6 refs/tags/v2.1.0
+1828ee9f8074075675013e4d488d5d49ddc1b502 refs/tags/v2.2.0
+d7352560158175e3b6aa11e22efb06d9e87e6eea refs/tags/v2.5.5
+fb9e35b393eb0cccc37e13e243ce60b4ff8c7eea refs/tags/v2.6.0-pre1
+-- go.mod --
+module vcs-test.golang.org/git/querytest.git
+-- v2/go.mod --
+module vcs-test.golang.org/git/querytest.git/v2
diff --git a/src/cmd/go/testdata/vcstest/git/retract-pseudo.txt b/src/cmd/go/testdata/vcstest/git/retract-pseudo.txt
new file mode 100644
index 00000000000..3f07d6cce3f
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/retract-pseudo.txt
@@ -0,0 +1,32 @@
+handle git
+
+env GIT_AUTHOR_NAME='Jay Conrod'
+env GIT_AUTHOR_EMAIL='jayconrod@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+at 2020-10-09T13:37:47-04:00
+
+git init
+
+git add go.mod p.go
+git commit -m 'create module retract-pseudo'
+git tag v1.0.0
+
+git mv p.go q.go
+git commit -m 'trivial change'
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+713affd19d7b9b6dc876b603017f3dcaab8ba674 refs/heads/main
+64c061ed4371ef372b6bbfd58ee32015d6bfc3e5 refs/tags/v1.0.0
+-- go.mod --
+module vcs-test.golang.org/git/retract-pseudo.git
+
+go 1.16
+
+retract v1.0.0
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/vcstest/git/semver-branch.txt b/src/cmd/go/testdata/vcstest/git/semver-branch.txt
new file mode 100644
index 00000000000..86bdd8c9f09
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/semver-branch.txt
@@ -0,0 +1,52 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+
+at 2022-02-02T14:15:21-05:00
+git add pkg go.mod
+git commit -a -m 'pkg: add empty package'
+git tag 'v0.1.0'
+
+at 2022-02-02T14:19:44-05:00
+git branch -c 'v1.0.0'
+git branch -c 'v2.0.0'
+git checkout 'v1.0.0'
+cp v1/pkg/pkg.go pkg/pkg.go
+git commit -a -m 'pkg: start developing toward v1.0.0'
+
+at 2022-02-03T10:53:13-05:00
+git branch -c 'v3.0.0-devel'
+git checkout 'v3.0.0-devel'
+git checkout v0.1.0 pkg/pkg.go
+git commit -a -m 'pkg: remove panic'
+git tag v4.0.0-beta.1
+
+git checkout main
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+33ea7ee36f3e3f44f528664b3712c9fa0cef7502 refs/heads/main
+09c4d8f6938c7b5eeae46858a72712b8700fa46a refs/heads/v1.0.0
+33ea7ee36f3e3f44f528664b3712c9fa0cef7502 refs/heads/v2.0.0
+d59622f6e4d77f008819083582fde71ea1921b0c refs/heads/v3.0.0-devel
+33ea7ee36f3e3f44f528664b3712c9fa0cef7502 refs/tags/v0.1.0
+d59622f6e4d77f008819083582fde71ea1921b0c refs/tags/v4.0.0-beta.1
+-- go.mod --
+module vcs-test.golang.org/git/semver-branch.git
+
+go 1.16
+-- pkg/pkg.go --
+package pkg
+-- v1/pkg/pkg.go --
+package pkg
+
+func init() {
+	panic("TODO")
+}
diff --git a/src/cmd/go/testdata/vcstest/git/tagtests.txt b/src/cmd/go/testdata/vcstest/git/tagtests.txt
new file mode 100644
index 00000000000..b0babb50e2b
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/tagtests.txt
@@ -0,0 +1,44 @@
+handle git
+
+env GIT_AUTHOR_NAME='Jay Conrod'
+env GIT_AUTHOR_EMAIL='jayconrod@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+at 2019-05-09T18:56:25-04:00
+
+git init
+git branch -m master
+
+git add go.mod tagtests.go
+git commit -m 'create module tagtests'
+git branch -c b
+
+git add v0.2.1
+git commit -m 'v0.2.1'
+git tag 'v0.2.1'
+
+git checkout b
+git add 'v0.2.2'
+git commit -m 'v0.2.2'
+git tag 'v0.2.2'
+
+git checkout master
+git merge b -m 'merge'
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+59356c8cd18c5fe9a598167d98a6843e52d57952 refs/heads/b
+c7818c24fa2f3f714c67d0a6d3e411c85a518d1f refs/heads/master
+101c49f5af1b2466332158058cf5f03c8cca6429 refs/tags/v0.2.1
+59356c8cd18c5fe9a598167d98a6843e52d57952 refs/tags/v0.2.2
+-- go.mod --
+module vcs-test.golang.org/git/tagtests.git
+-- tagtests.go --
+package tagtests
+-- v0.2.1 --
+v0.2.1
+-- v0.2.2 --
+v0.2.2
diff --git a/src/cmd/go/testdata/vcstest/git/v2repo.txt b/src/cmd/go/testdata/vcstest/git/v2repo.txt
new file mode 100644
index 00000000000..6a2216d1272
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/v2repo.txt
@@ -0,0 +1,26 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2019-04-03T11:52:15-04:00
+env GIT_AUTHOR_DATE=2019-04-03T11:44:11-04:00
+git add go.mod
+git commit -m 'all: add go.mod'
+git tag 'v2.0.0'
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+203b91c896acd173aa719e4cdcb7d463c4b090fa refs/heads/master
+203b91c896acd173aa719e4cdcb7d463c4b090fa refs/tags/v2.0.0
+-- go.mod --
+module vcs-test.golang.org/go/v2module/v2
+
+go 1.12
diff --git a/src/cmd/go/testdata/vcstest/git/v2sub.txt b/src/cmd/go/testdata/vcstest/git/v2sub.txt
new file mode 100644
index 00000000000..5ad197a114d
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/v2sub.txt
@@ -0,0 +1,34 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+
+at 2022-02-22T15:53:33-05:00
+git add v2sub.go v2
+git commit -m 'all: add package v2sub and v2sub/v2'
+git tag v2.0.0
+
+at 2022-02-22T15:55:07-05:00
+git add README.txt
+git commit -m 'v2sub: add README.txt'
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+80beb17a16036f17a5aedd1bb5bd6d407b3c6dc5 refs/heads/main
+5fcd3eaeeb391d399f562fd45a50dac9fc34ae8b refs/tags/v2.0.0
+-- v2/go.mod --
+module vcs-test.golang.org/git/v2sub.git/v2
+
+go 1.16
+-- v2/v2sub.go --
+package v2sub
+-- v2sub.go --
+package v2sub
+-- README.txt --
+This root module lacks a go.mod file.
diff --git a/src/cmd/go/testdata/vcstest/git/v3pkg.txt b/src/cmd/go/testdata/vcstest/git/v3pkg.txt
new file mode 100644
index 00000000000..b5276db4da4
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/v3pkg.txt
@@ -0,0 +1,28 @@
+handle git
+
+env GIT_AUTHOR_NAME='Bryan C. Mills'
+env GIT_AUTHOR_EMAIL='bcmills@google.com'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2019-07-15T14:01:24-04:00
+env GIT_AUTHOR_DATE=2019-07-15T13:59:34-04:00
+git add go.mod v3pkg.go
+git commit -a -m 'all: add go.mod with v3 path'
+git tag 'v3.0.0'
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+a3eab1261b8e3164bcbde9171c23d5fd36e32a85 refs/heads/master
+a3eab1261b8e3164bcbde9171c23d5fd36e32a85 refs/tags/v3.0.0
+-- go.mod --
+module vcs-test.golang.org/git/v3pkg.git/v3
+
+go 1.13
+-- v3pkg.go --
+package v3pkg
diff --git a/src/cmd/go/testdata/vcstest/git/vgotest1.txt b/src/cmd/go/testdata/vcstest/git/vgotest1.txt
new file mode 100644
index 00000000000..a9730ba1ef1
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/git/vgotest1.txt
@@ -0,0 +1,257 @@
+handle git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2018-02-19T17:21:09-05:00
+git add LICENSE README.md
+git commit -m 'initial commit'
+
+git checkout --detach HEAD
+
+at 2018-02-19T18:10:06-05:00
+mkdir pkg
+echo 'package p // pkg/p.go'
+cp stdout pkg/p.go
+git add pkg/p.go
+git commit -m 'add pkg/p.go'
+git tag v0.0.0
+git tag v1.0.0
+git tag mytag
+
+git checkout --detach HEAD
+
+at 2018-02-19T18:14:23-05:00
+mkdir v2
+echo 'module "github.com/rsc/vgotest1/v2" // root go.mod'
+cp stdout go.mod
+git add go.mod
+git commit -m 'go.mod v2'
+git tag v2.0.1
+
+at 2018-02-19T18:15:11-05:00
+mkdir submod/pkg
+echo 'package p // submod/pkg/p.go'
+cp stdout submod/pkg/p.go
+git add submod/pkg/p.go
+git commit -m 'submod/pkg/p.go'
+git tag v2.0.2
+
+at 2018-02-19T18:16:04-05:00
+echo 'module "github.com/rsc/vgotest" // v2/go.mod'
+cp stdout v2/go.mod
+git add v2/go.mod
+git commit -m 'v2/go.mod: bad go.mod (no version)'
+git tag v2.0.3
+
+at 2018-02-19T19:03:38-05:00
+env GIT_AUTHOR_DATE=2018-02-19T18:16:38-05:00
+echo 'module "github.com/rsc/vgotest1/v2" // v2/go.mod'
+cp stdout v2/go.mod
+git add v2/go.mod
+git commit -m 'v2/go.mod: fix'
+git tag v2.0.4
+
+at 2018-02-19T19:03:59-05:00
+env GIT_AUTHOR_DATE=2018-02-19T18:17:02-05:00
+echo 'module "github.com/rsc/vgotest1" // root go.mod'
+cp stdout go.mod
+git add go.mod
+git commit -m 'go.mod: drop v2'
+git tag v2.0.5
+
+git checkout --detach mytag
+
+at 2018-02-19T18:10:28-05:00
+echo 'module "github.com/rsc/vgotest1" // root go.mod'
+cp stdout go.mod
+git add go.mod
+git commit -m 'go.mod'
+git tag v0.0.1
+git tag v1.0.1
+
+at 2018-02-19T18:11:28-05:00
+mkdir submod/pkg
+echo 'package pkg // submod/pkg/p.go'
+cp stdout submod/pkg/p.go
+git add submod
+git commit -m 'submod/pkg/p.go'
+git tag v1.0.2
+
+at 2018-02-19T18:12:07-05:00
+echo 'module "github.com/vgotest1/submod" // submod/go.mod'
+cp stdout submod/go.mod
+git add submod/go.mod
+git commit -m 'submod/go.mod'
+git tag v1.0.3
+git tag submod/v1.0.4
+
+at 2018-02-19T18:12:59-05:00
+git apply 0001-submod-go.mod-add-require-vgotest1-v1.1.0.patch
+git commit -a -m 'submod/go.mod: add require vgotest1 v1.1.0'
+git tag submod/v1.0.5
+
+at 2018-02-19T18:13:36-05:00
+git apply 0002-go.mod-add-require-submod-v1.0.5.patch
+git commit -a -m 'go.mod: add require submod v1.0.5'
+git tag v1.1.0
+
+git checkout master
+
+at 2018-02-19T17:23:01-05:00
+mkdir pkg
+echo 'package pkg'
+cp stdout pkg/p.go
+git add pkg/p.go
+git commit -m 'pkg: add'
+
+at 2018-02-19T17:30:23-05:00
+env GIT_AUTHOR_DATE=2018-02-19T17:24:48-05:00
+echo 'module "github.com/vgotest1/v2"'
+cp stdout go.mod
+git add go.mod
+git commit -m 'add go.mod'
+
+at 2018-02-19T17:30:45-05:00
+echo 'module "github.com/vgotest1"'
+cp stdout go.mod
+git add go.mod
+git commit -m 'bad mod path'
+
+at 2018-02-19T17:31:34-05:00
+mkdir v2
+echo 'module "github.com/vgotest1/v2"'
+cp stdout v2/go.mod
+git add v2/go.mod
+git commit -m 'add v2/go.mod'
+
+at 2018-02-19T17:32:37-05:00
+echo 'module "github.com/vgotest1/v2"'
+cp stdout go.mod
+git add go.mod
+git commit -m 'say v2 in root go.mod'
+
+git checkout --detach HEAD
+at 2018-02-19T17:51:24-05:00
+	# README.md at this commit lacked a trailing newline, so 'git apply' can't
+	# seem to apply it correctly as a patch. Instead, we use 'echo -e' to write
+	# the exact contents.
+unquote 'This is a test repo for versioned go.\nThere''s nothing useful here.\n\n	v0.0.0 - has pkg/p.go\n	v0.0.1 - has go.mod\n	\n	v1.0.0 - has pkg/p.go\n	v1.0.1 - has go.mod\n	v1.0.2 - has submod/pkg/p.go\n	v1.0.3 - has submod/go.mod\n	submod/v1.0.4 - same\n	submod/v1.0.5 - add requirement on v1.1.0\n	v1.1.0 - add requirement on submod/v1.0.5\n	\n	v2.0.0 - has pkg/p.go\n	v2.0.1 - has go.mod with v2 module path\n	v2.0.2 - has go.mod with v1 (no version) module path\n	v2.0.3 - has v2/go.mod with v2 module path\n	v2.0.5 - has go.mod AND v2/go.mod with v2 module path\n	'
+cp stdout README.md
+mkdir v2/pkg
+echo 'package q'
+cp stdout v2/pkg/q.go
+git add README.md v2/pkg/q.go
+git commit -m 'add q'
+git tag v2.0.6
+
+git checkout --detach mytag~1
+at 2018-07-18T21:21:27-04:00
+env GIT_AUTHOR_DATE=2018-02-19T18:10:06-05:00
+mkdir pkg
+echo 'package p // pkg/p.go'
+cp stdout pkg/p.go
+git add pkg/p.go
+unquote 'add pkg/p.go\n\nv2\n'
+cp stdout COMMIT_MSG
+git commit -F COMMIT_MSG
+git tag v2.0.0
+
+git checkout master
+
+git show-ref --tags --heads
+cmp stdout .git-refs
+
+-- .git-refs --
+a08abb797a6764035a9314ed5f1d757e0224f3bf refs/heads/master
+80d85c5d4d17598a0e9055e7c175a32b415d6128 refs/tags/mytag
+8afe2b2efed96e0880ecd2a69b98a53b8c2738b6 refs/tags/submod/v1.0.4
+70fd92eaa4dacf82548d0c6099f5b853ae2c1fc8 refs/tags/submod/v1.0.5
+80d85c5d4d17598a0e9055e7c175a32b415d6128 refs/tags/v0.0.0
+5a115c66393dd8c4a5cc3215653850d7f5640d0e refs/tags/v0.0.1
+80d85c5d4d17598a0e9055e7c175a32b415d6128 refs/tags/v1.0.0
+5a115c66393dd8c4a5cc3215653850d7f5640d0e refs/tags/v1.0.1
+2e38a1a347ba4d9e9946ec0ce480710ff445c919 refs/tags/v1.0.2
+8afe2b2efed96e0880ecd2a69b98a53b8c2738b6 refs/tags/v1.0.3
+b769f2de407a4db81af9c5de0a06016d60d2ea09 refs/tags/v1.1.0
+45f53230a74ad275c7127e117ac46914c8126160 refs/tags/v2.0.0
+ea65f87c8f52c15ea68f3bdd9925ef17e20d91e9 refs/tags/v2.0.1
+f7b23352af1cd750b11e4673b20b24c2d239430a refs/tags/v2.0.2
+f18795870fb14388a21ef3ebc1d75911c8694f31 refs/tags/v2.0.3
+1f863feb76bc7029b78b21c5375644838962f88d refs/tags/v2.0.4
+2f615117ce481c8efef46e0cc0b4b4dccfac8fea refs/tags/v2.0.5
+a01a0aef06cbd571294fc5451788cd4eadbfd651 refs/tags/v2.0.6
+-- LICENSE --
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-- README.md --
+This is a test repo for versioned go.
+There's nothing useful here.
+-- 0001-submod-go.mod-add-require-vgotest1-v1.1.0.patch --
+From 70fd92eaa4dacf82548d0c6099f5b853ae2c1fc8 Mon Sep 17 00:00:00 2001
+From: Russ Cox 
+Date: Mon, 19 Feb 2018 18:12:59 -0500
+Subject: [PATCH] submod/go.mod: add require vgotest1 v1.1.0
+
+---
+ submod/go.mod | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/submod/go.mod b/submod/go.mod
+index 7b18d93..c88de0f 100644
+--- a/submod/go.mod
++++ b/submod/go.mod
+@@ -1 +1,2 @@
+ module "github.com/vgotest1/submod" // submod/go.mod
++require "github.com/vgotest1" v1.1.0
+--
+2.36.1.838.g23b219f8e3
+-- 0002-go.mod-add-require-submod-v1.0.5.patch --
+From b769f2de407a4db81af9c5de0a06016d60d2ea09 Mon Sep 17 00:00:00 2001
+From: Russ Cox 
+Date: Mon, 19 Feb 2018 18:13:36 -0500
+Subject: [PATCH] go.mod: add require submod v1.0.5
+
+---
+ go.mod | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/go.mod b/go.mod
+index ac7a6d7..6118671 100644
+--- a/go.mod
++++ b/go.mod
+@@ -1 +1,2 @@
+ module "github.com/rsc/vgotest1" // root go.mod
++require "github.com/rsc/vgotest1/submod" v1.0.5
+--
+2.36.1.838.g23b219f8e3
diff --git a/src/cmd/go/testdata/vcstest/hg/custom-hg-hello.txt b/src/cmd/go/testdata/vcstest/hg/custom-hg-hello.txt
new file mode 100644
index 00000000000..572cbdfef06
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/hg/custom-hg-hello.txt
@@ -0,0 +1,17 @@
+handle hg
+hg init
+
+hg add hello.go
+hg commit --user 'Russ Cox ' --date '2017-10-10T19:39:36-04:00' --message 'hello'
+
+hg log -r ':' --template '{node|short} {desc|strip|firstline}\n'
+cmp stdout .hg-log
+
+-- .hg-log --
+a8c8e7a40da9 hello
+-- hello.go --
+package main // import "vcs-test.golang.org/go/custom-hg-hello"
+
+func main() {
+	println("hello")
+}
diff --git a/src/cmd/go/testdata/vcstest/hg/hello.txt b/src/cmd/go/testdata/vcstest/hg/hello.txt
new file mode 100644
index 00000000000..10f114e5729
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/hg/hello.txt
@@ -0,0 +1,17 @@
+handle hg
+hg init
+
+hg add hello.go
+hg commit --user 'bwk' --date '2017-09-21T21:14:14-04:00' --message 'hello world'
+
+hg log -r ':' --template '{node|short} {desc|strip|firstline}\n'
+cmp stdout .hg-log
+
+-- .hg-log --
+e483a7d9f8c9 hello world
+-- hello.go --
+package main
+
+func main() {
+	println("hello, world")
+}
diff --git a/src/cmd/go/testdata/vcstest/hg/hgrepo1.txt b/src/cmd/go/testdata/vcstest/hg/hgrepo1.txt
new file mode 100644
index 00000000000..0022cf52cbf
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/hg/hgrepo1.txt
@@ -0,0 +1,138 @@
+handle hg
+
+mkdir git
+cd git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+at 2018-04-17T15:43:22-04:00
+unquote ''
+cp stdout README
+git add README
+git commit -a -m 'empty README'
+git tag v1.2.3
+
+at 2018-04-17T15:45:48-04:00
+git branch -c v2
+git checkout v2
+echo 'v2'
+cp stdout v2
+git add v2
+git commit -a -m 'v2'
+git tag v2.3
+git tag v2.0.1
+git branch -c v2.3.4
+git tag branch-v2.3.4
+
+at 2018-04-17T16:00:19-04:00
+echo 'intermediate'
+cp stdout foo.txt
+git add foo.txt
+git commit -a -m 'intermediate'
+
+at 2018-04-17T16:00:32-04:00
+echo 'another'
+cp stdout another.txt
+git add another.txt
+git commit -a -m 'another'
+git tag v2.0.2
+git tag branch-v2
+
+at 2018-04-17T16:16:52-04:00
+git branch -c master v3
+git checkout v3
+mkdir v3/sub/dir
+echo 'v3/sub/dir/file'
+cp stdout v3/sub/dir/file.txt
+git add v3
+git commit -a -m 'add v3/sub/dir/file.txt'
+git tag branch-v3
+
+at 2018-04-17T22:23:00-04:00
+git checkout master
+git tag -a v1.2.4-annotated -m 'v1.2.4-annotated'
+
+cd ..
+
+hg init
+hg convert --datesort ./git .
+rm ./git
+
+hg update -C v2
+hg branch v2
+unquote ''
+cp stdout dummy
+hg add dummy
+hg commit --user 'Russ Cox ' --date '2018-06-27T12:15:24-04:00' -m 'dummy'
+
+# 'hg convert' blindly stamps a tag-update commit at the end of whatever branch
+# happened to contain the last converted commit — in this case, v3. However, the
+# original vcs-test.golang.org copy of this repo had this commit on the v3
+# branch as a descendent of 'add v3/sub/dir/file.txt', so that's where we put it
+# here. That leaves the convert-repo 'update tags' commit only reachable as the
+# head of the default branch.
+hg update -r 4
+
+hg branch v3
+unquote ''
+cp stdout dummy
+hg add dummy
+hg commit --user 'Russ Cox ' --date '2018-06-27T12:15:45-04:00' -m 'dummy'
+
+hg update v2.3.4
+hg branch v2.3.4
+unquote ''
+cp stdout dummy
+hg add dummy
+hg commit --user 'Russ Cox ' --date '2018-06-27T12:16:10-04:00' -m 'dummy'
+
+hg tag --user 'Russ Cox ' --date '2018-06-27T12:16:30-04:00' -m 'Removed tag branch-v2, branch-v3, branch-v2.3.4' --remove branch-v2 branch-v3 branch-v2.3.4
+
+# Adding commits to the above branches updates both the branch heads and the
+# corresponding bookmarks.
+# But apparently at some point it did not do so? The original copy of this repo
+# had bookmarks pointing to the base of each branch instead of the tip. 🤔
+# Either way, force the bookmarks we care about to match the original copy of
+# the repo.
+hg book v2 -r 3 --force
+hg book v2.3.4 -r 1 --force
+hg book v3 -r 5 --force
+
+hg log -G --debug
+
+hg tags
+cmp stdout .hg-tags
+
+	# 'hg convert' leaves an 'update tags' commit on the default branch, and that
+	# commit always uses the current date (so is not reproducible). Fortunately,
+	# that commit lands on the 'default' branch and is not tagged as 'tip', so it
+	# seems to be mostly harmless. However, because it is nondeterministic we
+	# should avoid listing it here.
+hg branches -r 6 -r 7 -r 9
+cmp stdout .hg-branches
+
+	# Likewise, omit bookmark v3, which ends up on the nondeterministic commit.
+hg bookmarks -l master v2 v2.3.4
+cmp stdout .hg-bookmarks
+
+-- .hg-branches --
+v2.3.4                         9:18518c07eb8e
+v3                             7:a2cad8a2b1bb
+v2                             6:9a4f43d231ec
+-- .hg-tags --
+tip                                9:18518c07eb8e
+v2.0.2                             3:8f49ee7a6ddc
+v2.3                               1:88fde824ec8b
+v2.0.1                             1:88fde824ec8b
+v1.2.4-annotated                   0:41964ddce118
+v1.2.3                             0:41964ddce118
+-- .hg-bookmarks --
+   master                    0:41964ddce118
+   v2                        3:8f49ee7a6ddc
+   v2.3.4                    1:88fde824ec8b
diff --git a/src/cmd/go/testdata/vcstest/hg/vgotest1.txt b/src/cmd/go/testdata/vcstest/hg/vgotest1.txt
new file mode 100644
index 00000000000..5e10cefa7b9
--- /dev/null
+++ b/src/cmd/go/testdata/vcstest/hg/vgotest1.txt
@@ -0,0 +1,322 @@
+handle hg
+
+cd git
+
+env GIT_AUTHOR_NAME='Russ Cox'
+env GIT_AUTHOR_EMAIL='rsc@golang.org'
+env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME
+env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL
+
+git init
+git branch -m master
+
+# 0
+at 2018-02-19T17:21:09-05:00
+git add LICENSE README.md
+git commit -m 'initial commit'
+
+# 1
+git branch -c mybranch
+git checkout mybranch
+
+at 2018-02-19T18:10:06-05:00
+mkdir pkg
+echo 'package p // pkg/p.go'
+cp stdout pkg/p.go
+git add pkg/p.go
+git commit -m 'add pkg/p.go'
+git tag v0.0.0
+git tag v1.0.0
+git tag v2.0.0
+git tag mytag
+
+git branch -c v1
+git branch -c v2
+git checkout v2
+
+# 2
+at 2018-02-19T18:14:23-05:00
+mkdir v2
+echo 'module "github.com/rsc/vgotest1/v2" // root go.mod'
+cp stdout go.mod
+git add go.mod
+git commit -m 'go.mod v2'
+git tag v2.0.1
+
+# 3
+at 2018-02-19T18:15:11-05:00
+mkdir submod/pkg
+echo 'package p // submod/pkg/p.go'
+cp stdout submod/pkg/p.go
+git add submod/pkg/p.go
+git commit -m 'submod/pkg/p.go'
+git tag v2.0.2
+
+# 4
+at 2018-02-19T18:16:04-05:00
+echo 'module "github.com/rsc/vgotest" // v2/go.mod'
+cp stdout v2/go.mod
+git add v2/go.mod
+git commit -m 'v2/go.mod: bad go.mod (no version)'
+git tag v2.0.3
+
+# 5
+at 2018-02-19T19:03:38-05:00
+env GIT_AUTHOR_DATE=2018-02-19T18:16:38-05:00
+echo 'module "github.com/rsc/vgotest1/v2" // v2/go.mod'
+cp stdout v2/go.mod
+git add v2/go.mod
+git commit -m 'v2/go.mod: fix'
+git tag v2.0.4
+
+# 6
+at 2018-02-19T19:03:59-05:00
+env GIT_AUTHOR_DATE=2018-02-19T18:17:02-05:00
+echo 'module "github.com/rsc/vgotest1" // root go.mod'
+cp stdout go.mod
+git add go.mod
+git commit -m 'go.mod: drop v2'
+git tag v2.0.5
+
+git checkout v1
+
+# 7
+at 2018-02-19T18:10:28-05:00
+echo 'module "github.com/rsc/vgotest1" // root go.mod'
+cp stdout go.mod
+git add go.mod
+git commit -m 'go.mod'
+git tag v0.0.1
+git tag v1.0.1
+
+# 8
+at 2018-02-19T18:11:28-05:00
+mkdir submod/pkg
+echo 'package pkg // submod/pkg/p.go'
+cp stdout submod/pkg/p.go
+git add submod
+git commit -m 'submod/pkg/p.go'
+git tag v1.0.2
+
+# 9
+at 2018-02-19T18:12:07-05:00
+echo 'module "github.com/vgotest1/submod" // submod/go.mod'
+cp stdout submod/go.mod
+git add submod/go.mod
+git commit -m 'submod/go.mod'
+git tag v1.0.3
+git tag submod/v1.0.4
+
+# 10
+at 2018-02-19T18:12:59-05:00
+git apply ../0001-submod-go.mod-add-require-vgotest1-v1.1.0.patch
+git commit -a -m 'submod/go.mod: add require vgotest1 v1.1.0'
+git tag submod/v1.0.5
+
+# 11
+at 2018-02-19T18:13:36-05:00
+git apply ../0002-go.mod-add-require-submod-v1.0.5.patch
+git commit -a -m 'go.mod: add require submod v1.0.5'
+git tag v1.1.0
+
+git checkout master
+
+# 12
+at 2018-02-19T17:23:01-05:00
+mkdir pkg
+echo 'package pkg'
+cp stdout pkg/p.go
+git add pkg/p.go
+git commit -m 'pkg: add'
+
+# 13
+at 2018-02-19T17:30:23-05:00
+env GIT_AUTHOR_DATE=2018-02-19T17:24:48-05:00
+echo 'module "github.com/vgotest1/v2"'
+cp stdout go.mod
+git add go.mod
+git commit -m 'add go.mod'
+
+# 14
+at 2018-02-19T17:30:45-05:00
+echo 'module "github.com/vgotest1"'
+cp stdout go.mod
+git add go.mod
+git commit -m 'bad mod path'
+
+# 15
+at 2018-02-19T17:31:34-05:00
+mkdir v2
+echo 'module "github.com/vgotest1/v2"'
+cp stdout v2/go.mod
+git add v2/go.mod
+git commit -m 'add v2/go.mod'
+
+# 16
+at 2018-02-19T17:32:37-05:00
+echo 'module "github.com/vgotest1/v2"'
+cp stdout go.mod
+git add go.mod
+git commit -m 'say v2 in root go.mod'
+
+# 17
+at 2018-02-19T17:51:24-05:00
+	# README.md at this commit lacked a trailing newline, so 'git apply' can't
+	# seem to apply it correctly as a patch. Instead, we use 'unquote' to write
+	# the exact contents.
+unquote 'This is a test repo for versioned go.\nThere''s nothing useful here.\n\n	v0.0.0 - has pkg/p.go\n	v0.0.1 - has go.mod\n	\n	v1.0.0 - has pkg/p.go\n	v1.0.1 - has go.mod\n	v1.0.2 - has submod/pkg/p.go\n	v1.0.3 - has submod/go.mod\n	submod/v1.0.4 - same\n	submod/v1.0.5 - add requirement on v1.1.0\n	v1.1.0 - add requirement on submod/v1.0.5\n	\n	v2.0.0 - has pkg/p.go\n	v2.0.1 - has go.mod with v2 module path\n	v2.0.2 - has go.mod with v1 (no version) module path\n	v2.0.3 - has v2/go.mod with v2 module path\n	v2.0.5 - has go.mod AND v2/go.mod with v2 module path\n	'
+cp stdout README.md
+mkdir v2/pkg
+echo 'package q'
+cp stdout v2/pkg/q.go
+git add README.md v2/pkg/q.go
+git commit -m 'add q'
+git tag v2.0.6
+
+cd ..
+
+hg init
+hg convert ./git .
+rm ./git
+
+# Note: commit #18 is an 'update tags' commit automatically generated by 'hg
+# convert'. We have no control over its timestamp, so it and its descendent
+# commit #19 both end up with unpredictable commit hashes.
+#
+# Fortunately, these commits don't seem to matter for the purpose of reproducing
+# the final branches and heads from the original copy of this repo.
+
+# 19
+hg update -C -r 18
+hg tag --user 'Russ Cox ' --date '2018-07-18T21:24:45-04:00' -m 'Removed tag v2.0.0' --remove v2.0.0
+
+# 20
+hg branch default
+hg update -C -r 1
+echo 'v2'
+cp stdout v2
+hg add v2
+hg commit --user 'Russ Cox ' --date '2018-07-18T21:25:08-04:00' -m 'v2.0.0'
+
+# 21
+hg tag --user 'Russ Cox ' --date '2018-07-18T21:25:13-04:00' -r f0ababb31f75 -m 'Added tag v2.0.0 for changeset f0ababb31f75' v2.0.0
+
+# 22
+hg tag --user 'Russ Cox ' --date '2018-07-18T21:26:02-04:00' -m 'Removed tag v2.0.0' --remove v2.0.0
+
+# 23
+hg update -C -r 1
+echo 'v2'
+cp stdout v2
+hg add v2
+hg commit --user 'Russ Cox ' --date '2018-07-19T01:21:27+00:00' -m 'v2'
+
+# 24
+hg tag --user 'Russ Cox ' --date '2018-07-18T21:26:33-04:00' -m 'Added tag v2.0.0 for changeset 814fce58e83a' -r 814fce58e83a v2.0.0
+
+hg book --delete v1
+hg book --delete v2
+hg book --force -r 16 master
+
+hg log -G --debug
+
+hg tags
+cmp stdout .hg-tags
+hg branches
+cmp stdout .hg-branches
+hg bookmarks -l master mybranch
+cmp stdout .hg-bookmarks
+
+-- .hg-tags --
+tip                               24:645b06ca536d
+v2.0.0                            23:814fce58e83a
+v2.0.6                            17:3d4b89a2d059
+v1.1.0                            11:92c7eb888b4f
+submod/v1.0.5                     10:f3f560a6065c
+v1.0.3                             9:4e58084d459a
+submod/v1.0.4                      9:4e58084d459a
+v1.0.2                             8:3ccdce3897f9
+v1.0.1                             7:7890ea771ced
+v0.0.1                             7:7890ea771ced
+v2.0.5                             6:879ea98f7743
+v2.0.4                             5:bf6388016230
+v2.0.3                             4:a9ad6d1d14eb
+v2.0.2                             3:de3663002f0f
+v2.0.1                             2:f1fc0f22021b
+v1.0.0                             1:e125018e286a
+v0.0.0                             1:e125018e286a
+mytag                              1:e125018e286a
+-- .hg-branches --
+default                       24:645b06ca536d
+-- .hg-bookmarks --
+   master                    16:577bde103b24
+   mybranch                  1:e125018e286a
+-- git/LICENSE --
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-- git/README.md --
+This is a test repo for versioned go.
+There's nothing useful here.
+-- 0001-submod-go.mod-add-require-vgotest1-v1.1.0.patch --
+From 70fd92eaa4dacf82548d0c6099f5b853ae2c1fc8 Mon Sep 17 00:00:00 2001
+From: Russ Cox 
+Date: Mon, 19 Feb 2018 18:12:59 -0500
+Subject: [PATCH] submod/go.mod: add require vgotest1 v1.1.0
+
+---
+ submod/go.mod | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/submod/go.mod b/submod/go.mod
+index 7b18d93..c88de0f 100644
+--- a/submod/go.mod
++++ b/submod/go.mod
+@@ -1 +1,2 @@
+ module "github.com/vgotest1/submod" // submod/go.mod
++require "github.com/vgotest1" v1.1.0
+-- 
+2.36.1.838.g23b219f8e3
+-- 0002-go.mod-add-require-submod-v1.0.5.patch --
+From b769f2de407a4db81af9c5de0a06016d60d2ea09 Mon Sep 17 00:00:00 2001
+From: Russ Cox 
+Date: Mon, 19 Feb 2018 18:13:36 -0500
+Subject: [PATCH] go.mod: add require submod v1.0.5
+
+---
+ go.mod | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/go.mod b/go.mod
+index ac7a6d7..6118671 100644
+--- a/go.mod
++++ b/go.mod
+@@ -1 +1,2 @@
+ module "github.com/rsc/vgotest1" // root go.mod
++require "github.com/rsc/vgotest1/submod" v1.0.5
+-- 
+2.36.1.838.g23b219f8e3