checkout_test.go

  1package checkout
  2
  3import (
  4	"context"
  5	"os"
  6	"os/exec"
  7	"path/filepath"
  8	"strings"
  9	"testing"
 10
 11	"git.kilimanjaro.io/ci/internal/lex"
 12)
 13
 14func sh(t *testing.T, dir string, args ...string) string {
 15	t.Helper()
 16	cmd := exec.Command(args[0], args[1:]...)
 17	cmd.Dir = dir
 18	cmd.Env = append(os.Environ(),
 19		"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t",
 20		"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
 21	out, err := cmd.CombinedOutput()
 22	if err != nil {
 23		t.Fatalf("%v: %v\n%s", args, err, out)
 24	}
 25	return strings.TrimSpace(string(out))
 26}
 27
 28// forge makes a bare repo <forge>/<name>.git with commits writing file=v1
 29// then file=v2 on main, and a tag v1 at the first. It returns the shas.
 30func forge(t *testing.T, dir, name string) (v1, v2 string) {
 31	t.Helper()
 32	work := filepath.Join(t.TempDir(), name)
 33	if err := os.MkdirAll(work, 0o755); err != nil {
 34		t.Fatal(err)
 35	}
 36	sh(t, work, "git", "init", "-q", "-b", "main")
 37	if err := os.WriteFile(filepath.Join(work, "file"), []byte("v1"), 0o644); err != nil {
 38		t.Fatal(err)
 39	}
 40	sh(t, work, "git", "add", "file")
 41	sh(t, work, "git", "commit", "-qm", "v1")
 42	v1 = sh(t, work, "git", "rev-parse", "HEAD")
 43	sh(t, work, "git", "tag", "v1")
 44	if err := os.WriteFile(filepath.Join(work, "file"), []byte("v2"), 0o644); err != nil {
 45		t.Fatal(err)
 46	}
 47	sh(t, work, "git", "commit", "-qam", "v2")
 48	v2 = sh(t, work, "git", "rev-parse", "HEAD")
 49	bare := filepath.Join(dir, name+".git")
 50	sh(t, dir, "git", "clone", "-q", "--bare", work, bare)
 51	return v1, v2
 52}
 53
 54func read(t *testing.T, p string) string {
 55	t.Helper()
 56	b, err := os.ReadFile(p)
 57	if err != nil {
 58		t.Fatal(err)
 59	}
 60	return string(b)
 61}
 62
 63func TestLayout(t *testing.T) {
 64	t.Setenv("GIT_CONFIG_GLOBAL", "/dev/null")
 65	ctx := context.Background()
 66	fdir := t.TempDir()
 67	m1, m2 := forge(t, fdir, "main")
 68	_, _ = forge(t, fdir, "servers")
 69	d1, _ := forge(t, fdir, "data")
 70	src := &Source{MirrorsDir: filepath.Join(t.TempDir(), "mirrors"), URL: URLFunc(filepath.Join(fdir, "{repo}.git"), "")}
 71
 72	run := filepath.Join(t.TempDir(), "run")
 73	mainMirror, err := src.Mirror(ctx, "main")
 74	if err != nil {
 75		t.Fatal(err)
 76	}
 77	local := t.TempDir()
 78	got, err := Layout(ctx, src, run, Main{Repo: "main", From: mainMirror, Sha: m1, Origin: "git@forge:main"},
 79		[]lex.Repo{
 80			{Repo: "servers", Path: "../servers"},
 81			{Repo: "data", Path: "../data", Rev: "v1", Env: "SECRETS_DIR"},
 82			{Repo: "tools", Path: "../tools"},
 83		}, map[string]string{"tools": local})
 84	if err != nil {
 85		t.Fatal(err)
 86	}
 87	if len(got) != 4 {
 88		t.Fatalf("%+v", got)
 89	}
 90	if read(t, filepath.Join(run, "main", "file")) != "v1" || got[0].Rev != m1 {
 91		t.Errorf("main checkout not at v1: %+v", got[0])
 92	}
 93	if read(t, filepath.Join(run, "servers", "file")) != "v2" {
 94		t.Error("servers not at the default branch")
 95	}
 96	if got[2].Rev != d1 || got[2].Env != "SECRETS_DIR" || read(t, filepath.Join(run, "data", "file")) != "v1" {
 97		t.Errorf("data: %+v", got[2])
 98	}
 99	if p, _ := os.Readlink(filepath.Join(run, "tools")); p != local || !got[3].Link {
100		t.Errorf("tools override: %q %+v", p, got[3])
101	}
102	if u := sh(t, filepath.Join(run, "main"), "git", "remote", "get-url", "origin"); u != "git@forge:main" {
103		t.Errorf("origin %q", u)
104	}
105	if u := sh(t, filepath.Join(run, "servers"), "git", "remote", "get-url", "origin"); u != filepath.Join(fdir, "servers.git") {
106		t.Errorf("servers origin %q", u)
107	}
108	// the checkout borrows objects; nothing was registered in the mirror
109	if _, err := os.Stat(filepath.Join(mainMirror, "worktrees")); err == nil {
110		t.Error("mirror has worktree metadata")
111	}
112	_ = m2
113}
114
115func TestMirrorFetchesNewCommits(t *testing.T) {
116	t.Setenv("GIT_CONFIG_GLOBAL", "/dev/null")
117	ctx := context.Background()
118	fdir := t.TempDir()
119	_, v2 := forge(t, fdir, "r")
120	src := &Source{MirrorsDir: t.TempDir(), URL: URLFunc(filepath.Join(fdir, "{repo}.git"), "")}
121	m, err := src.Mirror(ctx, "r")
122	if err != nil {
123		t.Fatal(err)
124	}
125	if sha, _ := Resolve(ctx, m, ""); sha != v2 {
126		t.Fatalf("HEAD %s want %s", sha, v2)
127	}
128	// push a new commit to the forge; the next Mirror fetches it
129	work := filepath.Join(t.TempDir(), "w")
130	sh(t, "", "git", "clone", "-q", filepath.Join(fdir, "r.git"), work)
131	if err := os.WriteFile(filepath.Join(work, "file"), []byte("v3"), 0o644); err != nil {
132		t.Fatal(err)
133	}
134	sh(t, work, "git", "commit", "-qam", "v3")
135	sh(t, work, "git", "push", "-q", "origin", "main")
136	v3 := sh(t, work, "git", "rev-parse", "HEAD")
137	if _, err := src.Mirror(ctx, "r"); err != nil {
138		t.Fatal(err)
139	}
140	if sha, _ := Resolve(ctx, m, ""); sha != v3 {
141		t.Errorf("after fetch HEAD %s want %s", sha, v3)
142	}
143	if _, err := Resolve(ctx, m, "nope"); err == nil {
144		t.Error("resolved a missing rev")
145	}
146}
147
148func TestLayoutRejectsBadPaths(t *testing.T) {
149	ctx := context.Background()
150	src := &Source{MirrorsDir: t.TempDir(), URL: URLFunc("/nowhere/{repo}.git", "")}
151	wc := t.TempDir()
152	for _, p := range []string{"../../escape", "sub", "../main", "."} {
153		run := filepath.Join(t.TempDir(), "run")
154		_, err := Layout(ctx, src, run, Main{Repo: "main", WorkingCopy: wc}, []lex.Repo{{Repo: "x", Path: p}}, nil)
155		if err == nil || !strings.Contains(err.Error(), "sibling") {
156			t.Errorf("path %q: %v", p, err)
157		}
158	}
159	run := filepath.Join(t.TempDir(), "run")
160	if _, err := Layout(ctx, src, run, Main{Repo: "main", WorkingCopy: wc}, nil, map[string]string{"x": wc}); err == nil {
161		t.Error("override for an unknown repo accepted")
162	}
163}
164
165func TestURLs(t *testing.T) {
166	cases := map[string]string{
167		"[email protected]:sovrn":           "[email protected]:servers",
168		"ssh://git@host/u/sovrn":                 "ssh://git@host/u/servers",
169		"https://host/u/sovrn.git":               "https://host/u/servers.git",
170		"git+ssh://[email protected]/sovrn": "git+ssh://[email protected]/servers",
171	}
172	for of, want := range cases {
173		if got, err := SiblingURL(of, "servers"); err != nil || got != want {
174			t.Errorf("SiblingURL(%q) = %q, %v", of, got, err)
175		}
176	}
177	for in, want := range map[string]string{"git@h:sovrn.git": "sovrn", "ssh://h/u/moods/": "moods", "https://h/x.git": "x", "": ""} {
178		if got := RepoName(in); got != want {
179			t.Errorf("RepoName(%q) = %q", in, got)
180		}
181	}
182	if u, _ := URLFunc("git@h:{repo}", "x")("data"); u != "git@h:data" {
183		t.Errorf("template: %s", u)
184	}
185	if _, err := URLFunc("", "")("data"); err == nil {
186		t.Error("no origin and no template accepted")
187	}
188	if _, err := URLFunc("git@h:fixed", "")("data"); err == nil {
189		t.Error("template without {repo} accepted")
190	}
191}