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