Source file src/cmd/go/internal/modfetch/coderepo_test.go

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modfetch
     6  
     7  import (
     8  	"archive/zip"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/hex"
    12  	"flag"
    13  	"hash"
    14  	"internal/testenv"
    15  	"io"
    16  	"log"
    17  	"os"
    18  	"path/filepath"
    19  	"reflect"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	"cmd/go/internal/cfg"
    25  	"cmd/go/internal/modfetch/codehost"
    26  	"cmd/go/internal/vcweb/vcstest"
    27  
    28  	"golang.org/x/mod/sumdb/dirhash"
    29  )
    30  
    31  func TestMain(m *testing.M) {
    32  	flag.Parse()
    33  	if err := testMain(m); err != nil {
    34  		log.Fatal(err)
    35  	}
    36  }
    37  
    38  func testMain(m *testing.M) (err error) {
    39  
    40  	cfg.GOPROXY = "direct"
    41  
    42  	// The sum database is populated using a released version of the go command,
    43  	// but this test may include fixes for additional modules that previously
    44  	// could not be fetched. Since this test isn't executing any of the resolved
    45  	// code, bypass the sum database.
    46  	cfg.GOSUMDB = "off"
    47  
    48  	dir, err := os.MkdirTemp("", "gitrepo-test-")
    49  	if err != nil {
    50  		return err
    51  	}
    52  	defer func() {
    53  		if rmErr := os.RemoveAll(dir); err == nil {
    54  			err = rmErr
    55  		}
    56  	}()
    57  
    58  	cfg.GOMODCACHE = filepath.Join(dir, "modcache")
    59  	if err := os.Mkdir(cfg.GOMODCACHE, 0755); err != nil {
    60  		return err
    61  	}
    62  
    63  	srv, err := vcstest.NewServer()
    64  	if err != nil {
    65  		return err
    66  	}
    67  	defer func() {
    68  		if closeErr := srv.Close(); err == nil {
    69  			err = closeErr
    70  		}
    71  	}()
    72  
    73  	m.Run()
    74  	return nil
    75  }
    76  
    77  const (
    78  	vgotest1git = "github.com/rsc/vgotest1"
    79  	vgotest1hg  = "vcs-test.golang.org/hg/vgotest1.hg"
    80  )
    81  
    82  var altVgotests = map[string]string{
    83  	"hg": vgotest1hg,
    84  }
    85  
    86  type codeRepoTest struct {
    87  	vcs         string
    88  	path        string
    89  	mpath       string
    90  	rev         string
    91  	err         string
    92  	version     string
    93  	name        string
    94  	short       string
    95  	time        time.Time
    96  	gomod       string
    97  	gomodErr    string
    98  	zip         []string
    99  	zipErr      string
   100  	zipSum      string
   101  	zipFileHash string
   102  }
   103  
   104  var codeRepoTests = []codeRepoTest{
   105  	{
   106  		vcs:     "git",
   107  		path:    "github.com/rsc/vgotest1",
   108  		rev:     "v0.0.0",
   109  		version: "v0.0.0",
   110  		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
   111  		short:   "80d85c5d4d17",
   112  		time:    time.Date(2018, 2, 19, 23, 10, 6, 0, time.UTC),
   113  		zip: []string{
   114  			"LICENSE",
   115  			"README.md",
   116  			"pkg/p.go",
   117  		},
   118  		zipSum:      "h1:zVEjciLdlk/TPWCOyZo7k24T+tOKRQC+u8MKq/xS80I=",
   119  		zipFileHash: "738a00ddbfe8c329dce6b48e1f23c8e22a92db50f3cfb2653caa0d62676bc09c",
   120  	},
   121  	{
   122  		vcs:     "git",
   123  		path:    "github.com/rsc/vgotest1",
   124  		rev:     "v0.0.0-20180219231006-80d85c5d4d17",
   125  		version: "v0.0.0-20180219231006-80d85c5d4d17",
   126  		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
   127  		short:   "80d85c5d4d17",
   128  		time:    time.Date(2018, 2, 19, 23, 10, 6, 0, time.UTC),
   129  		zip: []string{
   130  			"LICENSE",
   131  			"README.md",
   132  			"pkg/p.go",
   133  		},
   134  		zipSum:      "h1:nOznk2xKsLGkTnXe0q9t1Ewt9jxK+oadtafSUqHM3Ec=",
   135  		zipFileHash: "bacb08f391e29d2eaaef8281b5c129ee6d890e608ee65877e0003c0181a766c8",
   136  	},
   137  	{
   138  		vcs:  "git",
   139  		path: "github.com/rsc/vgotest1",
   140  		rev:  "v0.0.1-0.20180219231006-80d85c5d4d17",
   141  		err:  `github.com/rsc/vgotest1@v0.0.1-0.20180219231006-80d85c5d4d17: invalid pseudo-version: tag (v0.0.0) found on revision 80d85c5d4d17 is already canonical, so should not be replaced with a pseudo-version derived from that tag`,
   142  	},
   143  	{
   144  		vcs:     "git",
   145  		path:    "github.com/rsc/vgotest1",
   146  		rev:     "v1.0.0",
   147  		version: "v1.0.0",
   148  		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
   149  		short:   "80d85c5d4d17",
   150  		time:    time.Date(2018, 2, 19, 23, 10, 6, 0, time.UTC),
   151  		zip: []string{
   152  			"LICENSE",
   153  			"README.md",
   154  			"pkg/p.go",
   155  		},
   156  		zipSum:      "h1:e040hOoWGeuJLawDjK9DW6med+cz9FxMFYDMOVG8ctQ=",
   157  		zipFileHash: "74caab65cfbea427c341fa815f3bb0378681d8f0e3cf62a7f207014263ec7be3",
   158  	},
   159  	{
   160  		vcs:     "git",
   161  		path:    "github.com/rsc/vgotest1/v2",
   162  		rev:     "v2.0.0",
   163  		version: "v2.0.0",
   164  		name:    "45f53230a74ad275c7127e117ac46914c8126160",
   165  		short:   "45f53230a74a",
   166  		time:    time.Date(2018, 7, 19, 1, 21, 27, 0, time.UTC),
   167  		err:     "missing github.com/rsc/vgotest1/go.mod and .../v2/go.mod at revision v2.0.0",
   168  	},
   169  	{
   170  		vcs:     "git",
   171  		path:    "github.com/rsc/vgotest1",
   172  		rev:     "80d85c5",
   173  		version: "v1.0.0",
   174  		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
   175  		short:   "80d85c5d4d17",
   176  		time:    time.Date(2018, 2, 19, 23, 10, 6, 0, time.UTC),
   177  		zip: []string{
   178  			"LICENSE",
   179  			"README.md",
   180  			"pkg/p.go",
   181  		},
   182  		zipSum:      "h1:e040hOoWGeuJLawDjK9DW6med+cz9FxMFYDMOVG8ctQ=",
   183  		zipFileHash: "74caab65cfbea427c341fa815f3bb0378681d8f0e3cf62a7f207014263ec7be3",
   184  	},
   185  	{
   186  		vcs:     "git",
   187  		path:    "github.com/rsc/vgotest1",
   188  		rev:     "mytag",
   189  		version: "v1.0.0",
   190  		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
   191  		short:   "80d85c5d4d17",
   192  		time:    time.Date(2018, 2, 19, 23, 10, 6, 0, time.UTC),
   193  		zip: []string{
   194  			"LICENSE",
   195  			"README.md",
   196  			"pkg/p.go",
   197  		},
   198  	},
   199  	{
   200  		vcs:     "git",
   201  		path:    "github.com/rsc/vgotest1/v2",
   202  		rev:     "45f53230a",
   203  		version: "v2.0.0",
   204  		name:    "45f53230a74ad275c7127e117ac46914c8126160",
   205  		short:   "45f53230a74a",
   206  		time:    time.Date(2018, 7, 19, 1, 21, 27, 0, time.UTC),
   207  		err:     "missing github.com/rsc/vgotest1/go.mod and .../v2/go.mod at revision v2.0.0",
   208  	},
   209  	{
   210  		vcs:     "git",
   211  		path:    "github.com/rsc/vgotest1/v54321",
   212  		rev:     "80d85c5",
   213  		version: "v54321.0.0-20180219231006-80d85c5d4d17",
   214  		name:    "80d85c5d4d17598a0e9055e7c175a32b415d6128",
   215  		short:   "80d85c5d4d17",
   216  		time:    time.Date(2018, 2, 19, 23, 10, 6, 0, time.UTC),
   217  		err:     "missing github.com/rsc/vgotest1/go.mod and .../v54321/go.mod at revision 80d85c5d4d17",
   218  	},
   219  	{
   220  		vcs:  "git",
   221  		path: "github.com/rsc/vgotest1/submod",
   222  		rev:  "v1.0.0",
   223  		err:  "unknown revision submod/v1.0.0",
   224  	},
   225  	{
   226  		vcs:  "git",
   227  		path: "github.com/rsc/vgotest1/submod",
   228  		rev:  "v1.0.3",
   229  		err:  "unknown revision submod/v1.0.3",
   230  	},
   231  	{
   232  		vcs:     "git",
   233  		path:    "github.com/rsc/vgotest1/submod",
   234  		rev:     "v1.0.4",
   235  		version: "v1.0.4",
   236  		name:    "8afe2b2efed96e0880ecd2a69b98a53b8c2738b6",
   237  		short:   "8afe2b2efed9",
   238  		time:    time.Date(2018, 2, 19, 23, 12, 7, 0, time.UTC),
   239  		gomod:   "module \"github.com/vgotest1/submod\" // submod/go.mod\n",
   240  		zip: []string{
   241  			"go.mod",
   242  			"pkg/p.go",
   243  			"LICENSE",
   244  		},
   245  		zipSum:      "h1:iMsJ/9uQsk6MnZNnJK311f11QiSlmN92Q2aSjCywuJY=",
   246  		zipFileHash: "95801bfa69c5197ae809af512946d22f22850068527cd78100ae3f176bc8043b",
   247  	},
   248  	{
   249  		vcs:     "git",
   250  		path:    "github.com/rsc/vgotest1",
   251  		rev:     "v1.1.0",
   252  		version: "v1.1.0",
   253  		name:    "b769f2de407a4db81af9c5de0a06016d60d2ea09",
   254  		short:   "b769f2de407a",
   255  		time:    time.Date(2018, 2, 19, 23, 13, 36, 0, time.UTC),
   256  		gomod:   "module \"github.com/rsc/vgotest1\" // root go.mod\nrequire \"github.com/rsc/vgotest1/submod\" v1.0.5\n",
   257  		zip: []string{
   258  			"LICENSE",
   259  			"README.md",
   260  			"go.mod",
   261  			"pkg/p.go",
   262  		},
   263  		zipSum:      "h1:M69k7q+8bQ+QUpHov45Z/NoR8rj3DsQJUnXLWvf01+Q=",
   264  		zipFileHash: "58af45fb248d320ea471f568e006379e2b8d71d6d1663f9b19b2e00fd9ac9265",
   265  	},
   266  	{
   267  		vcs:         "git",
   268  		path:        "github.com/rsc/vgotest1/v2",
   269  		rev:         "v2.0.1",
   270  		version:     "v2.0.1",
   271  		name:        "ea65f87c8f52c15ea68f3bdd9925ef17e20d91e9",
   272  		short:       "ea65f87c8f52",
   273  		time:        time.Date(2018, 2, 19, 23, 14, 23, 0, time.UTC),
   274  		gomod:       "module \"github.com/rsc/vgotest1/v2\" // root go.mod\n",
   275  		zipSum:      "h1:QmgYy/zt+uoWhDpcsgrSVzYFvKtBEjl5zT/FRz9GTzA=",
   276  		zipFileHash: "1aedf1546d322a0121879ddfd6d0e8bfbd916d2cafbeb538ddb440e04b04b9ef",
   277  	},
   278  	{
   279  		vcs:     "git",
   280  		path:    "github.com/rsc/vgotest1/v2",
   281  		rev:     "v2.0.3",
   282  		version: "v2.0.3",
   283  		name:    "f18795870fb14388a21ef3ebc1d75911c8694f31",
   284  		short:   "f18795870fb1",
   285  		time:    time.Date(2018, 2, 19, 23, 16, 4, 0, time.UTC),
   286  		err:     "github.com/rsc/vgotest1/v2/go.mod has non-.../v2 module path \"github.com/rsc/vgotest\" at revision v2.0.3",
   287  	},
   288  	{
   289  		vcs:     "git",
   290  		path:    "github.com/rsc/vgotest1/v2",
   291  		rev:     "v2.0.4",
   292  		version: "v2.0.4",
   293  		name:    "1f863feb76bc7029b78b21c5375644838962f88d",
   294  		short:   "1f863feb76bc",
   295  		time:    time.Date(2018, 2, 20, 0, 3, 38, 0, time.UTC),
   296  		err:     "github.com/rsc/vgotest1/go.mod and .../v2/go.mod both have .../v2 module paths at revision v2.0.4",
   297  	},
   298  	{
   299  		vcs:         "git",
   300  		path:        "github.com/rsc/vgotest1/v2",
   301  		rev:         "v2.0.5",
   302  		version:     "v2.0.5",
   303  		name:        "2f615117ce481c8efef46e0cc0b4b4dccfac8fea",
   304  		short:       "2f615117ce48",
   305  		time:        time.Date(2018, 2, 20, 0, 3, 59, 0, time.UTC),
   306  		gomod:       "module \"github.com/rsc/vgotest1/v2\" // v2/go.mod\n",
   307  		zipSum:      "h1:RIEb9q1SUSEQOzMn0zfl/LQxGFWlhWEAdeEguf1MLGU=",
   308  		zipFileHash: "7d92c2c328c5e9b0694101353705d5843746ec1d93a1e986d0da54c8a14dfe6d",
   309  	},
   310  	{
   311  		// redirect to github
   312  		vcs:         "git",
   313  		path:        "rsc.io/quote",
   314  		rev:         "v1.0.0",
   315  		version:     "v1.0.0",
   316  		name:        "f488df80bcdbd3e5bafdc24ad7d1e79e83edd7e6",
   317  		short:       "f488df80bcdb",
   318  		time:        time.Date(2018, 2, 14, 0, 45, 20, 0, time.UTC),
   319  		gomod:       "module \"rsc.io/quote\"\n",
   320  		zipSum:      "h1:haUSojyo3j2M9g7CEUFG8Na09dtn7QKxvPGaPVQdGwM=",
   321  		zipFileHash: "5c08ba2c09a364f93704aaa780e7504346102c6ef4fe1333a11f09904a732078",
   322  	},
   323  	{
   324  		// redirect to static hosting proxy
   325  		vcs:     "mod",
   326  		path:    "swtch.com/testmod",
   327  		rev:     "v1.0.0",
   328  		version: "v1.0.0",
   329  		// NO name or short - we intentionally ignore those in the proxy protocol
   330  		time:  time.Date(1972, 7, 18, 12, 34, 56, 0, time.UTC),
   331  		gomod: "module \"swtch.com/testmod\"\n",
   332  	},
   333  	{
   334  		// redirect to googlesource
   335  		vcs:         "git",
   336  		path:        "golang.org/x/text",
   337  		rev:         "4e4a3210bb",
   338  		version:     "v0.3.1-0.20180208041248-4e4a3210bb54",
   339  		name:        "4e4a3210bb54bb31f6ab2cdca2edcc0b50c420c1",
   340  		short:       "4e4a3210bb54",
   341  		time:        time.Date(2018, 2, 8, 4, 12, 48, 0, time.UTC),
   342  		zipSum:      "h1:Yxu6pHX9X2RECiuw/Q5/4uvajuaowck8zOFKXgbfNBk=",
   343  		zipFileHash: "ac2c165a5c10aa5a7545dea60a08e019270b982fa6c8bdcb5943931de64922fe",
   344  	},
   345  	{
   346  		vcs:         "git",
   347  		path:        "github.com/pkg/errors",
   348  		rev:         "v0.8.0",
   349  		version:     "v0.8.0",
   350  		name:        "645ef00459ed84a119197bfb8d8205042c6df63d",
   351  		short:       "645ef00459ed",
   352  		time:        time.Date(2016, 9, 29, 1, 48, 1, 0, time.UTC),
   353  		zipSum:      "h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=",
   354  		zipFileHash: "e4fa69ba057356614edbc1da881a7d3ebb688505be49f65965686bcb859e2fae",
   355  	},
   356  	{
   357  		// package in subdirectory - custom domain
   358  		// In general we can't reject these definitively in Lookup,
   359  		// but gopkg.in is special.
   360  		vcs:  "git",
   361  		path: "gopkg.in/yaml.v2/abc",
   362  		err:  "invalid module path \"gopkg.in/yaml.v2/abc\"",
   363  	},
   364  	{
   365  		// package in subdirectory - github
   366  		// Because it's a package, Stat should fail entirely.
   367  		vcs:  "git",
   368  		path: "github.com/rsc/quote/buggy",
   369  		rev:  "c4d4236f",
   370  		err:  "missing github.com/rsc/quote/buggy/go.mod at revision c4d4236f9242",
   371  	},
   372  	{
   373  		vcs:         "git",
   374  		path:        "gopkg.in/yaml.v2",
   375  		rev:         "d670f940",
   376  		version:     "v2.0.0",
   377  		name:        "d670f9405373e636a5a2765eea47fac0c9bc91a4",
   378  		short:       "d670f9405373",
   379  		time:        time.Date(2018, 1, 9, 11, 43, 31, 0, time.UTC),
   380  		gomod:       "module gopkg.in/yaml.v2\n",
   381  		zipSum:      "h1:uUkhRGrsEyx/laRdeS6YIQKIys8pg+lRSRdVMTYjivs=",
   382  		zipFileHash: "7b0a141b1b0b49772ab4eecfd11dfd6609a94a5e868cab04a3abb1861ffaa877",
   383  	},
   384  	{
   385  		vcs:         "git",
   386  		path:        "gopkg.in/check.v1",
   387  		rev:         "20d25e280405",
   388  		version:     "v1.0.0-20161208181325-20d25e280405",
   389  		name:        "20d25e2804050c1cd24a7eea1e7a6447dd0e74ec",
   390  		short:       "20d25e280405",
   391  		time:        time.Date(2016, 12, 8, 18, 13, 25, 0, time.UTC),
   392  		gomod:       "module gopkg.in/check.v1\n",
   393  		zipSum:      "h1:829vOVxxusYHC+IqBtkX5mbKtsY9fheQiQn0MZRVLfQ=",
   394  		zipFileHash: "9e7cb3f4f1e66d722306442b0dbe1f6f43d74d1736d54c510537bdfb1d6f432f",
   395  	},
   396  	{
   397  		vcs:         "git",
   398  		path:        "vcs-test.golang.org/go/mod/gitrepo1",
   399  		rev:         "master",
   400  		version:     "v1.2.4-annotated",
   401  		name:        "ede458df7cd0fdca520df19a33158086a8a68e81",
   402  		short:       "ede458df7cd0",
   403  		time:        time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
   404  		gomod:       "module vcs-test.golang.org/go/mod/gitrepo1\n",
   405  		zipSum:      "h1:YJYZRsM9BHFTlVr8YADjT0cJH8uFIDtoc5NLiVqZEx8=",
   406  		zipFileHash: "c15e49d58b7a4c37966cbe5bc01a0330cd5f2927e990e1839bda1d407766d9c5",
   407  	},
   408  	{
   409  		vcs:  "git",
   410  		path: "gopkg.in/natefinch/lumberjack.v2",
   411  		// This repo has a v2.1 tag.
   412  		// We only allow semver references to tags that are fully qualified, as in v2.1.0.
   413  		// Because we can't record v2.1.0 (the actual tag is v2.1), we record a pseudo-version
   414  		// instead, same as if the tag were any other non-version-looking string.
   415  		// We use a v2 pseudo-version here because of the .v2 in the path, not because
   416  		// of the v2 in the rev.
   417  		rev:     "v2.1", // non-canonical semantic version turns into pseudo-version
   418  		version: "v2.0.0-20170531160350-a96e63847dc3",
   419  		name:    "a96e63847dc3c67d17befa69c303767e2f84e54f",
   420  		short:   "a96e63847dc3",
   421  		time:    time.Date(2017, 5, 31, 16, 3, 50, 0, time.UTC),
   422  		gomod:   "module gopkg.in/natefinch/lumberjack.v2\n",
   423  	},
   424  	{
   425  		vcs:         "git",
   426  		path:        "vcs-test.golang.org/go/v2module/v2",
   427  		rev:         "v2.0.0",
   428  		version:     "v2.0.0",
   429  		name:        "203b91c896acd173aa719e4cdcb7d463c4b090fa",
   430  		short:       "203b91c896ac",
   431  		time:        time.Date(2019, 4, 3, 15, 52, 15, 0, time.UTC),
   432  		gomod:       "module vcs-test.golang.org/go/v2module/v2\n\ngo 1.12\n",
   433  		zipSum:      "h1:JItBZ+gwA5WvtZEGEbuDL4lUttGtLrs53lmdurq3bOg=",
   434  		zipFileHash: "9ea9ae1673cffcc44b7fdd3cc89953d68c102449b46c982dbf085e4f2e394da5",
   435  	},
   436  	{
   437  		// Git branch with a semver name, +incompatible version, and no go.mod file.
   438  		vcs:  "git",
   439  		path: "vcs-test.golang.org/go/mod/gitrepo1",
   440  		rev:  "v2.3.4+incompatible",
   441  		err:  `resolves to version v2.0.1+incompatible (v2.3.4 is not a tag)`,
   442  	},
   443  	{
   444  		// Git branch with a semver name, matching go.mod file, and compatible version.
   445  		vcs:  "git",
   446  		path: "vcs-test.golang.org/git/semver-branch.git",
   447  		rev:  "v1.0.0",
   448  		err:  `resolves to version v0.1.1-0.20220202191944-09c4d8f6938c (v1.0.0 is not a tag)`,
   449  	},
   450  	{
   451  		// Git branch with a semver name, matching go.mod file, and disallowed +incompatible version.
   452  		// The version/tag mismatch takes precedence over the +incompatible mismatched.
   453  		vcs:  "git",
   454  		path: "vcs-test.golang.org/git/semver-branch.git",
   455  		rev:  "v2.0.0+incompatible",
   456  		err:  `resolves to version v0.1.0 (v2.0.0 is not a tag)`,
   457  	},
   458  	{
   459  		// Git branch with a semver name, matching go.mod file, and mismatched version.
   460  		// The version/tag mismatch takes precedence over the +incompatible mismatched.
   461  		vcs:  "git",
   462  		path: "vcs-test.golang.org/git/semver-branch.git",
   463  		rev:  "v2.0.0",
   464  		err:  `resolves to version v0.1.0 (v2.0.0 is not a tag)`,
   465  	},
   466  	{
   467  		// v3.0.0-devel is the same as tag v4.0.0-beta.1, but v4.0.0-beta.1 would
   468  		// not be allowed because it is incompatible and a go.mod file exists.
   469  		// The error message should refer to a valid pseudo-version, not the
   470  		// unusable semver tag.
   471  		vcs:  "git",
   472  		path: "vcs-test.golang.org/git/semver-branch.git",
   473  		rev:  "v3.0.0-devel",
   474  		err:  `resolves to version v0.1.1-0.20220203155313-d59622f6e4d7 (v3.0.0-devel is not a tag)`,
   475  	},
   476  
   477  	// If v2/go.mod exists, then we should prefer to match the "v2"
   478  	// pseudo-versions to the nested module, and resolve the module in the parent
   479  	// directory to only compatible versions.
   480  	//
   481  	// However (https://go.dev/issue/51324), previous versions of the 'go' command
   482  	// didn't always do so, so if the user explicitly requests a +incompatible
   483  	// version (as would be present in an existing go.mod file), we should
   484  	// continue to allow it.
   485  	{
   486  		vcs:     "git",
   487  		path:    "vcs-test.golang.org/git/v2sub.git",
   488  		rev:     "80beb17a1603",
   489  		version: "v0.0.0-20220222205507-80beb17a1603",
   490  		name:    "80beb17a16036f17a5aedd1bb5bd6d407b3c6dc5",
   491  		short:   "80beb17a1603",
   492  		time:    time.Date(2022, 2, 22, 20, 55, 7, 0, time.UTC),
   493  	},
   494  	{
   495  		vcs:  "git",
   496  		path: "vcs-test.golang.org/git/v2sub.git",
   497  		rev:  "v2.0.0",
   498  		err:  `module contains a go.mod file, so module path must match major version ("vcs-test.golang.org/git/v2sub.git/v2")`,
   499  	},
   500  	{
   501  		vcs:  "git",
   502  		path: "vcs-test.golang.org/git/v2sub.git",
   503  		rev:  "v2.0.1-0.20220222205507-80beb17a1603",
   504  		err:  `module contains a go.mod file, so module path must match major version ("vcs-test.golang.org/git/v2sub.git/v2")`,
   505  	},
   506  	{
   507  		vcs:     "git",
   508  		path:    "vcs-test.golang.org/git/v2sub.git",
   509  		rev:     "v2.0.0+incompatible",
   510  		version: "v2.0.0+incompatible",
   511  		name:    "5fcd3eaeeb391d399f562fd45a50dac9fc34ae8b",
   512  		short:   "5fcd3eaeeb39",
   513  		time:    time.Date(2022, 2, 22, 20, 53, 33, 0, time.UTC),
   514  	},
   515  	{
   516  		vcs:     "git",
   517  		path:    "vcs-test.golang.org/git/v2sub.git",
   518  		rev:     "v2.0.1-0.20220222205507-80beb17a1603+incompatible",
   519  		version: "v2.0.1-0.20220222205507-80beb17a1603+incompatible",
   520  		name:    "80beb17a16036f17a5aedd1bb5bd6d407b3c6dc5",
   521  		short:   "80beb17a1603",
   522  		time:    time.Date(2022, 2, 22, 20, 55, 7, 0, time.UTC),
   523  	},
   524  
   525  	// A version tag with explicit build metadata is valid but not canonical.
   526  	// It should resolve to a pseudo-version based on the same tag.
   527  	{
   528  		vcs:     "git",
   529  		path:    "vcs-test.golang.org/git/odd-tags.git",
   530  		rev:     "v0.1.0+build-metadata",
   531  		version: "v0.1.1-0.20220223184835-9d863d525bbf",
   532  		name:    "9d863d525bbfcc8eda09364738c4032393711a56",
   533  		short:   "9d863d525bbf",
   534  		time:    time.Date(2022, 2, 23, 18, 48, 35, 0, time.UTC),
   535  	},
   536  	{
   537  		vcs:     "git",
   538  		path:    "vcs-test.golang.org/git/odd-tags.git",
   539  		rev:     "9d863d525bbf",
   540  		version: "v0.1.1-0.20220223184835-9d863d525bbf",
   541  		name:    "9d863d525bbfcc8eda09364738c4032393711a56",
   542  		short:   "9d863d525bbf",
   543  		time:    time.Date(2022, 2, 23, 18, 48, 35, 0, time.UTC),
   544  	},
   545  	{
   546  		vcs:     "git",
   547  		path:    "vcs-test.golang.org/git/odd-tags.git",
   548  		rev:     "latest",
   549  		version: "v0.1.1-0.20220223184835-9d863d525bbf",
   550  		name:    "9d863d525bbfcc8eda09364738c4032393711a56",
   551  		short:   "9d863d525bbf",
   552  		time:    time.Date(2022, 2, 23, 18, 48, 35, 0, time.UTC),
   553  	},
   554  
   555  	// A version tag with an erroneous "+incompatible" suffix should resolve using
   556  	// only the prefix before the "+incompatible" suffix, not the "+incompatible"
   557  	// tag itself. (Otherwise, we would potentially have two different commits
   558  	// both named "v2.0.0+incompatible".) However, the tag is still valid semver
   559  	// and can still be used as the base for an unambiguous pseudo-version.
   560  	{
   561  		vcs:  "git",
   562  		path: "vcs-test.golang.org/git/odd-tags.git",
   563  		rev:  "v2.0.0+incompatible",
   564  		err:  `unknown revision v2.0.0`,
   565  	},
   566  	{
   567  		vcs:     "git",
   568  		path:    "vcs-test.golang.org/git/odd-tags.git",
   569  		rev:     "12d19af20458",
   570  		version: "v2.0.1-0.20220223184802-12d19af20458+incompatible",
   571  		name:    "12d19af204585b0db3d2a876ceddf5b9323f5a4a",
   572  		short:   "12d19af20458",
   573  		time:    time.Date(2022, 2, 23, 18, 48, 2, 0, time.UTC),
   574  	},
   575  
   576  	// Similarly, a pseudo-version must resolve to the named commit, even if a tag
   577  	// matching that pseudo-version is present on a *different* commit.
   578  	{
   579  		vcs:     "git",
   580  		path:    "vcs-test.golang.org/git/odd-tags.git",
   581  		rev:     "v3.0.0-20220223184802-12d19af20458",
   582  		version: "v3.0.0-20220223184802-12d19af20458+incompatible",
   583  		name:    "12d19af204585b0db3d2a876ceddf5b9323f5a4a",
   584  		short:   "12d19af20458",
   585  		time:    time.Date(2022, 2, 23, 18, 48, 2, 0, time.UTC),
   586  	},
   587  }
   588  
   589  func TestCodeRepo(t *testing.T) {
   590  	testenv.MustHaveExternalNetwork(t)
   591  	tmpdir := t.TempDir()
   592  
   593  	for _, tt := range codeRepoTests {
   594  		f := func(tt codeRepoTest) func(t *testing.T) {
   595  			return func(t *testing.T) {
   596  				if strings.Contains(tt.path, "gopkg.in") {
   597  					testenv.SkipFlaky(t, 54503)
   598  				}
   599  
   600  				t.Parallel()
   601  				if tt.vcs != "mod" {
   602  					testenv.MustHaveExecPath(t, tt.vcs)
   603  				}
   604  				ctx := context.Background()
   605  
   606  				repo := Lookup(ctx, "direct", tt.path)
   607  
   608  				if tt.mpath == "" {
   609  					tt.mpath = tt.path
   610  				}
   611  				if mpath := repo.ModulePath(); mpath != tt.mpath {
   612  					t.Errorf("repo.ModulePath() = %q, want %q", mpath, tt.mpath)
   613  				}
   614  
   615  				info, err := repo.Stat(ctx, tt.rev)
   616  				if err != nil {
   617  					if tt.err != "" {
   618  						if !strings.Contains(err.Error(), tt.err) {
   619  							t.Fatalf("repoStat(%q): %v, wanted %q", tt.rev, err, tt.err)
   620  						}
   621  						return
   622  					}
   623  					t.Fatalf("repo.Stat(%q): %v", tt.rev, err)
   624  				}
   625  				if tt.err != "" {
   626  					t.Errorf("repo.Stat(%q): success, wanted error", tt.rev)
   627  				}
   628  				if info.Version != tt.version {
   629  					t.Errorf("info.Version = %q, want %q", info.Version, tt.version)
   630  				}
   631  				if info.Name != tt.name {
   632  					t.Errorf("info.Name = %q, want %q", info.Name, tt.name)
   633  				}
   634  				if info.Short != tt.short {
   635  					t.Errorf("info.Short = %q, want %q", info.Short, tt.short)
   636  				}
   637  				if !info.Time.Equal(tt.time) {
   638  					t.Errorf("info.Time = %v, want %v", info.Time, tt.time)
   639  				}
   640  
   641  				if tt.gomod != "" || tt.gomodErr != "" {
   642  					data, err := repo.GoMod(ctx, tt.version)
   643  					if err != nil && tt.gomodErr == "" {
   644  						t.Errorf("repo.GoMod(%q): %v", tt.version, err)
   645  					} else if err != nil && tt.gomodErr != "" {
   646  						if err.Error() != tt.gomodErr {
   647  							t.Errorf("repo.GoMod(%q): %v, want %q", tt.version, err, tt.gomodErr)
   648  						}
   649  					} else if tt.gomodErr != "" {
   650  						t.Errorf("repo.GoMod(%q) = %q, want error %q", tt.version, data, tt.gomodErr)
   651  					} else if string(data) != tt.gomod {
   652  						t.Errorf("repo.GoMod(%q) = %q, want %q", tt.version, data, tt.gomod)
   653  					}
   654  				}
   655  
   656  				needHash := !testing.Short() && (tt.zipFileHash != "" || tt.zipSum != "")
   657  				if tt.zip != nil || tt.zipErr != "" || needHash {
   658  					f, err := os.CreateTemp(tmpdir, tt.version+".zip.")
   659  					if err != nil {
   660  						t.Fatalf("os.CreateTemp: %v", err)
   661  					}
   662  					zipfile := f.Name()
   663  					defer func() {
   664  						f.Close()
   665  						os.Remove(zipfile)
   666  					}()
   667  
   668  					var w io.Writer
   669  					var h hash.Hash
   670  					if needHash {
   671  						h = sha256.New()
   672  						w = io.MultiWriter(f, h)
   673  					} else {
   674  						w = f
   675  					}
   676  					err = repo.Zip(ctx, w, tt.version)
   677  					f.Close()
   678  					if err != nil {
   679  						if tt.zipErr != "" {
   680  							if err.Error() == tt.zipErr {
   681  								return
   682  							}
   683  							t.Fatalf("repo.Zip(%q): %v, want error %q", tt.version, err, tt.zipErr)
   684  						}
   685  						t.Fatalf("repo.Zip(%q): %v", tt.version, err)
   686  					}
   687  					if tt.zipErr != "" {
   688  						t.Errorf("repo.Zip(%q): success, want error %q", tt.version, tt.zipErr)
   689  					}
   690  
   691  					if tt.zip != nil {
   692  						prefix := tt.path + "@" + tt.version + "/"
   693  						z, err := zip.OpenReader(zipfile)
   694  						if err != nil {
   695  							t.Fatalf("open zip %s: %v", zipfile, err)
   696  						}
   697  						var names []string
   698  						for _, file := range z.File {
   699  							if !strings.HasPrefix(file.Name, prefix) {
   700  								t.Errorf("zip entry %v does not start with prefix %v", file.Name, prefix)
   701  								continue
   702  							}
   703  							names = append(names, file.Name[len(prefix):])
   704  						}
   705  						z.Close()
   706  						if !reflect.DeepEqual(names, tt.zip) {
   707  							t.Fatalf("zip = %v\nwant %v\n", names, tt.zip)
   708  						}
   709  					}
   710  
   711  					if needHash {
   712  						sum, err := dirhash.HashZip(zipfile, dirhash.Hash1)
   713  						if err != nil {
   714  							t.Errorf("repo.Zip(%q): %v", tt.version, err)
   715  						} else if sum != tt.zipSum {
   716  							t.Errorf("repo.Zip(%q): got file with sum %q, want %q", tt.version, sum, tt.zipSum)
   717  						} else if zipFileHash := hex.EncodeToString(h.Sum(nil)); zipFileHash != tt.zipFileHash {
   718  							t.Errorf("repo.Zip(%q): got file with hash %q, want %q (but content has correct sum)", tt.version, zipFileHash, tt.zipFileHash)
   719  						}
   720  					}
   721  				}
   722  			}
   723  		}
   724  		t.Run(strings.ReplaceAll(tt.path, "/", "_")+"/"+tt.rev, f(tt))
   725  		if strings.HasPrefix(tt.path, vgotest1git) {
   726  			for vcs, alt := range altVgotests {
   727  				altTest := tt
   728  				altTest.vcs = vcs
   729  				altTest.path = alt + strings.TrimPrefix(altTest.path, vgotest1git)
   730  				if strings.HasPrefix(altTest.mpath, vgotest1git) {
   731  					altTest.mpath = alt + strings.TrimPrefix(altTest.mpath, vgotest1git)
   732  				}
   733  				var m map[string]string
   734  				if alt == vgotest1hg {
   735  					m = hgmap
   736  				}
   737  				altTest.version = remap(altTest.version, m)
   738  				altTest.name = remap(altTest.name, m)
   739  				altTest.short = remap(altTest.short, m)
   740  				altTest.rev = remap(altTest.rev, m)
   741  				altTest.err = remap(altTest.err, m)
   742  				altTest.gomodErr = remap(altTest.gomodErr, m)
   743  				altTest.zipErr = remap(altTest.zipErr, m)
   744  				altTest.zipSum = ""
   745  				altTest.zipFileHash = ""
   746  				t.Run(strings.ReplaceAll(altTest.path, "/", "_")+"/"+altTest.rev, f(altTest))
   747  			}
   748  		}
   749  	}
   750  }
   751  
   752  var hgmap = map[string]string{
   753  	"github.com/rsc/vgotest1":                  "vcs-test.golang.org/hg/vgotest1.hg",
   754  	"f18795870fb14388a21ef3ebc1d75911c8694f31": "a9ad6d1d14eb544f459f446210c7eb3b009807c6",
   755  	"ea65f87c8f52c15ea68f3bdd9925ef17e20d91e9": "f1fc0f22021b638d073d31c752847e7bf385def7",
   756  	"b769f2de407a4db81af9c5de0a06016d60d2ea09": "92c7eb888b4fac17f1c6bd2e1060a1b881a3b832",
   757  	"8afe2b2efed96e0880ecd2a69b98a53b8c2738b6": "4e58084d459ae7e79c8c2264d0e8e9a92eb5cd44",
   758  	"2f615117ce481c8efef46e0cc0b4b4dccfac8fea": "879ea98f7743c8eff54f59a918f3a24123d1cf46",
   759  	"80d85c5d4d17598a0e9055e7c175a32b415d6128": "e125018e286a4b09061079a81e7b537070b7ff71",
   760  	"1f863feb76bc7029b78b21c5375644838962f88d": "bf63880162304a9337477f3858f5b7e255c75459",
   761  	"45f53230a74ad275c7127e117ac46914c8126160": "814fce58e83abd5bf2a13892e0b0e1198abefcd4",
   762  }
   763  
   764  func remap(name string, m map[string]string) string {
   765  	if m[name] != "" {
   766  		return m[name]
   767  	}
   768  	if codehost.AllHex(name) {
   769  		for k, v := range m {
   770  			if strings.HasPrefix(k, name) {
   771  				return v[:len(name)]
   772  			}
   773  		}
   774  	}
   775  	for k, v := range m {
   776  		name = strings.ReplaceAll(name, k, v)
   777  		if codehost.AllHex(k) {
   778  			name = strings.ReplaceAll(name, k[:12], v[:12])
   779  		}
   780  	}
   781  	return name
   782  }
   783  
   784  var codeRepoVersionsTests = []struct {
   785  	vcs      string
   786  	path     string
   787  	prefix   string
   788  	versions []string
   789  }{
   790  	{
   791  		vcs:      "git",
   792  		path:     "github.com/rsc/vgotest1",
   793  		versions: []string{"v0.0.0", "v0.0.1", "v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.1.0"},
   794  	},
   795  	{
   796  		vcs:      "git",
   797  		path:     "github.com/rsc/vgotest1",
   798  		prefix:   "v1.0",
   799  		versions: []string{"v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3"},
   800  	},
   801  	{
   802  		vcs:      "git",
   803  		path:     "github.com/rsc/vgotest1/v2",
   804  		versions: []string{"v2.0.0", "v2.0.1", "v2.0.2", "v2.0.3", "v2.0.4", "v2.0.5", "v2.0.6"},
   805  	},
   806  	{
   807  		vcs:      "mod",
   808  		path:     "swtch.com/testmod",
   809  		versions: []string{"v1.0.0", "v1.1.1"},
   810  	},
   811  	{
   812  		vcs:      "git",
   813  		path:     "vcs-test.golang.org/git/odd-tags.git",
   814  		versions: nil,
   815  	},
   816  }
   817  
   818  func TestCodeRepoVersions(t *testing.T) {
   819  	testenv.MustHaveExternalNetwork(t)
   820  
   821  	for _, tt := range codeRepoVersionsTests {
   822  		tt := tt
   823  		t.Run(strings.ReplaceAll(tt.path, "/", "_"), func(t *testing.T) {
   824  			if strings.Contains(tt.path, "gopkg.in") {
   825  				testenv.SkipFlaky(t, 54503)
   826  			}
   827  
   828  			t.Parallel()
   829  			if tt.vcs != "mod" {
   830  				testenv.MustHaveExecPath(t, tt.vcs)
   831  			}
   832  			ctx := context.Background()
   833  
   834  			repo := Lookup(ctx, "direct", tt.path)
   835  			list, err := repo.Versions(ctx, tt.prefix)
   836  			if err != nil {
   837  				t.Fatalf("Versions(%q): %v", tt.prefix, err)
   838  			}
   839  			if !reflect.DeepEqual(list.List, tt.versions) {
   840  				t.Fatalf("Versions(%q):\nhave %v\nwant %v", tt.prefix, list, tt.versions)
   841  			}
   842  		})
   843  	}
   844  }
   845  
   846  var latestTests = []struct {
   847  	vcs     string
   848  	path    string
   849  	version string
   850  	err     string
   851  }{
   852  	{
   853  		vcs:  "git",
   854  		path: "github.com/rsc/empty",
   855  		err:  "no commits",
   856  	},
   857  	{
   858  		vcs:  "git",
   859  		path: "github.com/rsc/vgotest1",
   860  		err:  `github.com/rsc/vgotest1@v0.0.0-20180219223237-a08abb797a67: invalid version: go.mod has post-v0 module path "github.com/vgotest1/v2" at revision a08abb797a67`,
   861  	},
   862  	{
   863  		vcs:  "git",
   864  		path: "github.com/rsc/vgotest1/v2",
   865  		err:  `github.com/rsc/vgotest1/v2@v2.0.0-20180219223237-a08abb797a67: invalid version: github.com/rsc/vgotest1/go.mod and .../v2/go.mod both have .../v2 module paths at revision a08abb797a67`,
   866  	},
   867  	{
   868  		vcs:  "git",
   869  		path: "github.com/rsc/vgotest1/subdir",
   870  		err:  "github.com/rsc/vgotest1/subdir@v0.0.0-20180219223237-a08abb797a67: invalid version: missing github.com/rsc/vgotest1/subdir/go.mod at revision a08abb797a67",
   871  	},
   872  	{
   873  		vcs:     "git",
   874  		path:    "vcs-test.golang.org/git/commit-after-tag.git",
   875  		version: "v1.0.1-0.20190715211727-b325d8217783",
   876  	},
   877  	{
   878  		vcs:     "git",
   879  		path:    "vcs-test.golang.org/git/no-tags.git",
   880  		version: "v0.0.0-20190715212047-e706ba1d9f6d",
   881  	},
   882  	{
   883  		vcs:     "mod",
   884  		path:    "swtch.com/testmod",
   885  		version: "v1.1.1",
   886  	},
   887  }
   888  
   889  func TestLatest(t *testing.T) {
   890  	testenv.MustHaveExternalNetwork(t)
   891  
   892  	for _, tt := range latestTests {
   893  		name := strings.ReplaceAll(tt.path, "/", "_")
   894  		t.Run(name, func(t *testing.T) {
   895  			tt := tt
   896  			t.Parallel()
   897  			if tt.vcs != "mod" {
   898  				testenv.MustHaveExecPath(t, tt.vcs)
   899  			}
   900  			ctx := context.Background()
   901  
   902  			repo := Lookup(ctx, "direct", tt.path)
   903  			info, err := repo.Latest(ctx)
   904  			if err != nil {
   905  				if tt.err != "" {
   906  					if err.Error() == tt.err {
   907  						return
   908  					}
   909  					t.Fatalf("Latest(): %v, want %q", err, tt.err)
   910  				}
   911  				t.Fatalf("Latest(): %v", err)
   912  			}
   913  			if tt.err != "" {
   914  				t.Fatalf("Latest() = %v, want error %q", info.Version, tt.err)
   915  			}
   916  			if info.Version != tt.version {
   917  				t.Fatalf("Latest() = %v, want %v", info.Version, tt.version)
   918  			}
   919  		})
   920  	}
   921  }
   922  
   923  // fixedTagsRepo is a fake codehost.Repo that returns a fixed list of tags
   924  type fixedTagsRepo struct {
   925  	tags []string
   926  	codehost.Repo
   927  }
   928  
   929  func (ch *fixedTagsRepo) Tags(ctx context.Context, prefix string) (*codehost.Tags, error) {
   930  	tags := &codehost.Tags{}
   931  	for _, t := range ch.tags {
   932  		tags.List = append(tags.List, codehost.Tag{Name: t})
   933  	}
   934  	return tags, nil
   935  }
   936  
   937  func TestNonCanonicalSemver(t *testing.T) {
   938  	t.Parallel()
   939  	ctx := context.Background()
   940  
   941  	root := "golang.org/x/issue24476"
   942  	ch := &fixedTagsRepo{
   943  		tags: []string{
   944  			"", "huh?", "1.0.1",
   945  			// what about "version 1 dot dogcow"?
   946  			"v1.🐕.🐄",
   947  			"v1", "v0.1",
   948  			// and one normal one that should pass through
   949  			"v1.0.1",
   950  		},
   951  	}
   952  
   953  	cr, err := newCodeRepo(ch, root, root)
   954  	if err != nil {
   955  		t.Fatal(err)
   956  	}
   957  
   958  	v, err := cr.Versions(ctx, "")
   959  	if err != nil {
   960  		t.Fatal(err)
   961  	}
   962  	if len(v.List) != 1 || v.List[0] != "v1.0.1" {
   963  		t.Fatal("unexpected versions returned:", v)
   964  	}
   965  }
   966  

View as plain text