github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/modfetch/codehost/git_test.go (about)

     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 codehost
     6  
     7  import (
     8  	"archive/zip"
     9  	"bytes"
    10  	"context"
    11  	"flag"
    12  	"io"
    13  	"io/fs"
    14  	"log"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"reflect"
    19  	"runtime"
    20  	"strings"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/go-asm/go/cmd/go/cfg"
    26  	"github.com/go-asm/go/cmd/go/vcweb/vcstest"
    27  	"github.com/go-asm/go/testenv"
    28  )
    29  
    30  func TestMain(m *testing.M) {
    31  	flag.Parse()
    32  	if err := testMain(m); err != nil {
    33  		log.Fatal(err)
    34  	}
    35  }
    36  
    37  var gitrepo1, hgrepo1, vgotest1 string
    38  
    39  var altRepos = func() []string {
    40  	return []string{
    41  		"localGitRepo",
    42  		hgrepo1,
    43  	}
    44  }
    45  
    46  // TODO: Convert gitrepo1 to svn, bzr, fossil and add tests.
    47  // For now, at least the hgrepo1 tests check the general vcs.go logic.
    48  
    49  // localGitRepo is like gitrepo1 but allows archive access
    50  // (although that doesn't really matter after CL 120041),
    51  // and has a file:// URL instead of http:// or https://
    52  // (which might still matter).
    53  var localGitRepo string
    54  
    55  // localGitURL initializes the repo in localGitRepo and returns its URL.
    56  func localGitURL(t testing.TB) string {
    57  	testenv.MustHaveExecPath(t, "git")
    58  	if runtime.GOOS == "android" && strings.HasSuffix(testenv.Builder(), "-corellium") {
    59  		testenv.SkipFlaky(t, 59940)
    60  	}
    61  
    62  	localGitURLOnce.Do(func() {
    63  		// Clone gitrepo1 into a local directory.
    64  		// If we use a file:// URL to access the local directory,
    65  		// then git starts up all the usual protocol machinery,
    66  		// which will let us test remote git archive invocations.
    67  		_, localGitURLErr = Run(context.Background(), "", "git", "clone", "--mirror", gitrepo1, localGitRepo)
    68  		if localGitURLErr != nil {
    69  			return
    70  		}
    71  		_, localGitURLErr = Run(context.Background(), localGitRepo, "git", "config", "daemon.uploadarch", "true")
    72  	})
    73  
    74  	if localGitURLErr != nil {
    75  		t.Fatal(localGitURLErr)
    76  	}
    77  	// Convert absolute path to file URL. LocalGitRepo will not accept
    78  	// Windows absolute paths because they look like a host:path remote.
    79  	// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
    80  	if strings.HasPrefix(localGitRepo, "/") {
    81  		return "file://" + localGitRepo
    82  	} else {
    83  		return "file:///" + filepath.ToSlash(localGitRepo)
    84  	}
    85  }
    86  
    87  var (
    88  	localGitURLOnce sync.Once
    89  	localGitURLErr  error
    90  )
    91  
    92  func testMain(m *testing.M) (err error) {
    93  	cfg.BuildX = testing.Verbose()
    94  
    95  	srv, err := vcstest.NewServer()
    96  	if err != nil {
    97  		return err
    98  	}
    99  	defer func() {
   100  		if closeErr := srv.Close(); err == nil {
   101  			err = closeErr
   102  		}
   103  	}()
   104  
   105  	gitrepo1 = srv.HTTP.URL + "/git/gitrepo1"
   106  	hgrepo1 = srv.HTTP.URL + "/hg/hgrepo1"
   107  	vgotest1 = srv.HTTP.URL + "/git/vgotest1"
   108  
   109  	dir, err := os.MkdirTemp("", "gitrepo-test-")
   110  	if err != nil {
   111  		return err
   112  	}
   113  	defer func() {
   114  		if rmErr := os.RemoveAll(dir); err == nil {
   115  			err = rmErr
   116  		}
   117  	}()
   118  
   119  	localGitRepo = filepath.Join(dir, "gitrepo2")
   120  
   121  	// Redirect the module cache to a fresh directory to avoid crosstalk, and make
   122  	// it read/write so that the test can still clean it up easily when done.
   123  	cfg.GOMODCACHE = filepath.Join(dir, "modcache")
   124  	cfg.ModCacheRW = true
   125  
   126  	m.Run()
   127  	return nil
   128  }
   129  
   130  func testContext(t testing.TB) context.Context {
   131  	w := newTestWriter(t)
   132  	return cfg.WithBuildXWriter(context.Background(), w)
   133  }
   134  
   135  // A testWriter is an io.Writer that writes to a test's log.
   136  //
   137  // The writer batches written data until the last byte of a write is a newline
   138  // character, then flushes the batched data as a single call to Logf.
   139  // Any remaining unflushed data is logged during Cleanup.
   140  type testWriter struct {
   141  	t testing.TB
   142  
   143  	mu  sync.Mutex
   144  	buf bytes.Buffer
   145  }
   146  
   147  func newTestWriter(t testing.TB) *testWriter {
   148  	w := &testWriter{t: t}
   149  
   150  	t.Cleanup(func() {
   151  		w.mu.Lock()
   152  		defer w.mu.Unlock()
   153  		if b := w.buf.Bytes(); len(b) > 0 {
   154  			w.t.Logf("%s", b)
   155  			w.buf.Reset()
   156  		}
   157  	})
   158  
   159  	return w
   160  }
   161  
   162  func (w *testWriter) Write(p []byte) (int, error) {
   163  	w.mu.Lock()
   164  	defer w.mu.Unlock()
   165  	n, err := w.buf.Write(p)
   166  	if b := w.buf.Bytes(); len(b) > 0 && b[len(b)-1] == '\n' {
   167  		w.t.Logf("%s", b)
   168  		w.buf.Reset()
   169  	}
   170  	return n, err
   171  }
   172  
   173  func testRepo(ctx context.Context, t *testing.T, remote string) (Repo, error) {
   174  	if remote == "localGitRepo" {
   175  		return LocalGitRepo(ctx, localGitURL(t))
   176  	}
   177  	vcsName := "git"
   178  	for _, k := range []string{"hg"} {
   179  		if strings.Contains(remote, "/"+k+"/") {
   180  			vcsName = k
   181  		}
   182  	}
   183  	if testing.Short() && vcsName == "hg" {
   184  		t.Skipf("skipping hg test in short mode: hg is slow")
   185  	}
   186  	testenv.MustHaveExecPath(t, vcsName)
   187  	if runtime.GOOS == "android" && strings.HasSuffix(testenv.Builder(), "-corellium") {
   188  		testenv.SkipFlaky(t, 59940)
   189  	}
   190  	return NewRepo(ctx, vcsName, remote)
   191  }
   192  
   193  func TestTags(t *testing.T) {
   194  	t.Parallel()
   195  
   196  	type tagsTest struct {
   197  		repo   string
   198  		prefix string
   199  		tags   []Tag
   200  	}
   201  
   202  	runTest := func(tt tagsTest) func(*testing.T) {
   203  		return func(t *testing.T) {
   204  			t.Parallel()
   205  			ctx := testContext(t)
   206  
   207  			r, err := testRepo(ctx, t, tt.repo)
   208  			if err != nil {
   209  				t.Fatal(err)
   210  			}
   211  			tags, err := r.Tags(ctx, tt.prefix)
   212  			if err != nil {
   213  				t.Fatal(err)
   214  			}
   215  			if tags == nil || !reflect.DeepEqual(tags.List, tt.tags) {
   216  				t.Errorf("Tags(%q): incorrect tags\nhave %v\nwant %v", tt.prefix, tags, tt.tags)
   217  			}
   218  		}
   219  	}
   220  
   221  	for _, tt := range []tagsTest{
   222  		{gitrepo1, "xxx", []Tag{}},
   223  		{gitrepo1, "", []Tag{
   224  			{"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
   225  			{"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
   226  			{"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
   227  			{"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"},
   228  			{"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
   229  		}},
   230  		{gitrepo1, "v", []Tag{
   231  			{"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
   232  			{"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
   233  			{"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
   234  			{"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"},
   235  			{"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
   236  		}},
   237  		{gitrepo1, "v1", []Tag{
   238  			{"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
   239  			{"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
   240  		}},
   241  		{gitrepo1, "2", []Tag{}},
   242  	} {
   243  		t.Run(path.Base(tt.repo)+"/"+tt.prefix, runTest(tt))
   244  		if tt.repo == gitrepo1 {
   245  			// Clear hashes.
   246  			clearTags := []Tag{}
   247  			for _, tag := range tt.tags {
   248  				clearTags = append(clearTags, Tag{tag.Name, ""})
   249  			}
   250  			tags := tt.tags
   251  			for _, tt.repo = range altRepos() {
   252  				if strings.Contains(tt.repo, "Git") {
   253  					tt.tags = tags
   254  				} else {
   255  					tt.tags = clearTags
   256  				}
   257  				t.Run(path.Base(tt.repo)+"/"+tt.prefix, runTest(tt))
   258  			}
   259  		}
   260  	}
   261  }
   262  
   263  func TestLatest(t *testing.T) {
   264  	t.Parallel()
   265  
   266  	type latestTest struct {
   267  		repo string
   268  		info *RevInfo
   269  	}
   270  	runTest := func(tt latestTest) func(*testing.T) {
   271  		return func(t *testing.T) {
   272  			t.Parallel()
   273  			ctx := testContext(t)
   274  
   275  			r, err := testRepo(ctx, t, tt.repo)
   276  			if err != nil {
   277  				t.Fatal(err)
   278  			}
   279  			info, err := r.Latest(ctx)
   280  			if err != nil {
   281  				t.Fatal(err)
   282  			}
   283  			if !reflect.DeepEqual(info, tt.info) {
   284  				t.Errorf("Latest: incorrect info\nhave %+v (origin %+v)\nwant %+v (origin %+v)", info, info.Origin, tt.info, tt.info.Origin)
   285  			}
   286  		}
   287  	}
   288  
   289  	for _, tt := range []latestTest{
   290  		{
   291  			gitrepo1,
   292  			&RevInfo{
   293  				Origin: &Origin{
   294  					VCS:  "git",
   295  					URL:  gitrepo1,
   296  					Ref:  "HEAD",
   297  					Hash: "ede458df7cd0fdca520df19a33158086a8a68e81",
   298  				},
   299  				Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
   300  				Short:   "ede458df7cd0",
   301  				Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
   302  				Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
   303  				Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
   304  			},
   305  		},
   306  		{
   307  			hgrepo1,
   308  			&RevInfo{
   309  				Origin: &Origin{
   310  					VCS:  "hg",
   311  					URL:  hgrepo1,
   312  					Hash: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
   313  				},
   314  				Name:    "18518c07eb8ed5c80221e997e518cccaa8c0c287",
   315  				Short:   "18518c07eb8e",
   316  				Version: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
   317  				Time:    time.Date(2018, 6, 27, 16, 16, 30, 0, time.UTC),
   318  			},
   319  		},
   320  	} {
   321  		t.Run(path.Base(tt.repo), runTest(tt))
   322  		if tt.repo == gitrepo1 {
   323  			tt.repo = "localGitRepo"
   324  			info := *tt.info
   325  			tt.info = &info
   326  			o := *info.Origin
   327  			info.Origin = &o
   328  			o.URL = localGitURL(t)
   329  			t.Run(path.Base(tt.repo), runTest(tt))
   330  		}
   331  	}
   332  }
   333  
   334  func TestReadFile(t *testing.T) {
   335  	t.Parallel()
   336  
   337  	type readFileTest struct {
   338  		repo string
   339  		rev  string
   340  		file string
   341  		err  string
   342  		data string
   343  	}
   344  	runTest := func(tt readFileTest) func(*testing.T) {
   345  		return func(t *testing.T) {
   346  			t.Parallel()
   347  			ctx := testContext(t)
   348  
   349  			r, err := testRepo(ctx, t, tt.repo)
   350  			if err != nil {
   351  				t.Fatal(err)
   352  			}
   353  			data, err := r.ReadFile(ctx, tt.rev, tt.file, 100)
   354  			if err != nil {
   355  				if tt.err == "" {
   356  					t.Fatalf("ReadFile: unexpected error %v", err)
   357  				}
   358  				if !strings.Contains(err.Error(), tt.err) {
   359  					t.Fatalf("ReadFile: wrong error %q, want %q", err, tt.err)
   360  				}
   361  				if len(data) != 0 {
   362  					t.Errorf("ReadFile: non-empty data %q with error %v", data, err)
   363  				}
   364  				return
   365  			}
   366  			if tt.err != "" {
   367  				t.Fatalf("ReadFile: no error, wanted %v", tt.err)
   368  			}
   369  			if string(data) != tt.data {
   370  				t.Errorf("ReadFile: incorrect data\nhave %q\nwant %q", data, tt.data)
   371  			}
   372  		}
   373  	}
   374  
   375  	for _, tt := range []readFileTest{
   376  		{
   377  			repo: gitrepo1,
   378  			rev:  "latest",
   379  			file: "README",
   380  			data: "",
   381  		},
   382  		{
   383  			repo: gitrepo1,
   384  			rev:  "v2",
   385  			file: "another.txt",
   386  			data: "another\n",
   387  		},
   388  		{
   389  			repo: gitrepo1,
   390  			rev:  "v2.3.4",
   391  			file: "another.txt",
   392  			err:  fs.ErrNotExist.Error(),
   393  		},
   394  	} {
   395  		t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, runTest(tt))
   396  		if tt.repo == gitrepo1 {
   397  			for _, tt.repo = range altRepos() {
   398  				t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, runTest(tt))
   399  			}
   400  		}
   401  	}
   402  }
   403  
   404  type zipFile struct {
   405  	name string
   406  	size int64
   407  }
   408  
   409  func TestReadZip(t *testing.T) {
   410  	t.Parallel()
   411  
   412  	type readZipTest struct {
   413  		repo   string
   414  		rev    string
   415  		subdir string
   416  		err    string
   417  		files  map[string]uint64
   418  	}
   419  	runTest := func(tt readZipTest) func(*testing.T) {
   420  		return func(t *testing.T) {
   421  			t.Parallel()
   422  			ctx := testContext(t)
   423  
   424  			r, err := testRepo(ctx, t, tt.repo)
   425  			if err != nil {
   426  				t.Fatal(err)
   427  			}
   428  			rc, err := r.ReadZip(ctx, tt.rev, tt.subdir, 100000)
   429  			if err != nil {
   430  				if tt.err == "" {
   431  					t.Fatalf("ReadZip: unexpected error %v", err)
   432  				}
   433  				if !strings.Contains(err.Error(), tt.err) {
   434  					t.Fatalf("ReadZip: wrong error %q, want %q", err, tt.err)
   435  				}
   436  				if rc != nil {
   437  					t.Errorf("ReadZip: non-nil io.ReadCloser with error %v", err)
   438  				}
   439  				return
   440  			}
   441  			defer rc.Close()
   442  			if tt.err != "" {
   443  				t.Fatalf("ReadZip: no error, wanted %v", tt.err)
   444  			}
   445  			zipdata, err := io.ReadAll(rc)
   446  			if err != nil {
   447  				t.Fatal(err)
   448  			}
   449  			z, err := zip.NewReader(bytes.NewReader(zipdata), int64(len(zipdata)))
   450  			if err != nil {
   451  				t.Fatalf("ReadZip: cannot read zip file: %v", err)
   452  			}
   453  			have := make(map[string]bool)
   454  			for _, f := range z.File {
   455  				size, ok := tt.files[f.Name]
   456  				if !ok {
   457  					t.Errorf("ReadZip: unexpected file %s", f.Name)
   458  					continue
   459  				}
   460  				have[f.Name] = true
   461  				if size != ^uint64(0) && f.UncompressedSize64 != size {
   462  					t.Errorf("ReadZip: file %s has unexpected size %d != %d", f.Name, f.UncompressedSize64, size)
   463  				}
   464  			}
   465  			for name := range tt.files {
   466  				if !have[name] {
   467  					t.Errorf("ReadZip: missing file %s", name)
   468  				}
   469  			}
   470  		}
   471  	}
   472  
   473  	for _, tt := range []readZipTest{
   474  		{
   475  			repo:   gitrepo1,
   476  			rev:    "v2.3.4",
   477  			subdir: "",
   478  			files: map[string]uint64{
   479  				"prefix/":       0,
   480  				"prefix/README": 0,
   481  				"prefix/v2":     3,
   482  			},
   483  		},
   484  		{
   485  			repo:   hgrepo1,
   486  			rev:    "v2.3.4",
   487  			subdir: "",
   488  			files: map[string]uint64{
   489  				"prefix/.hg_archival.txt": ^uint64(0),
   490  				"prefix/README":           0,
   491  				"prefix/v2":               3,
   492  			},
   493  		},
   494  
   495  		{
   496  			repo:   gitrepo1,
   497  			rev:    "v2",
   498  			subdir: "",
   499  			files: map[string]uint64{
   500  				"prefix/":            0,
   501  				"prefix/README":      0,
   502  				"prefix/v2":          3,
   503  				"prefix/another.txt": 8,
   504  				"prefix/foo.txt":     13,
   505  			},
   506  		},
   507  		{
   508  			repo:   hgrepo1,
   509  			rev:    "v2",
   510  			subdir: "",
   511  			files: map[string]uint64{
   512  				"prefix/.hg_archival.txt": ^uint64(0),
   513  				"prefix/README":           0,
   514  				"prefix/v2":               3,
   515  				"prefix/another.txt":      8,
   516  				"prefix/foo.txt":          13,
   517  			},
   518  		},
   519  
   520  		{
   521  			repo:   gitrepo1,
   522  			rev:    "v3",
   523  			subdir: "",
   524  			files: map[string]uint64{
   525  				"prefix/":                    0,
   526  				"prefix/v3/":                 0,
   527  				"prefix/v3/sub/":             0,
   528  				"prefix/v3/sub/dir/":         0,
   529  				"prefix/v3/sub/dir/file.txt": 16,
   530  				"prefix/README":              0,
   531  			},
   532  		},
   533  		{
   534  			repo:   hgrepo1,
   535  			rev:    "v3",
   536  			subdir: "",
   537  			files: map[string]uint64{
   538  				"prefix/.hg_archival.txt":    ^uint64(0),
   539  				"prefix/.hgtags":             405,
   540  				"prefix/v3/sub/dir/file.txt": 16,
   541  				"prefix/README":              0,
   542  			},
   543  		},
   544  
   545  		{
   546  			repo:   gitrepo1,
   547  			rev:    "v3",
   548  			subdir: "v3/sub/dir",
   549  			files: map[string]uint64{
   550  				"prefix/":                    0,
   551  				"prefix/v3/":                 0,
   552  				"prefix/v3/sub/":             0,
   553  				"prefix/v3/sub/dir/":         0,
   554  				"prefix/v3/sub/dir/file.txt": 16,
   555  			},
   556  		},
   557  		{
   558  			repo:   hgrepo1,
   559  			rev:    "v3",
   560  			subdir: "v3/sub/dir",
   561  			files: map[string]uint64{
   562  				"prefix/v3/sub/dir/file.txt": 16,
   563  			},
   564  		},
   565  
   566  		{
   567  			repo:   gitrepo1,
   568  			rev:    "v3",
   569  			subdir: "v3/sub",
   570  			files: map[string]uint64{
   571  				"prefix/":                    0,
   572  				"prefix/v3/":                 0,
   573  				"prefix/v3/sub/":             0,
   574  				"prefix/v3/sub/dir/":         0,
   575  				"prefix/v3/sub/dir/file.txt": 16,
   576  			},
   577  		},
   578  		{
   579  			repo:   hgrepo1,
   580  			rev:    "v3",
   581  			subdir: "v3/sub",
   582  			files: map[string]uint64{
   583  				"prefix/v3/sub/dir/file.txt": 16,
   584  			},
   585  		},
   586  
   587  		{
   588  			repo:   gitrepo1,
   589  			rev:    "aaaaaaaaab",
   590  			subdir: "",
   591  			err:    "unknown revision",
   592  		},
   593  		{
   594  			repo:   hgrepo1,
   595  			rev:    "aaaaaaaaab",
   596  			subdir: "",
   597  			err:    "unknown revision",
   598  		},
   599  
   600  		{
   601  			repo:   vgotest1,
   602  			rev:    "submod/v1.0.4",
   603  			subdir: "submod",
   604  			files: map[string]uint64{
   605  				"prefix/":                0,
   606  				"prefix/submod/":         0,
   607  				"prefix/submod/go.mod":   53,
   608  				"prefix/submod/pkg/":     0,
   609  				"prefix/submod/pkg/p.go": 31,
   610  			},
   611  		},
   612  	} {
   613  		t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, runTest(tt))
   614  		if tt.repo == gitrepo1 {
   615  			tt.repo = "localGitRepo"
   616  			t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, runTest(tt))
   617  		}
   618  	}
   619  }
   620  
   621  var hgmap = map[string]string{
   622  	"HEAD": "41964ddce1180313bdc01d0a39a2813344d6261d", // not tip due to bad hgrepo1 conversion
   623  	"9d02800338b8a55be062c838d1f02e0c5780b9eb": "8f49ee7a6ddcdec6f0112d9dca48d4a2e4c3c09e",
   624  	"76a00fb249b7f93091bc2c89a789dab1fc1bc26f": "88fde824ec8b41a76baa16b7e84212cee9f3edd0",
   625  	"ede458df7cd0fdca520df19a33158086a8a68e81": "41964ddce1180313bdc01d0a39a2813344d6261d",
   626  	"97f6aa59c81c623494825b43d39e445566e429a4": "c0cbbfb24c7c3c50c35c7b88e7db777da4ff625d",
   627  }
   628  
   629  func TestStat(t *testing.T) {
   630  	t.Parallel()
   631  
   632  	type statTest struct {
   633  		repo string
   634  		rev  string
   635  		err  string
   636  		info *RevInfo
   637  	}
   638  	runTest := func(tt statTest) func(*testing.T) {
   639  		return func(t *testing.T) {
   640  			t.Parallel()
   641  			ctx := testContext(t)
   642  
   643  			r, err := testRepo(ctx, t, tt.repo)
   644  			if err != nil {
   645  				t.Fatal(err)
   646  			}
   647  			info, err := r.Stat(ctx, tt.rev)
   648  			if err != nil {
   649  				if tt.err == "" {
   650  					t.Fatalf("Stat: unexpected error %v", err)
   651  				}
   652  				if !strings.Contains(err.Error(), tt.err) {
   653  					t.Fatalf("Stat: wrong error %q, want %q", err, tt.err)
   654  				}
   655  				if info != nil && info.Origin == nil {
   656  					t.Errorf("Stat: non-nil info with nil Origin with error %q", err)
   657  				}
   658  				return
   659  			}
   660  			info.Origin = nil // TestLatest and ../../../testdata/script/reuse_git.txt test Origin well enough
   661  			if !reflect.DeepEqual(info, tt.info) {
   662  				t.Errorf("Stat: incorrect info\nhave %+v\nwant %+v", *info, *tt.info)
   663  			}
   664  		}
   665  	}
   666  
   667  	for _, tt := range []statTest{
   668  		{
   669  			repo: gitrepo1,
   670  			rev:  "HEAD",
   671  			info: &RevInfo{
   672  				Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
   673  				Short:   "ede458df7cd0",
   674  				Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
   675  				Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
   676  				Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
   677  			},
   678  		},
   679  		{
   680  			repo: gitrepo1,
   681  			rev:  "v2", // branch
   682  			info: &RevInfo{
   683  				Name:    "9d02800338b8a55be062c838d1f02e0c5780b9eb",
   684  				Short:   "9d02800338b8",
   685  				Version: "9d02800338b8a55be062c838d1f02e0c5780b9eb",
   686  				Time:    time.Date(2018, 4, 17, 20, 00, 32, 0, time.UTC),
   687  				Tags:    []string{"v2.0.2"},
   688  			},
   689  		},
   690  		{
   691  			repo: gitrepo1,
   692  			rev:  "v2.3.4", // badly-named branch (semver should be a tag)
   693  			info: &RevInfo{
   694  				Name:    "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
   695  				Short:   "76a00fb249b7",
   696  				Version: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
   697  				Time:    time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC),
   698  				Tags:    []string{"v2.0.1", "v2.3"},
   699  			},
   700  		},
   701  		{
   702  			repo: gitrepo1,
   703  			rev:  "v2.3", // badly-named tag (we only respect full semver v2.3.0)
   704  			info: &RevInfo{
   705  				Name:    "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
   706  				Short:   "76a00fb249b7",
   707  				Version: "v2.3",
   708  				Time:    time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC),
   709  				Tags:    []string{"v2.0.1", "v2.3"},
   710  			},
   711  		},
   712  		{
   713  			repo: gitrepo1,
   714  			rev:  "v1.2.3", // tag
   715  			info: &RevInfo{
   716  				Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
   717  				Short:   "ede458df7cd0",
   718  				Version: "v1.2.3",
   719  				Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
   720  				Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
   721  			},
   722  		},
   723  		{
   724  			repo: gitrepo1,
   725  			rev:  "ede458df", // hash prefix in refs
   726  			info: &RevInfo{
   727  				Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
   728  				Short:   "ede458df7cd0",
   729  				Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
   730  				Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
   731  				Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
   732  			},
   733  		},
   734  		{
   735  			repo: gitrepo1,
   736  			rev:  "97f6aa59", // hash prefix not in refs
   737  			info: &RevInfo{
   738  				Name:    "97f6aa59c81c623494825b43d39e445566e429a4",
   739  				Short:   "97f6aa59c81c",
   740  				Version: "97f6aa59c81c623494825b43d39e445566e429a4",
   741  				Time:    time.Date(2018, 4, 17, 20, 0, 19, 0, time.UTC),
   742  			},
   743  		},
   744  		{
   745  			repo: gitrepo1,
   746  			rev:  "v1.2.4-annotated", // annotated tag uses unwrapped commit hash
   747  			info: &RevInfo{
   748  				Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
   749  				Short:   "ede458df7cd0",
   750  				Version: "v1.2.4-annotated",
   751  				Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
   752  				Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
   753  			},
   754  		},
   755  		{
   756  			repo: gitrepo1,
   757  			rev:  "aaaaaaaaab",
   758  			err:  "unknown revision",
   759  		},
   760  	} {
   761  		t.Run(path.Base(tt.repo)+"/"+tt.rev, runTest(tt))
   762  		if tt.repo == gitrepo1 {
   763  			for _, tt.repo = range altRepos() {
   764  				old := tt
   765  				var m map[string]string
   766  				if tt.repo == hgrepo1 {
   767  					m = hgmap
   768  				}
   769  				if tt.info != nil {
   770  					info := *tt.info
   771  					tt.info = &info
   772  					tt.info.Name = remap(tt.info.Name, m)
   773  					tt.info.Version = remap(tt.info.Version, m)
   774  					tt.info.Short = remap(tt.info.Short, m)
   775  				}
   776  				tt.rev = remap(tt.rev, m)
   777  				t.Run(path.Base(tt.repo)+"/"+tt.rev, runTest(tt))
   778  				tt = old
   779  			}
   780  		}
   781  	}
   782  }
   783  
   784  func remap(name string, m map[string]string) string {
   785  	if m[name] != "" {
   786  		return m[name]
   787  	}
   788  	if AllHex(name) {
   789  		for k, v := range m {
   790  			if strings.HasPrefix(k, name) {
   791  				return v[:len(name)]
   792  			}
   793  		}
   794  	}
   795  	return name
   796  }